Skip to content

Commit 4815e85

Browse files
committed
SWBBuild SDK support
1 parent c3bb2e5 commit 4815e85

10 files changed

Lines changed: 333 additions & 150 deletions

File tree

Package.swift

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -166,6 +166,7 @@ let package = Package(
166166
name: "PackLib",
167167
dependencies: [
168168
"XUtils",
169+
.product(name: "Superutils", package: "xtool-core"),
169170
.product(name: "Yams", package: "Yams"),
170171
.product(name: "XcodeGenKit", package: "XcodeGen", condition: .when(platforms: [.macOS])),
171172
]
Lines changed: 115 additions & 29 deletions
Original file line numberDiff line numberDiff line change
@@ -1,37 +1,73 @@
11
import Foundation
22
import Subprocess
33
import XUtils
4+
import Superutils
45

56
public struct BuildSettings: Sendable {
67
private static let customBinDir =
78
// this is the same option used by SwiftPM itself for dev builds
89
ProcessInfo.processInfo.environment["SWIFTPM_CUSTOM_BIN_DIR"].map { FilePath($0) }
910

10-
private static let envURL = URL(fileURLWithPath: "/usr/bin/env")
11-
12-
public let packagePath: String
11+
public var packagePath: String
1312
public let configuration: BuildConfiguration
1413
public let triple: String
15-
public let sdkOptions: [String]
16-
public let options: [String]
14+
public let buildSystem: BuildSystem
15+
public let customOptions: [String]
16+
17+
public var sdkOptions: [String]
18+
public var sdkEnvironment: [Environment.Key: String?]
19+
20+
private var configOptions: [String] {
21+
return [
22+
"--configuration", configuration.rawValue,
23+
"--build-system", buildSystem.pmName,
24+
"--package-path", packagePath,
25+
]
26+
}
27+
28+
private var resolvedBaseOptions: [String] {
29+
configOptions + sdkOptions + customOptions
30+
}
1731

1832
public init(
1933
configuration: BuildConfiguration,
2034
triple: String,
35+
buildSystem: BuildSystem = .default,
2136
packagePath: String = ".",
2237
options: [String] = []
2338
) async throws {
2439
self.packagePath = packagePath
2540
self.configuration = configuration
26-
self.options = options
41+
self.customOptions = options
2742
self.triple = triple
43+
self.buildSystem = buildSystem
44+
45+
self.sdkEnvironment = [
46+
// xcrun passes an SDKROOT that messes with our sdk configuration
47+
"SDKROOT": nil,
48+
]
2849

29-
// on macOS we don't explicitly install a Swift SDK but
30-
// SwiftPM vends "implicit" Darwin SDKs as of Swift 6.1,
31-
// i.e. we can pass `--swift-sdk arm64-apple-ios` and it
32-
// just works. See:
33-
// https://github.com/swiftlang/swift-package-manager/pull/6828
34-
self.sdkOptions = ["--swift-sdk", triple]
50+
switch buildSystem {
51+
case .swiftPM:
52+
// on macOS we don't explicitly install a Swift SDK but
53+
// SwiftPM vends "implicit" Darwin SDKs as of Swift 6.1,
54+
// i.e. we can pass `--swift-sdk arm64-apple-ios` and it
55+
// just works. See:
56+
// https://github.com/swiftlang/swift-package-manager/pull/6828
57+
self.sdkOptions = ["--swift-sdk", triple]
58+
case .swiftBuild:
59+
self.sdkOptions = ["--triple", triple]
60+
#if !os(macOS)
61+
let darwinSDK = try await DarwinSDK.current()
62+
.orThrow(StringError("No Darwin SDK configured. Please run `xtool setup`."))
63+
self.sdkOptions += [
64+
"--toolset", "\(darwinSDK.bundle.path)/toolset-swb.json",
65+
]
66+
self.sdkEnvironment.merge([
67+
"XCODE_EXTRA_PLATFORM_FOLDERS": "\(darwinSDK.bundle.path)/Developer/Platforms",
68+
]) { $1 }
69+
#endif
70+
}
3571
}
3672

3773
#if os(macOS)
@@ -44,15 +80,40 @@ public struct BuildSettings: Sendable {
4480
return result.standardOutput?.trimmingCharacters(in: .whitespacesAndNewlines) ?? ""
4581
}
4682

47-
private static let swiftURL = Task {
83+
private static let _swiftURL = Task {
4884
try await FilePath(xcrun(["-f", "swift"]))
4985
}
86+
87+
public static func swiftURL() async throws -> FilePath {
88+
try await _swiftURL.value
89+
}
90+
91+
private static let _swiftcURL = Task {
92+
try await FilePath(xcrun(["-f", "swiftc"]))
93+
}
94+
95+
public static func swiftcURL() async throws -> FilePath {
96+
try await _swiftcURL.value
97+
}
98+
#else
99+
public static func swiftURL() async throws -> FilePath {
100+
try await FilePath(ToolRegistry.locate("swift")).orThrow(StringError("Got bad path for swift executable"))
101+
}
102+
103+
public static func swiftcURL() async throws -> FilePath {
104+
try await FilePath(ToolRegistry.locate("swiftc")).orThrow(StringError("Got bad path for swiftc executable"))
105+
}
50106
#endif
51107

108+
public func withPackagePath(_ path: String) -> Self {
109+
var copy = self
110+
copy.packagePath = path
111+
return copy
112+
}
113+
52114
public func swiftPMInvocation(
53115
forTool tool: String,
54116
arguments: [String],
55-
packagePathOverride: String? = nil
56117
) async throws -> Subprocess.Configuration {
57118
let executable: Executable
58119
let baseArguments: [String]
@@ -65,35 +126,60 @@ public struct BuildSettings: Sendable {
65126
// to add SDKROOT=.../MacOSX.sdk to our invocations. We avoid this by
66127
// 1) invoking the real swift executable (located with `xcrun -f`) and
67128
// 2) explicitly removing SDKROOT from the env, as it may be inherited
68-
// through the `swift run pack` invocation.
69-
executable = .path(try await Self.swiftURL.value)
129+
// through a parent process (e.g. `swift run xtool`).
130+
executable = .path(try await Self.swiftURL())
70131
#else
71132
executable = .name("swift")
72133
#endif
73134
baseArguments = [tool]
74135
}
75136

76-
let extraArguments: [String] = [
77-
"--package-path", packagePathOverride ?? packagePath,
78-
"--configuration", configuration.rawValue,
79-
]
80-
81-
var rawEnv = ProcessInfo.processInfo.environment
82-
rawEnv.removeValue(forKey: "SDKROOT")
83-
let env = Dictionary(uniqueKeysWithValues: rawEnv.map {
84-
(Environment.Key(rawValue: $0)!, $1)
85-
})
86-
87137
return Configuration(
88138
executable,
89-
arguments: .init(baseArguments + extraArguments + sdkOptions + options + arguments),
90-
environment: .custom(env),
139+
arguments: .init(baseArguments + resolvedBaseOptions + arguments),
140+
environment: .inherit.updating(sdkEnvironment),
91141
platformOptions: .withGracefulShutDown,
92142
)
93143
}
144+
145+
public var buildServerArguments: [String] {
146+
return [
147+
"package", "experimental-build-server",
148+
"--disable-automatic-resolution",
149+
// TODO: once https://github.com/swiftlang/swift-package-manager/pull/9819 makes it into a release
150+
// (Swift 6.4), pass --experimental-skip-acquiring-lock
151+
] + resolvedBaseOptions
152+
}
94153
}
95154

96155
public enum BuildConfiguration: String, CaseIterable, Sendable {
97156
case debug
98157
case release
158+
159+
var swiftBuildValue: String {
160+
switch self {
161+
case .debug: "Debug"
162+
case .release: "Release"
163+
}
164+
}
165+
}
166+
167+
public enum BuildSystem: Sendable {
168+
case swiftPM
169+
case swiftBuild
170+
171+
public static var `default`: Self {
172+
#if os(macOS)
173+
return .swiftBuild
174+
#else
175+
return .swiftBuild
176+
#endif
177+
}
178+
179+
var pmName: String {
180+
switch self {
181+
case .swiftPM: "native"
182+
case .swiftBuild: "swiftbuild"
183+
}
184+
}
99185
}

Sources/PackLib/DarwinSDK.swift

Lines changed: 95 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,95 @@
1+
import Foundation
2+
import Subprocess
3+
import XUtils
4+
5+
public struct DarwinSDK {
6+
public let bundle: URL
7+
public let version: String
8+
9+
public init?(bundle: URL) {
10+
self.bundle = bundle
11+
if let version = try? Data(contentsOf: bundle.appendingPathComponent("darwin-sdk-version.txt")) {
12+
self.version = String(decoding: version, as: UTF8.self)
13+
.trimmingCharacters(in: .whitespacesAndNewlines)
14+
} else if ["darwin.xtoolsdk", "darwin.artifactbundle"].contains(bundle.lastPathComponent) {
15+
self.version = "unknown"
16+
} else {
17+
return nil
18+
}
19+
}
20+
21+
public static func install(from path: String) async throws {
22+
// we can't just move into ~/.swiftpm/swift-sdks because the swiftpm directory
23+
// location depends on factors like $XDG_CONFIG_HOME. Rather than replicating
24+
// SwiftPM's logic, which may change, it's more reliable to directly invoke
25+
// `swift sdk install`. See: https://github.com/xtool-org/xtool/pull/40
26+
27+
let url = URL(fileURLWithPath: path)
28+
guard DarwinSDK(bundle: url) != nil else { throw StringError("Invalid Darwin SDK at '\(path)'")}
29+
30+
try await addHostClangResourceDir(to: url)
31+
32+
try await Subprocess.run(
33+
.name("swift"),
34+
arguments: ["sdk", "install", url.path],
35+
output: .discarded
36+
)
37+
.checkSuccess()
38+
}
39+
40+
private static func addHostClangResourceDir(to sdk: URL) async throws {
41+
let clangURL = try await ToolRegistry.locate("clang")
42+
let process = try await Subprocess.run(
43+
.path(FilePath(clangURL.path)),
44+
arguments: ["-print-resource-dir"],
45+
output: .string(limit: .max)
46+
).checkSuccess()
47+
let output = process.standardOutput ?? ""
48+
let hostClangResources = URL(filePath: output.trimmingCharacters(in: .whitespacesAndNewlines))
49+
let hostInclude = hostClangResources.appending(path: "include")
50+
let sdkInclude = sdk.appending(path: "Developer/Toolchains/XcodeDefault.xctoolchain/usr/lib/swift/clang/include")
51+
try FileManager.default.copyItem(at: hostInclude, to: sdkInclude)
52+
}
53+
54+
public static func current() async throws -> DarwinSDK? {
55+
let outputString: String
56+
do {
57+
outputString = try await Subprocess.run(
58+
.name("swift"),
59+
arguments: ["sdk", "configure", "darwin", "arm64-apple-ios", "--show-configuration"],
60+
output: .string(limit: .max)
61+
)
62+
.checkSuccess()
63+
.standardOutput
64+
?? ""
65+
} catch SubprocessFailure.exited {
66+
return nil
67+
}
68+
69+
// should be something like
70+
// swiftResourcesPath: /home/user/.swiftpm/swift-sdks/darwin.artifactbundle/Developer/Toolchains/XcodeDefault.xctoolchain/usr/lib/swift
71+
// swiftlint:disable:previous line_length
72+
let resourcesPathPrefix = "swiftResourcesPath: "
73+
74+
guard let resourcesPath = outputString
75+
.split(separator: "\n")
76+
.first(where: { $0.hasPrefix(resourcesPathPrefix) })?
77+
.dropFirst(resourcesPathPrefix.count)
78+
else { return nil }
79+
80+
var resourcesURL = URL(fileURLWithPath: String(resourcesPath))
81+
for _ in 0..<6 {
82+
resourcesURL = resourcesURL.deletingLastPathComponent()
83+
}
84+
85+
return DarwinSDK(bundle: resourcesURL)
86+
}
87+
88+
public func isUpToDate() -> Bool {
89+
true
90+
}
91+
92+
public func remove() throws {
93+
try FileManager.default.removeItem(at: bundle)
94+
}
95+
}

Sources/PackLib/Packer.swift

Lines changed: 24 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -55,20 +55,21 @@ public struct Packer: Sendable {
5555
try Data().write(to: sources.appendingPathComponent("stub.c", isDirectory: false))
5656
}
5757

58-
let buildConfig = try await buildSettings.swiftPMInvocation(
59-
forTool: "build",
60-
arguments: [
61-
"--package-path", packageDir.path,
62-
"--scratch-path", ".build",
63-
// resolving can cause SwiftPM to overwrite the root package deps
64-
// with just the deps needed for the builder package (which is to
65-
// say, any "dev dependencies" of the root package may be removed.)
66-
// fortunately we've already resolved the root package by this point
67-
// in order to dump the plan, so we can skip resolution here to skirt
68-
// the issue.
69-
"--disable-automatic-resolution",
70-
]
71-
)
58+
let buildConfig = try await buildSettings
59+
.withPackagePath(packageDir.path)
60+
.swiftPMInvocation(
61+
forTool: "build",
62+
arguments: [
63+
"--scratch-path", ".build",
64+
// resolving can cause SwiftPM to overwrite the root package deps
65+
// with just the deps needed for the builder package (which is to
66+
// say, any "dev dependencies" of the root package may be removed.)
67+
// fortunately we've already resolved the root package by this point
68+
// in order to dump the plan, so we can skip resolution here to skirt
69+
// the issue.
70+
"--disable-automatic-resolution",
71+
],
72+
)
7273
try await Subprocess.run(
7374
buildConfig,
7475
output: .standardError,
@@ -84,8 +85,16 @@ public struct Packer: Sendable {
8485

8586
let outputURL = output.url
8687

88+
let binPath: String
89+
switch buildSettings.buildSystem {
90+
case .swiftPM:
91+
binPath = "\(buildSettings.triple)/\(buildSettings.configuration.rawValue)"
92+
case .swiftBuild:
93+
let platformName = buildSettings.triple.contains("simulator") ? "iphonesimulator" : "iphoneos"
94+
binPath = "out/Products/\(buildSettings.configuration.swiftBuildValue)-\(platformName)"
95+
}
8796
let binDir = URL(
88-
fileURLWithPath: ".build/\(buildSettings.triple)/\(buildSettings.configuration.rawValue)",
97+
fileURLWithPath: ".build/\(binPath)",
8998
isDirectory: true
9099
)
91100

Sources/PackLib/Planner.swift

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -244,10 +244,9 @@ public struct Planner: Sendable {
244244
}
245245

246246
private func _dumpAction(arguments: [String], path: String) async throws -> Data {
247-
let dumpConfig = try await buildSettings.swiftPMInvocation(
247+
let dumpConfig = try await buildSettings.withPackagePath(path).swiftPMInvocation(
248248
forTool: "package",
249249
arguments: arguments,
250-
packagePathOverride: path
251250
)
252251
return try await Subprocess.run(
253252
dumpConfig,

0 commit comments

Comments
 (0)