Skip to content

Commit

Permalink
Convert import_indexstores.sh into Swift binary
Browse files Browse the repository at this point in the history
This is mainly to be able to do more complex stuff in the near future for incremental generators. It has the side benefit of being faster.

Signed-off-by: Brentley Jones <[email protected]>
  • Loading branch information
brentleyjones committed Oct 10, 2023
1 parent cbc9042 commit aa7a7ee
Show file tree
Hide file tree
Showing 17 changed files with 439 additions and 134 deletions.
3 changes: 3 additions & 0 deletions distribution/BUILD
Original file line number Diff line number Diff line change
Expand Up @@ -52,6 +52,9 @@ pkg_tar(
remap_paths = dicts.add(
{
"MODULE.release.bazel": "MODULE.bazel",
"tools/import_indexstores/BUILD.release.bazel": (
"tools/import_indexstores/BUILD"
),
"tools/swiftc_stub/BUILD.release.bazel": "tools/swiftc_stub/BUILD",
"xcodeproj/repositories.release.bzl": "xcodeproj/repositories.bzl",
},
Expand Down
File renamed without changes.
File renamed without changes.
File renamed without changes.
20 changes: 20 additions & 0 deletions tools/BUILD
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ load("//xcodeproj/internal:collections.bzl", "flatten", "uniq")

_TOOLS = {
"files_and_groups": "//tools/generators/files_and_groups",
"import_indexstores": "//tools/import_indexstores",
"pbxproj_prefix": "//tools/generators/pbxproj_prefix",
"pbxtargetdependencies": "//tools/generators/pbxtargetdependencies",
"swiftc_stub": "//tools/swiftc_stub:swiftc",
Expand Down Expand Up @@ -75,6 +76,24 @@ _SCHEMES = [
diagnostics = _SCHEME_DIAGNOSTICS,
),
),
xcode_schemes.scheme(
name = "import_indexstores",
build_action = xcode_schemes.build_action(
targets = [
xcode_schemes.build_target(
_TOOLS["import_indexstores"],
),
],
),
launch_action = xcode_schemes.launch_action(
_TOOLS["import_indexstores"],
diagnostics = _SCHEME_DIAGNOSTICS,
),
profile_action = xcode_schemes.profile_action(
_TOOLS["import_indexstores"],
build_configuration = "Release",
),
),
xcode_schemes.scheme(
name = "pbxproj_prefix",
launch_action = xcode_schemes.launch_action(
Expand Down Expand Up @@ -223,6 +242,7 @@ filegroup(
srcs = [
"//" + package_name() + "/extension_point_identifiers_parser:release_files",
"//" + package_name() + "/generators:release_files",
"//" + package_name() + "/import_indexstores:release_files",
"//" + package_name() + "/params_processors:release_files",
"//" + package_name() + "/swiftc_stub:release_files",
"//" + package_name() + "/xccurrentversions_parser:release_files",
Expand Down
59 changes: 59 additions & 0 deletions tools/import_indexstores/BUILD
Original file line number Diff line number Diff line change
@@ -0,0 +1,59 @@
load("@build_bazel_rules_apple//apple:apple.bzl", "apple_universal_binary")
load(
"@build_bazel_rules_apple//apple:macos.bzl",
"macos_command_line_application",
)
load(
"@build_bazel_rules_swift//swift:swift.bzl",
"swift_binary",
"swift_library",
)

# This target exists to keep configurations the same between the generator
# and the tests, which makes the Xcode development experience better. If we used
# `swift_binary` or `apple_universal_binary` in `xcodeproj`, then the
# `macos_unit_test` transition (which is used to be able to set a minimum os
# version on the tests) will create slightly different configurations for our
# `swift_library`s. Maybe https://github.com/bazelbuild/bazel/issues/6526 will
# fix that for us.
macos_command_line_application(
name = "import_indexstores",
minimum_os_version = "12.0",
visibility = ["//visibility:public"],
deps = [":import_indexstores.library"],
)

swift_library(
name = "import_indexstores.library",
srcs = glob(["*.swift"]),
module_name = "import_indexstores",
)

swift_binary(
name = "import_indexstores_binary",
deps = [":import_indexstores.library"],
)

apple_universal_binary(
name = "universal_import_indexstores",
binary = ":import_indexstores_binary",
forced_cpus = [
"x86_64",
"arm64",
],
minimum_os_version = "12.0",
platform_type = "macos",
visibility = ["//visibility:public"],
)

# Release

filegroup(
name = "release_files",
srcs = [
"BUILD.release.bazel",
":universal_import_indexstores",
],
tags = ["manual"],
visibility = ["//:__subpackages__"],
)
1 change: 1 addition & 0 deletions tools/import_indexstores/BUILD.release.bazel
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
exports_files(["universal_import_indexstores"])
29 changes: 29 additions & 0 deletions tools/import_indexstores/Errors.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
import Foundation

/// An `Error` that represents a programming error.
public struct PreconditionError: Error {
public let message: String
public let file: StaticString
public let line: UInt

public init(
message: String,
file: StaticString = #filePath,
line: UInt = #line
) {
self.message = message
self.file = file
self.line = line
}
}

extension PreconditionError: LocalizedError {
public var errorDescription: String? {
return """
Internal precondition failure:
\(file):\(line): \(message)
Please file a bug report at \
https://github.com/MobileNativeFoundation/rules_xcodeproj/issues/new?template=bug.md
"""
}
}
216 changes: 216 additions & 0 deletions tools/import_indexstores/ImportIndexstores.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,216 @@
import Foundation

@main
struct ImportIndex {
static func main() async throws {
let args = CommandLine.arguments
let pidFile =
try getEnvironmentVariable("OBJROOT") + "/import_indexstores.pid"

guard args.count > 1 else {
throw PreconditionError(
message: "Not enough arguments, expected path to execution root"
)
}
let buildExecutionRoot = args[1]

// Exit early if no indexstore filelists were provided
if args.count == 2 {
return
}

// MARK: pidFile

// Kill any previously running import
if FileManager.default.fileExists(atPath: pidFile) {
let pid = try String(contentsOfFile: pidFile)

try runSubProcess("/bin/kill", [pid])
while true {
if try runSubProcess("/bin/kill", ["-0", pid]) != 0 {
break
}
sleep(1)
}
}

// Set pid to allow cleanup later
try String(ProcessInfo.processInfo.processIdentifier)
.write(toFile: pidFile, atomically: true, encoding: .utf8)
defer {
try? FileManager.default.removeItem(atPath: pidFile)
}

// MARK: filelist

let projectDirPrefix = try getEnvironmentVariable("PROJECT_DIR") + "/"

// Merge all filelists into a single file
var indexStores: Set<String> = []
for filePath in args.dropFirst(2) {
let url = URL(fileURLWithPath: filePath)
for try await indexStore in url.lines {
indexStores.insert(indexStore)
}
}

// Exit early if no indexstores were provided
guard !indexStores.isEmpty else {
return
}

let filelistContent = indexStores
.map { projectDirPrefix + $0 + "\n" }
.joined()
let filelist = try TemporaryFile()
try filelistContent
.write(to: filelist.url, atomically: true, encoding: .utf8)

// MARK: Remaps

// We remove any `/private` prefix from the current execution_root,
// since it's removed in the Project navigator
let xcodeExecutionRoot: String
if buildExecutionRoot.hasPrefix("/private") {
xcodeExecutionRoot = String(buildExecutionRoot.dropFirst(8))
} else {
xcodeExecutionRoot = buildExecutionRoot
}

let projectTempDir = try getEnvironmentVariable("PROJECT_TEMP_DIR")

let objectFilePrefix: String
if try getEnvironmentVariable("ACTION") == "indexbuild" {
// Remove `Index.noindex/` part of path
objectFilePrefix = projectTempDir.replacingOccurrences(
of: "/Index.noindex/Build/Intermediates.noindex/",
with: "/Build/Intermediates.noindex/"
)
} else {
// Remove SwiftUI Previews part of path
objectFilePrefix = try projectTempDir.replacingRegex(
matching: #"""
Intermediates\.noindex/Previews/[^/]*/Intermediates\.noindex
"""#,
with: "Intermediates.noindex"
)
}

let xcodeOutputBase = xcodeExecutionRoot
.split(separator: "/")
.dropLast(2)
.joined(separator: "/")

let archs = try getEnvironmentVariable("ARCHS")
let arch = String(archs.split(separator: " ", maxSplits: 1).first!)

let remaps = remapArgs(
arch: arch,
developerDir: try getEnvironmentVariable("DEVELOPER_DIR"),
objectFilePrefix: objectFilePrefix,
srcRoot: try getEnvironmentVariable("SRCROOT"),
xcodeExecutionRoot: xcodeExecutionRoot,
xcodeOutputBase: xcodeOutputBase
)

// MARK: Import

let indexDataStoreDir = URL(
fileURLWithPath: try getEnvironmentVariable("INDEX_DATA_STORE_DIR")
)
let recordsDir = indexDataStoreDir.appendingPathComponent("v5/records")

try FileManager.default.createDirectory(
at: recordsDir,
withIntermediateDirectories: true
)

try runSubProcess(
try getEnvironmentVariable("INDEX_IMPORT"),
remaps + [
"-undo-rules_swift-renames",
"-incremental",
"@\(filelist.url.path)",
indexDataStoreDir.path,
]
)

// Unit files are created fresh, but record files are copied from
// `bazel-out/`, which are read-only. We need to adjust their
// permissions.
// TODO: do this in `index-import`
try setWritePermissions(in: recordsDir)
}
}

private func getEnvironmentVariable(
_ key: String,
file: StaticString = #filePath,
line: UInt = #line
) throws -> String {
guard let value = ProcessInfo.processInfo.environment[key] else {
throw PreconditionError(
message: #"Environment variable "\#(key)" not set"#,
file: file,
line: line
)
}
guard !value.isEmpty else {
throw PreconditionError(
message: #"""
Environment variable "\#(key)" is set to an empty string
"""#,
file: file,
line: line
)
}
return value
}

@discardableResult private func runSubProcess(
_ executable: String,
_ args: [String]
) throws -> Int32 {
let task = Process()
task.launchPath = executable
task.arguments = args
try task.run()
task.waitUntilExit()
return task.terminationStatus
}

private func setWritePermissions(in url: URL) throws {
let enumerator = FileManager.default.enumerator(
at: url,
includingPropertiesForKeys: [.isDirectoryKey]
)!
for case let url as URL in enumerator {
let resourceValues = try url.resourceValues(forKeys: [.isDirectoryKey])
if resourceValues.isDirectory! {
try FileManager.default.setAttributes(
[.posixPermissions: 0o755],
ofItemAtPath: url.path
)
} else {
try FileManager.default.setAttributes(
[.posixPermissions: 0o644],
ofItemAtPath: url.path
)
}
}
}

extension String {
func replacingRegex(
matching pattern: String,
with template: String
) throws -> String {
let regex = try NSRegularExpression(pattern: pattern)
let range = NSRange(startIndex..., in: self)
return regex.stringByReplacingMatches(
in: self,
range: range,
withTemplate: template
)
}
}
Loading

0 comments on commit aa7a7ee

Please sign in to comment.