diff --git a/Documentation/xtool.docc/Control.md b/Documentation/xtool.docc/Control.md index babd0c2e..0762a726 100644 --- a/Documentation/xtool.docc/Control.md +++ b/Documentation/xtool.docc/Control.md @@ -60,6 +60,20 @@ For example, you might create an `Info.plist` file that looks something like - The default `Info.plist` has a `UISupportedInterfaceOrientations` value of portrait. This will replace it with landscape-only. - We don't include a `CFBundleDisplayName` by default, so this will add the key and specify a display name of "My App". +## Run a step before packing + +When your package depends on files another tool produces, such as a binary target built by a +different compiler, list the commands that produce them under `prepare` in `xtool.yml`. +xtool runs them in order, in the package directory, with `/bin/sh -c`, before it plans and +builds the app; a failing command stops the build. + +```yaml +version: 1 +bundleID: com.example.Hello +prepare: + - make generated-sources +``` + ## Copy resources (SwiftPM) If you need to include non-code resource files, like images, the easiest way to do so is to use [SwiftPM Resources](https://developer.apple.com/documentation/xcode/bundling-resources-with-a-swift-package). These "just work" with xtool. diff --git a/Sources/PackLib/PackSchema.swift b/Sources/PackLib/PackSchema.swift index 297c4241..23c765f5 100644 --- a/Sources/PackLib/PackSchema.swift +++ b/Sources/PackLib/PackSchema.swift @@ -20,6 +20,11 @@ public struct PackSchemaBase: Codable, Sendable { public var iconPath: String? public var resources: [String]? + /// Shell commands run in the package directory before planning, in order. A command that + /// fails stops the build. Use it to generate inputs the package depends on, such as a + /// binary target built by another tool. + public var prepare: [String]? + public var extensions: [Extension]? /// Skip auto-creating the LSP support file if it's missing. diff --git a/Sources/PackLib/Packer.swift b/Sources/PackLib/Packer.swift index bd2057b7..c550c213 100644 --- a/Sources/PackLib/Packer.swift +++ b/Sources/PackLib/Packer.swift @@ -13,6 +13,7 @@ public struct Packer: Sendable { private func build() async throws { let xtoolDir = URL(fileURLWithPath: "xtool") + let compilerRT = Self.compilerRTLibrary(for: buildSettings.triple) let packageDir = xtoolDir.appendingPathComponent(".xtool-tmp") try? FileManager.default.removeItem(at: packageDir) try FileManager.default.createDirectory(at: packageDir, withIntermediateDirectories: true) @@ -38,7 +39,7 @@ public struct Packer: Sendable { dependencies: [ .product(name: "\($0.product)", package: "RootPackage"), ], - linkerSettings: \($0.linkerSettings) + linkerSettings: \($0.linkerSettings(compilerRT: compilerRT)) ) """ } @@ -211,6 +212,38 @@ public struct Packer: Sendable { } } + /// The compiler-rt builtins archive for `triple`, named as Apple's clang resource directory + /// names it: `libclang_rt.ios.a`, `libclang_rt.iossim.a`, `libclang_rt.osx.a`. + static func compilerRTLibraryName(for triple: String) -> String? { + switch cfBundleSupportedPlatforms(for: triple).first { + case "iPhoneOS": "libclang_rt.ios.a" + case "iPhoneSimulator": "libclang_rt.iossim.a" + case "MacOSX": "libclang_rt.osx.a" + default: nil + } + } + + /// Apple's clang driver links the platform's compiler-rt builtins into every binary; the + /// Linux clang that drives the link here does not, so an app whose dependencies use + /// `__builtin_available` (for example a Kotlin/Native framework) fails with + /// `undefined symbol: ___isPlatformVersionAtLeast`. The installed Darwin SDK carries + /// Xcode's toolchain, so link its archive explicitly on non-Apple hosts. + static func compilerRTLibrary(for triple: String) -> String? { + #if os(macOS) + return nil + #else + guard let name = compilerRTLibraryName(for: triple), + let sdk = try? DarwinSDK.current() else { return nil } + let clang = sdk.bundle.appendingPathComponent( + "Developer/Toolchains/XcodeDefault.xctoolchain/usr/lib/clang", isDirectory: true + ) + let versions = (try? FileManager.default.contentsOfDirectory(atPath: clang.path)) ?? [] + return versions.sorted().lazy + .map { version in clang.appendingPathComponent("\(version)/lib/darwin/\(name)").path } + .first { FileManager.default.fileExists(atPath: $0) } + #endif + } + static func cfBundleSupportedPlatforms(for triple: String) -> [String] { let components = triple.split(separator: "-").map(String.init) guard let appleIndex = components.firstIndex(of: "apple"), @@ -233,16 +266,18 @@ public struct Packer: Sendable { } extension Plan.Product { - fileprivate var linkerSettings: String { + fileprivate func linkerSettings(compilerRT: String?) -> String { + let compilerRTFlags = compilerRT.map { "\"-Xlinker\", \"\($0)\"," } ?? "" switch self.type { - case .application: """ + case .application: return """ [ .unsafeFlags([ "-Xlinker", "-rpath", "-Xlinker", "@executable_path/Frameworks", + \(compilerRTFlags) ]), ] """ - case .appExtension: """ + case .appExtension: return """ [ // Link to Foundation framework which implements the _NSExtensionMain entrypoint .linkedFramework("Foundation"), @@ -253,6 +288,7 @@ extension Plan.Product { "-Xlinker", "-rpath", "-Xlinker", "@executable_path/../../Frameworks", // ...as well as our own "-Xlinker", "-rpath", "-Xlinker", "@executable_path/Frameworks", + \(compilerRTFlags) ]), ] """ diff --git a/Sources/XToolSupport/DevCommand.swift b/Sources/XToolSupport/DevCommand.swift index 369bfa8d..624ed8d3 100644 --- a/Sources/XToolSupport/DevCommand.swift +++ b/Sources/XToolSupport/DevCommand.swift @@ -1,6 +1,7 @@ import Foundation import ArgumentParser import PackLib +import Subprocess import XKit import Dependencies import XUtils @@ -54,6 +55,21 @@ struct PackOperation { await LSPConfig.tryEnsure(using: schema) + for command in schema.prepare ?? [] { + print("Preparing: \(command)") + try await Subprocess.run( + Configuration( + executable: .path("/bin/sh"), + arguments: ["-c", command], + environment: .inherit, + platformOptions: .withGracefulShutDown, + ), + output: .currentStandardOutput, + error: .currentStandardError, + ) + .checkSuccess() + } + let buildSettings = try await BuildSettings( configuration: buildOptions.configuration, triple: triple ?? Self.defaultTriple, diff --git a/Tests/XToolTests/PackerTests.swift b/Tests/XToolTests/PackerTests.swift index 5aabda83..09b61158 100644 --- a/Tests/XToolTests/PackerTests.swift +++ b/Tests/XToolTests/PackerTests.swift @@ -13,3 +13,9 @@ import Testing #expect(Packer.cfBundleSupportedPlatforms(for: "unknown-triple") == ["iPhoneOS"]) } + +@Test func compilerRTLibraryNameFollowsBuildTriple() { + #expect(Packer.compilerRTLibraryName(for: "arm64-apple-ios") == "libclang_rt.ios.a") + #expect(Packer.compilerRTLibraryName(for: "arm64-apple-ios17.0-simulator") == "libclang_rt.iossim.a") + #expect(Packer.compilerRTLibraryName(for: "arm64-apple-macosx") == "libclang_rt.osx.a") +}