Skip to content

Commit 4095b90

Browse files
authored
SE-0387: add --toolset option to build-related subcommands (#8051)
### Motivation: The only feature proposed in [SE-0387](https://github.com/swiftlang/swift-evolution/blob/main/proposals/0387-cross-compilation-destinations.md) that remains unimplemented is the `--toolset` CLI option: > We propose that users also should be able to pass `--toolset <path_to_toolset.json>` option to `swift build`, `swift test`, and `swift run`. > > We'd like to allow using multiple toolset files at once. This way users can "assemble" toolchains on the fly out of tools that in certain scenarios may even come from different vendors. A toolset file can have an arbitrary name, and each file should be passed with a separate `--toolset` option, i.e. `swift build --toolset t1.json --toolset t2.json`. > > All of the properties related to names of the tools are optional, which allows merging configuration from multiple toolset files. For example, consider `toolset1.json`: >```json5 >{ > "schemaVersion": "1.0", > "swiftCompiler": { > "path": "/usr/bin/swiftc", > "extraCLIOptions": ["-Xfrontend", "-enable-cxx-interop"] > }, > "cCompiler": { > "path": "/usr/bin/clang", > "extraCLIOptions": ["-pedantic"] > } >} >``` > > and `toolset2.json`: > > ```json5 >{ > "schemaVersion": "1.0", > "swiftCompiler": { > "path": "/custom/swiftc" > } >} >``` > > With multiple `--toolset` options, passing both of those files will merge them into a single configuration. Tools passed in subsequent `--toolset` options will shadow tools from previous options with the same names. That is, >`swift build --toolset toolset1.json --toolset toolset2.json` will build with `/custom/swiftc` and no extra flags, as specified in `toolset2.json`, but `/usr/bin/clang -pedantic` from `toolset1.json` will still be used. > > Tools not specified in any of the supplied toolset files will be looked up in existing implied search paths that are used without toolsets, even when `rootPath` is present. We'd like toolsets to be explicit in this regard: if a tool would like to participate in toolset path lookups, it must provide either a relative or an absolute path in a toolset. > > Tools that don't have `path` property but have `extraCLIOptions` present will append options from that property to a tool with the same name specified in a preceding toolset file. If no other toolset files were provided, these options will be appended to the default tool invocation. ### Modifications: Added `toolsetPaths` on `LocationOptions`, which passes new `customToolsets` argument on `SwiftSDK.deriveTargetSwiftSDK`. Added corresponding tests. ### Result: New `--toolset` option is able to accept toolset JSON files.
1 parent 4c6fd94 commit 4095b90

File tree

10 files changed

+278
-23
lines changed

10 files changed

+278
-23
lines changed

Sources/CoreCommands/Options.swift

+12-1
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@
22
//
33
// This source file is part of the Swift open source project
44
//
5-
// Copyright (c) 2014-2023 Apple Inc. and the Swift project authors
5+
// Copyright (c) 2014-2024 Apple Inc. and the Swift project authors
66
// Licensed under Apache License v2.0 with Runtime Library Exception
77
//
88
// See http://swift.org/LICENSE.txt for license information
@@ -124,6 +124,17 @@ public struct LocationOptions: ParsableArguments {
124124
completion: .directory
125125
)
126126
public var swiftSDKsDirectory: AbsolutePath?
127+
128+
@Option(
129+
name: .customLong("toolset"),
130+
help: """
131+
Specify a toolset JSON file to use when building for the target platform. \
132+
Use the option multiple times to specify more than one toolset. Toolsets will be merged in the order \
133+
they're specified into a single final toolset for the current build.
134+
""",
135+
completion: .file(extensions: [".json"])
136+
)
137+
public var toolsetPaths: [AbsolutePath] = []
127138

128139
@Option(
129140
name: .customLong("pkg-config-path"),

Sources/CoreCommands/SwiftCommandState.swift

+1
Original file line numberDiff line numberDiff line change
@@ -880,6 +880,7 @@ public final class SwiftCommandState {
880880
swiftSDK = try SwiftSDK.deriveTargetSwiftSDK(
881881
hostSwiftSDK: hostSwiftSDK,
882882
hostTriple: hostToolchain.targetTriple,
883+
customToolsets: options.locations.toolsetPaths,
883884
customCompileDestination: options.locations.customCompileDestination,
884885
customCompileTriple: options.build.customCompileTriple,
885886
customCompileToolchain: options.build.customCompileToolchain,

Sources/PackageModel/SwiftSDKs/SwiftSDK.swift

+11-1
Original file line numberDiff line numberDiff line change
@@ -659,6 +659,7 @@ public struct SwiftSDK: Equatable {
659659
public static func deriveTargetSwiftSDK(
660660
hostSwiftSDK: SwiftSDK,
661661
hostTriple: Triple,
662+
customToolsets: [AbsolutePath] = [],
662663
customCompileDestination: AbsolutePath? = nil,
663664
customCompileTriple: Triple? = nil,
664665
customCompileToolchain: AbsolutePath? = nil,
@@ -671,6 +672,7 @@ public struct SwiftSDK: Equatable {
671672
) throws -> SwiftSDK {
672673
var swiftSDK: SwiftSDK
673674
var isBasedOnHostSDK: Bool = false
675+
674676
// Create custom toolchain if present.
675677
if let customDestination = customCompileDestination {
676678
let swiftSDKs = try SwiftSDK.decode(
@@ -699,11 +701,19 @@ public struct SwiftSDK: Equatable {
699701
swiftSDK = hostSwiftSDK
700702
isBasedOnHostSDK = true
701703
}
704+
705+
if !customToolsets.isEmpty {
706+
for toolsetPath in customToolsets {
707+
let toolset = try Toolset(from: toolsetPath, at: fileSystem, observabilityScope)
708+
swiftSDK.toolset.merge(with: toolset)
709+
}
710+
}
711+
702712
// Apply any manual overrides.
703713
if let triple = customCompileTriple {
704714
swiftSDK.targetTriple = triple
705715

706-
if isBasedOnHostSDK {
716+
if isBasedOnHostSDK && customToolsets.isEmpty {
707717
// Don't pick up extraCLIOptions for a custom triple, since those are only valid for the host triple.
708718
for tool in swiftSDK.toolset.knownTools.keys {
709719
swiftSDK.toolset.knownTools[tool]?.extraCLIOptions = []

Sources/PackageModel/Toolset.swift

+1-1
Original file line numberDiff line numberDiff line change
@@ -137,7 +137,7 @@ extension Toolset {
137137
/// of replacing them.
138138
/// - Parameter newToolset: new toolset to merge into the existing `self` toolset.
139139
public mutating func merge(with newToolset: Toolset) {
140-
self.rootPaths.append(contentsOf: newToolset.rootPaths)
140+
self.rootPaths.insert(contentsOf: newToolset.rootPaths, at: 0)
141141

142142
for (newTool, newProperties) in newToolset.knownTools {
143143
if newProperties.path != nil {

Sources/PackageModel/UserToolchain.swift

+3-2
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@
22
//
33
// This source file is part of the Swift open source project
44
//
5-
// Copyright (c) 2014-2017 Apple Inc. and the Swift project authors
5+
// Copyright (c) 2014-2024 Apple Inc. and the Swift project authors
66
// Licensed under Apache License v2.0 with Runtime Library Exception
77
//
88
// See http://swift.org/LICENSE.txt for license information
@@ -135,6 +135,7 @@ public final class UserToolchain: Toolchain {
135135
// Take the first match.
136136
break
137137
}
138+
138139
guard let toolPath else {
139140
throw InvalidToolchainDiagnostic("could not find CLI tool `\(name)` at any of these directories: \(binDirectories)")
140141
}
@@ -629,7 +630,7 @@ public final class UserToolchain: Toolchain {
629630
pathString: environment[.path],
630631
currentWorkingDirectory: fileSystem.currentWorkingDirectory
631632
)
632-
self.useXcrun = true
633+
self.useXcrun = !(fileSystem is InMemoryFileSystem)
633634
case .custom(let searchPaths, let useXcrun):
634635
self.envSearchPaths = searchPaths
635636
self.useXcrun = useXcrun

Sources/_InternalTestSupport/MockWorkspace.swift

+11-1
Original file line numberDiff line numberDiff line change
@@ -26,9 +26,19 @@ extension UserToolchain {
2626
package static func mockHostToolchain(_ fileSystem: InMemoryFileSystem) throws -> UserToolchain {
2727
var hostSwiftSDK = try SwiftSDK.hostSwiftSDK(environment: .mockEnvironment, fileSystem: fileSystem)
2828
hostSwiftSDK.targetTriple = hostTriple
29+
30+
let env = Environment.mockEnvironment
31+
2932
return try UserToolchain(
3033
swiftSDK: hostSwiftSDK,
31-
environment: .mockEnvironment,
34+
environment: env,
35+
searchStrategy: .custom(
36+
searchPaths: getEnvSearchPaths(
37+
pathString: env[.path],
38+
currentWorkingDirectory: fileSystem.currentWorkingDirectory
39+
),
40+
useXcrun: true
41+
),
3242
fileSystem: fileSystem
3343
)
3444
}

Tests/BuildTests/BuildPlanTests.swift

+38-3
Original file line numberDiff line numberDiff line change
@@ -4661,7 +4661,20 @@ final class BuildPlanTests: XCTestCase {
46614661
swiftStaticResourcesPath: "/fake/lib/swift_static"
46624662
)
46634663
)
4664-
let mockToolchain = try UserToolchain(swiftSDK: userSwiftSDK, environment: .mockEnvironment, fileSystem: fs)
4664+
4665+
let env = Environment.mockEnvironment
4666+
let mockToolchain = try UserToolchain(
4667+
swiftSDK: userSwiftSDK,
4668+
environment: env,
4669+
searchStrategy: .custom(
4670+
searchPaths: getEnvSearchPaths(
4671+
pathString: env[.path],
4672+
currentWorkingDirectory: fs.currentWorkingDirectory
4673+
),
4674+
useXcrun: true
4675+
),
4676+
fileSystem: fs
4677+
)
46654678
let commonFlags = BuildFlags(
46664679
cCompilerFlags: ["-clang-command-line-flag"],
46674680
swiftCompilerFlags: ["-swift-command-line-flag"]
@@ -4775,9 +4788,18 @@ final class BuildPlanTests: XCTestCase {
47754788
swiftStaticResourcesPath: "/fake/lib/swift_static"
47764789
)
47774790
)
4791+
4792+
let env = Environment.mockEnvironment
47784793
let mockToolchain = try UserToolchain(
47794794
swiftSDK: userSwiftSDK,
4780-
environment: .mockEnvironment,
4795+
environment: env,
4796+
searchStrategy: .custom(
4797+
searchPaths: getEnvSearchPaths(
4798+
pathString: env[.path],
4799+
currentWorkingDirectory: fs.currentWorkingDirectory
4800+
),
4801+
useXcrun: true
4802+
),
47814803
fileSystem: fs
47824804
)
47834805

@@ -5065,7 +5087,20 @@ final class BuildPlanTests: XCTestCase {
50655087
.swiftCompiler: .init(extraCLIOptions: ["-use-ld=lld"]),
50665088
])
50675089
)
5068-
let toolchain = try UserToolchain(swiftSDK: swiftSDK, environment: .mockEnvironment, fileSystem: fileSystem)
5090+
5091+
let env = Environment.mockEnvironment
5092+
let toolchain = try UserToolchain(
5093+
swiftSDK: swiftSDK,
5094+
environment: env,
5095+
searchStrategy: .custom(
5096+
searchPaths: getEnvSearchPaths(
5097+
pathString: env[.path],
5098+
currentWorkingDirectory: fileSystem.currentWorkingDirectory
5099+
),
5100+
useXcrun: true
5101+
),
5102+
fileSystem: fileSystem
5103+
)
50695104
let result = try await BuildPlanResult(plan: mockBuildPlan(
50705105
toolchain: toolchain,
50715106
graph: graph,

Tests/CommandsTests/SwiftCommandStateTests.swift

+116-14
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@
22
//
33
// This source file is part of the Swift open source project
44
//
5-
// Copyright (c) 2021-2022 Apple Inc. and the Swift project authors
5+
// Copyright (c) 2021-2024 Apple Inc. and the Swift project authors
66
// Licensed under Apache License v2.0 with Runtime Library Exception
77
//
88
// See http://swift.org/LICENSE.txt for license information
@@ -215,10 +215,10 @@ final class SwiftCommandStateTests: CommandsTestCase {
215215
let tool = try SwiftCommandState.makeMockState(options: options)
216216

217217
// There is only one AuthorizationProvider depending on platform
218-
#if canImport(Security)
218+
#if canImport(Security)
219219
let keychainProvider = try tool.getRegistryAuthorizationProvider() as? KeychainAuthorizationProvider
220220
XCTAssertNotNil(keychainProvider)
221-
#else
221+
#else
222222
let netrcProvider = try tool.getRegistryAuthorizationProvider() as? NetrcAuthorizationProvider
223223
XCTAssertNotNil(netrcProvider)
224224
XCTAssertEqual(try netrcProvider.map { try resolveSymlinks($0.path) }, try resolveSymlinks(customPath))
@@ -232,7 +232,7 @@ final class SwiftCommandStateTests: CommandsTestCase {
232232
XCTAssertThrowsError(try tool.getRegistryAuthorizationProvider(), "error expected") { error in
233233
XCTAssertEqual(error as? StringError, StringError("did not find netrc file at \(customPath)"))
234234
}
235-
#endif
235+
#endif
236236
}
237237

238238
// Tests should not modify user's home dir .netrc so leaving that out intentionally
@@ -246,9 +246,9 @@ final class SwiftCommandStateTests: CommandsTestCase {
246246

247247
let observer = ObservabilitySystem.makeForTesting()
248248
let graph = try loadModulesGraph(fileSystem: fs, manifests: [
249-
Manifest.createRootManifest(displayName: "Pkg",
250-
path: "/Pkg",
251-
targets: [TargetDescription(name: "exe")])
249+
Manifest.createRootManifest(displayName: "Pkg",
250+
path: "/Pkg",
251+
targets: [TargetDescription(name: "exe")])
252252
], observabilityScope: observer.topScope)
253253

254254
var plan: BuildPlan
@@ -319,7 +319,7 @@ final class SwiftCommandStateTests: CommandsTestCase {
319319
[.anySequence, "-gnone", .anySequence])
320320
}
321321

322-
func testToolchainArgument() async throws {
322+
func testToolchainOption() async throws {
323323
let customTargetToolchain = AbsolutePath("/path/to/toolchain")
324324
let hostSwiftcPath = AbsolutePath("/usr/bin/swiftc")
325325
let hostArPath = AbsolutePath("/usr/bin/ar")
@@ -351,17 +351,16 @@ final class SwiftCommandStateTests: CommandsTestCase {
351351
observabilityScope: observer.topScope
352352
)
353353

354-
let options = try GlobalOptions.parse(
355-
[
356-
"--toolchain", customTargetToolchain.pathString,
357-
"--triple", "x86_64-unknown-linux-gnu",
358-
]
359-
)
354+
let options = try GlobalOptions.parse([
355+
"--toolchain", customTargetToolchain.pathString,
356+
"--triple", "x86_64-unknown-linux-gnu",
357+
])
360358
let swiftCommandState = try SwiftCommandState.makeMockState(
361359
options: options,
362360
fileSystem: fs,
363361
environment: ["PATH": "/usr/bin"]
364362
)
363+
365364
XCTAssertEqual(swiftCommandState.originalWorkingDirectory, fs.currentWorkingDirectory)
366365
XCTAssertEqual(
367366
try swiftCommandState.getTargetToolchain().swiftCompilerPath,
@@ -371,6 +370,7 @@ final class SwiftCommandStateTests: CommandsTestCase {
371370
try swiftCommandState.getTargetToolchain().swiftSDK.toolset.knownTools[.swiftCompiler]?.path,
372371
nil
373372
)
373+
374374
let plan = try await BuildPlan(
375375
destinationBuildParameters: swiftCommandState.productsBuildParameters,
376376
toolsBuildParameters: swiftCommandState.toolsBuildParameters,
@@ -383,6 +383,108 @@ final class SwiftCommandStateTests: CommandsTestCase {
383383

384384
XCTAssertMatch(arguments, [.contains("/path/to/toolchain")])
385385
}
386+
387+
func testToolsetOption() throws {
388+
let targetToolchainPath = "/path/to/toolchain"
389+
let customTargetToolchain = AbsolutePath(targetToolchainPath)
390+
let hostSwiftcPath = AbsolutePath("/usr/bin/swiftc")
391+
let hostArPath = AbsolutePath("/usr/bin/ar")
392+
let targetSwiftcPath = customTargetToolchain.appending(components: ["swiftc"])
393+
let targetArPath = customTargetToolchain.appending(components: ["llvm-ar"])
394+
395+
let fs = InMemoryFileSystem(emptyFiles: [
396+
hostSwiftcPath.pathString,
397+
hostArPath.pathString,
398+
targetSwiftcPath.pathString,
399+
targetArPath.pathString
400+
])
401+
402+
for path in [hostSwiftcPath, hostArPath, targetSwiftcPath, targetArPath,] {
403+
try fs.updatePermissions(path, isExecutable: true)
404+
}
405+
406+
try fs.writeFileContents("/toolset.json", string: """
407+
{
408+
"schemaVersion": "1.0",
409+
"rootPath": "\(targetToolchainPath)"
410+
}
411+
""")
412+
413+
let options = try GlobalOptions.parse(["--toolset", "/toolset.json"])
414+
let swiftCommandState = try SwiftCommandState.makeMockState(
415+
options: options,
416+
fileSystem: fs,
417+
environment: ["PATH": "/usr/bin"]
418+
)
419+
420+
let hostToolchain = try swiftCommandState.getHostToolchain()
421+
let targetToolchain = try swiftCommandState.getTargetToolchain()
422+
423+
XCTAssertEqual(
424+
targetToolchain.swiftSDK.toolset.rootPaths,
425+
[customTargetToolchain] + hostToolchain.swiftSDK.toolset.rootPaths
426+
)
427+
XCTAssertEqual(targetToolchain.swiftCompilerPath, targetSwiftcPath)
428+
XCTAssertEqual(targetToolchain.librarianPath, targetArPath)
429+
}
430+
431+
func testMultipleToolsets() throws {
432+
let targetToolchainPath1 = "/path/to/toolchain1"
433+
let customTargetToolchain1 = AbsolutePath(targetToolchainPath1)
434+
let targetToolchainPath2 = "/path/to/toolchain2"
435+
let customTargetToolchain2 = AbsolutePath(targetToolchainPath2)
436+
let hostSwiftcPath = AbsolutePath("/usr/bin/swiftc")
437+
let hostArPath = AbsolutePath("/usr/bin/ar")
438+
let targetSwiftcPath = customTargetToolchain1.appending(components: ["swiftc"])
439+
let targetArPath = customTargetToolchain1.appending(components: ["llvm-ar"])
440+
let targetClangPath = customTargetToolchain2.appending(components: ["clang"])
441+
442+
let fs = InMemoryFileSystem(emptyFiles: [
443+
hostSwiftcPath.pathString,
444+
hostArPath.pathString,
445+
targetSwiftcPath.pathString,
446+
targetArPath.pathString,
447+
targetClangPath.pathString
448+
])
449+
450+
for path in [hostSwiftcPath, hostArPath, targetSwiftcPath, targetArPath, targetClangPath,] {
451+
try fs.updatePermissions(path, isExecutable: true)
452+
}
453+
454+
try fs.writeFileContents("/toolset1.json", string: """
455+
{
456+
"schemaVersion": "1.0",
457+
"rootPath": "\(targetToolchainPath1)"
458+
}
459+
""")
460+
461+
try fs.writeFileContents("/toolset2.json", string: """
462+
{
463+
"schemaVersion": "1.0",
464+
"rootPath": "\(targetToolchainPath2)"
465+
}
466+
""")
467+
468+
let options = try GlobalOptions.parse([
469+
"--toolset", "/toolset1.json", "--toolset", "/toolset2.json"
470+
])
471+
let swiftCommandState = try SwiftCommandState.makeMockState(
472+
options: options,
473+
fileSystem: fs,
474+
environment: ["PATH": "/usr/bin"]
475+
)
476+
477+
let hostToolchain = try swiftCommandState.getHostToolchain()
478+
let targetToolchain = try swiftCommandState.getTargetToolchain()
479+
480+
XCTAssertEqual(
481+
targetToolchain.swiftSDK.toolset.rootPaths,
482+
[customTargetToolchain2, customTargetToolchain1] + hostToolchain.swiftSDK.toolset.rootPaths
483+
)
484+
XCTAssertEqual(targetToolchain.swiftCompilerPath, targetSwiftcPath)
485+
XCTAssertEqual(try targetToolchain.getClangCompiler(), targetClangPath)
486+
XCTAssertEqual(targetToolchain.librarianPath, targetArPath)
487+
}
386488
}
387489

388490
extension SwiftCommandState {

Tests/PackageModelTests/SwiftSDKBundleTests.swift

+1
Original file line numberDiff line numberDiff line change
@@ -388,6 +388,7 @@ final class SwiftSDKBundleTests: XCTestCase {
388388
observabilityScope: system.topScope,
389389
outputHandler: { _ in }
390390
)
391+
391392
for bundle in bundles {
392393
try await store.install(bundlePathOrURL: bundle.path, archiver)
393394
}

0 commit comments

Comments
 (0)