Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
14 changes: 14 additions & 0 deletions Documentation/xtool.docc/Control.md
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down
5 changes: 5 additions & 0 deletions Sources/PackLib/PackSchema.swift
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down
44 changes: 40 additions & 4 deletions Sources/PackLib/Packer.swift
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand All @@ -38,7 +39,7 @@ public struct Packer: Sendable {
dependencies: [
.product(name: "\($0.product)", package: "RootPackage"),
],
linkerSettings: \($0.linkerSettings)
linkerSettings: \($0.linkerSettings(compilerRT: compilerRT))
)
"""
}
Expand Down Expand Up @@ -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"),
Expand All @@ -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"),
Expand All @@ -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)
]),
]
"""
Expand Down
16 changes: 16 additions & 0 deletions Sources/XToolSupport/DevCommand.swift
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import Foundation
import ArgumentParser
import PackLib
import Subprocess
import XKit
import Dependencies
import XUtils
Expand Down Expand Up @@ -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,
Expand Down
6 changes: 6 additions & 0 deletions Tests/XToolTests/PackerTests.swift
Original file line number Diff line number Diff line change
Expand Up @@ -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")
}