Skip to content

Commit bd821ea

Browse files
Inherit host Swift SDK's toolset paths to fallback to host tools (#7297)
Fallback to host SDK tools if target SDK doesn't have tools. ### Motivation: Currently, Swift SDK fallbacks to system `PATH` search path for tools that are not found in the SDK. Usually, host compiler tool is capable to cross compile but just lacks some of artifacts like platform libraries. In that case, we don't need to have a separate compiler tools in target-specific Swift SDK, so we can share the host compiler tools as long as the host Swift and target Swift are the same version. ### Modifications: This patch makes Swift SDK fallback to host Swift SDK tools if the target Swift SDK doesn't have such tools. ### Result: This unlocks eliminating compiler tools from target-specific Swift SDKs, and will result in a smaller SDK size. Also makes it easier to build host platform independent Swift SDK.
1 parent d0c576a commit bd821ea

File tree

3 files changed

+201
-61
lines changed

3 files changed

+201
-61
lines changed

Sources/CoreCommands/SwiftTool.swift

Lines changed: 21 additions & 58 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@ import class Foundation.NSLock
1717
import class Foundation.ProcessInfo
1818
import PackageGraph
1919
import PackageLoading
20+
@_spi(SwiftPMInternal)
2021
import PackageModel
2122
import SPMBuildCore
2223
import Workspace
@@ -767,71 +768,33 @@ public final class SwiftTool {
767768

768769
/// Lazily compute the target toolchain.z
769770
private lazy var _targetToolchain: Result<UserToolchain, Swift.Error> = {
770-
var swiftSDK: SwiftSDK
771+
let swiftSDK: SwiftSDK
771772
let hostSwiftSDK: SwiftSDK
773+
let store = SwiftSDKBundleStore(
774+
swiftSDKsDirectory: self.sharedSwiftSDKsDirectory,
775+
fileSystem: fileSystem,
776+
observabilityScope: observabilityScope,
777+
outputHandler: { print($0.description) }
778+
)
772779
do {
773780
let hostToolchain = try _hostToolchain.get()
774781
hostSwiftSDK = hostToolchain.swiftSDK
775-
let hostTriple = hostToolchain.targetTriple
776-
777-
// Create custom toolchain if present.
778-
if let customDestination = self.options.locations.customCompileDestination {
779-
let swiftSDKs = try SwiftSDK.decode(
780-
fromFile: customDestination,
781-
fileSystem: self.fileSystem,
782-
observabilityScope: self.observabilityScope
783-
)
784-
if swiftSDKs.count == 1 {
785-
swiftSDK = swiftSDKs[0]
786-
} else if swiftSDKs.count > 1,
787-
let triple = options.build.customCompileTriple,
788-
let matchingSDK = swiftSDKs.first(where: { $0.targetTriple == triple })
789-
{
790-
swiftSDK = matchingSDK
791-
} else {
792-
return .failure(SwiftSDKError.noSwiftSDKDecoded(customDestination))
793-
}
794-
} else if let triple = options.build.customCompileTriple,
795-
let targetSwiftSDK = SwiftSDK.defaultSwiftSDK(for: triple, hostSDK: hostSwiftSDK)
796-
{
797-
swiftSDK = targetSwiftSDK
798-
} else if let swiftSDKSelector = options.build.swiftSDKSelector {
799-
let store = SwiftSDKBundleStore(
800-
swiftSDKsDirectory: self.sharedSwiftSDKsDirectory,
801-
fileSystem: self.fileSystem,
802-
observabilityScope: self.observabilityScope,
803-
outputHandler: { print($0.description) }
804-
)
805-
swiftSDK = try store.selectBundle(matching: swiftSDKSelector, hostTriple: hostTriple)
806-
} else {
807-
// Otherwise use the host toolchain.
808-
swiftSDK = hostSwiftSDK
809-
}
782+
swiftSDK = try SwiftSDK.deriveTargetSwiftSDK(
783+
hostSwiftSDK: hostSwiftSDK,
784+
hostTriple: hostToolchain.targetTriple,
785+
customCompileDestination: options.locations.customCompileDestination,
786+
customCompileTriple: options.build.customCompileTriple,
787+
customCompileToolchain: options.build.customCompileToolchain,
788+
customCompileSDK: options.build.customCompileSDK,
789+
swiftSDKSelector: options.build.swiftSDKSelector,
790+
architectures: options.build.architectures,
791+
store: store,
792+
observabilityScope: self.observabilityScope,
793+
fileSystem: self.fileSystem
794+
)
810795
} catch {
811796
return .failure(error)
812797
}
813-
// Apply any manual overrides.
814-
if let triple = options.build.customCompileTriple {
815-
swiftSDK.targetTriple = triple
816-
}
817-
if let binDir = options.build.customCompileToolchain {
818-
if !self.fileSystem.exists(binDir) {
819-
self.observabilityScope.emit(
820-
warning: """
821-
Toolchain directory specified through a command-line option doesn't exist and is ignored: `\(
822-
binDir
823-
)`
824-
"""
825-
)
826-
}
827-
828-
swiftSDK.add(toolsetRootPath: binDir.appending(components: "usr", "bin"))
829-
}
830-
if let sdk = options.build.customCompileSDK {
831-
swiftSDK.pathsConfiguration.sdkRootPath = sdk
832-
}
833-
swiftSDK.architectures = options.build.architectures.isEmpty ? nil : options.build.architectures
834-
835798
// Check if we ended up with the host toolchain.
836799
if hostSwiftSDK == swiftSDK {
837800
return self._hostToolchain

Sources/PackageModel/SwiftSDKs/SwiftSDK.swift

Lines changed: 77 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -598,6 +598,83 @@ public struct SwiftSDK: Equatable {
598598
return nil
599599
}
600600

601+
/// Computes the target Swift SDK for the given options.
602+
@_spi(SwiftPMInternal)
603+
public static func deriveTargetSwiftSDK(
604+
hostSwiftSDK: SwiftSDK,
605+
hostTriple: Triple,
606+
customCompileDestination: AbsolutePath? = nil,
607+
customCompileTriple: Triple? = nil,
608+
customCompileToolchain: AbsolutePath? = nil,
609+
customCompileSDK: AbsolutePath? = nil,
610+
swiftSDKSelector: String? = nil,
611+
architectures: [String] = [],
612+
store: SwiftSDKBundleStore,
613+
observabilityScope: ObservabilityScope,
614+
fileSystem: FileSystem
615+
) throws -> SwiftSDK {
616+
var swiftSDK: SwiftSDK
617+
var isBasedOnHostSDK: Bool = false
618+
// Create custom toolchain if present.
619+
if let customDestination = customCompileDestination {
620+
let swiftSDKs = try SwiftSDK.decode(
621+
fromFile: customDestination,
622+
fileSystem: fileSystem,
623+
observabilityScope: observabilityScope
624+
)
625+
if swiftSDKs.count == 1 {
626+
swiftSDK = swiftSDKs[0]
627+
} else if swiftSDKs.count > 1,
628+
let triple = customCompileTriple,
629+
let matchingSDK = swiftSDKs.first(where: { $0.targetTriple == triple })
630+
{
631+
swiftSDK = matchingSDK
632+
} else {
633+
throw SwiftSDKError.noSwiftSDKDecoded(customDestination)
634+
}
635+
} else if let triple = customCompileTriple,
636+
let targetSwiftSDK = SwiftSDK.defaultSwiftSDK(for: triple, hostSDK: hostSwiftSDK)
637+
{
638+
swiftSDK = targetSwiftSDK
639+
} else if let swiftSDKSelector {
640+
swiftSDK = try store.selectBundle(matching: swiftSDKSelector, hostTriple: hostTriple)
641+
} else {
642+
// Otherwise use the host toolchain.
643+
swiftSDK = hostSwiftSDK
644+
isBasedOnHostSDK = true
645+
}
646+
// Apply any manual overrides.
647+
if let triple = customCompileTriple {
648+
swiftSDK.targetTriple = triple
649+
}
650+
if let binDir = customCompileToolchain {
651+
if !fileSystem.exists(binDir) {
652+
observabilityScope.emit(
653+
warning: """
654+
Toolchain directory specified through a command-line option doesn't exist and is ignored: `\(
655+
binDir
656+
)`
657+
"""
658+
)
659+
}
660+
661+
swiftSDK.add(toolsetRootPath: binDir.appending(components: "usr", "bin"))
662+
}
663+
if let sdk = customCompileSDK {
664+
swiftSDK.pathsConfiguration.sdkRootPath = sdk
665+
}
666+
swiftSDK.architectures = architectures.isEmpty ? nil : architectures
667+
668+
if !isBasedOnHostSDK {
669+
// Append the host toolchain's toolset paths at the end for the case the target Swift SDK
670+
// doesn't have some of the tools (e.g. swift-frontend might be shared between the host and
671+
// target Swift SDKs).
672+
hostSwiftSDK.toolset.rootPaths.forEach { swiftSDK.add(toolsetRootPath: $0) }
673+
}
674+
675+
return swiftSDK
676+
}
677+
601678
/// Propagates toolchain and SDK paths known to the Swift SDK to `swiftc` CLI options.
602679
public mutating func applyPathCLIOptions() {
603680
var properties = self.toolset.knownTools[.swiftCompiler] ?? .init(extraCLIOptions: [])

Tests/PackageModelTests/SwiftSDKBundleTests.swift

Lines changed: 103 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@
1111
//===----------------------------------------------------------------------===//
1212

1313
import Basics
14+
@_spi(SwiftPMInternal)
1415
@testable import PackageModel
1516
import SPMTestSupport
1617
import XCTest
@@ -56,19 +57,33 @@ private func generateBundleFiles(bundle: MockBundle) throws -> [(String, ByteStr
5657
] + bundle.artifacts.map {
5758
(
5859
"\(bundle.path)/\($0.id)/\(targetTriple.tripleString)/swift-sdk.json",
59-
ByteString(json: try generateSwiftSDKMetadata(jsonEncoder))
60+
ByteString(json: try generateSwiftSDKMetadata(jsonEncoder, createToolset: $0.toolsetRootPath != nil))
6061
)
62+
} + bundle.artifacts.compactMap { artifact in
63+
artifact.toolsetRootPath.map { path in
64+
(
65+
"\(bundle.path)/\(artifact.id)/\(targetTriple.tripleString)/toolset.json",
66+
ByteString(json: """
67+
{
68+
"schemaVersion": "1.0",
69+
"rootPath": "\(path)"
70+
}
71+
""")
72+
)
73+
}
6174
}
6275
}
6376

64-
private func generateSwiftSDKMetadata(_ encoder: JSONEncoder) throws -> SerializedJSON {
77+
private func generateSwiftSDKMetadata(_ encoder: JSONEncoder, createToolset: Bool) throws -> SerializedJSON {
6578
try """
6679
{
6780
"schemaVersion": "4.0",
6881
"targetTriples": \(
6982
String(
7083
bytes: encoder.encode([
71-
targetTriple.tripleString: SwiftSDKMetadataV4.TripleProperties(sdkRootPath: "sdk")
84+
targetTriple.tripleString: SwiftSDKMetadataV4.TripleProperties(sdkRootPath: "sdk", toolsetPaths: createToolset ? [
85+
"toolset.json"
86+
] : nil)
7287
]),
7388
encoding: .utf8
7489
)!
@@ -86,6 +101,7 @@ private struct MockBundle {
86101
private struct MockArtifact {
87102
let id: String
88103
let supportedTriples: [Triple]
104+
var toolsetRootPath: AbsolutePath?
89105
}
90106

91107
private func generateTestFileSystem(bundleArtifacts: [MockArtifact]) throws -> (some FileSystem, [MockBundle], AbsolutePath) {
@@ -341,4 +357,88 @@ final class SwiftSDKBundleTests: XCTestCase {
341357
),
342358
])
343359
}
360+
361+
func testTargetSDKDeriviation() async throws {
362+
let toolsetRootPath = AbsolutePath("/path/to/toolpath")
363+
let (fileSystem, bundles, swiftSDKsDirectory) = try generateTestFileSystem(
364+
bundleArtifacts: [
365+
.init(id: "\(testArtifactID)1", supportedTriples: [arm64Triple]),
366+
.init(id: "\(testArtifactID)2", supportedTriples: [arm64Triple], toolsetRootPath: toolsetRootPath),
367+
]
368+
)
369+
let system = ObservabilitySystem.makeForTesting()
370+
let hostSwiftSDK = try SwiftSDK.hostSwiftSDK()
371+
let hostTriple = try! Triple("arm64-apple-macosx14.0")
372+
let archiver = MockArchiver()
373+
let store = SwiftSDKBundleStore(
374+
swiftSDKsDirectory: swiftSDKsDirectory,
375+
fileSystem: fileSystem,
376+
observabilityScope: system.topScope,
377+
outputHandler: { _ in }
378+
)
379+
for bundle in bundles {
380+
try await store.install(bundlePathOrURL: bundle.path, archiver)
381+
}
382+
383+
do {
384+
let targetSwiftSDK = try SwiftSDK.deriveTargetSwiftSDK(
385+
hostSwiftSDK: hostSwiftSDK,
386+
hostTriple: hostTriple,
387+
store: store,
388+
observabilityScope: system.topScope,
389+
fileSystem: fileSystem
390+
)
391+
// By default, the target SDK is the same as the host SDK.
392+
XCTAssertEqual(targetSwiftSDK, hostSwiftSDK)
393+
}
394+
395+
do {
396+
let targetSwiftSDK = try SwiftSDK.deriveTargetSwiftSDK(
397+
hostSwiftSDK: hostSwiftSDK,
398+
hostTriple: hostTriple,
399+
swiftSDKSelector: "\(testArtifactID)1",
400+
store: store,
401+
observabilityScope: system.topScope,
402+
fileSystem: fileSystem
403+
)
404+
// With a target SDK selector, SDK should be chosen from the store.
405+
XCTAssertEqual(targetSwiftSDK.targetTriple, targetTriple)
406+
// No toolset in the SDK, so it should be the same as the host SDK.
407+
XCTAssertEqual(targetSwiftSDK.toolset.rootPaths, hostSwiftSDK.toolset.rootPaths)
408+
}
409+
410+
do {
411+
let targetSwiftSDK = try SwiftSDK.deriveTargetSwiftSDK(
412+
hostSwiftSDK: hostSwiftSDK,
413+
hostTriple: hostTriple,
414+
swiftSDKSelector: "\(testArtifactID)2",
415+
store: store,
416+
observabilityScope: system.topScope,
417+
fileSystem: fileSystem
418+
)
419+
// With toolset in the target SDK, it should contain the host toolset roots at the end.
420+
XCTAssertEqual(targetSwiftSDK.toolset.rootPaths, [toolsetRootPath] + hostSwiftSDK.toolset.rootPaths)
421+
}
422+
423+
do {
424+
// Check explicit overriding options.
425+
let customCompileSDK = AbsolutePath("/path/to/sdk")
426+
let archs = ["x86_64-apple-macosx10.15"]
427+
let customCompileToolchain = AbsolutePath("/path/to/toolchain")
428+
try fileSystem.createDirectory(customCompileToolchain, recursive: true)
429+
430+
let targetSwiftSDK = try SwiftSDK.deriveTargetSwiftSDK(
431+
hostSwiftSDK: hostSwiftSDK,
432+
hostTriple: hostTriple,
433+
customCompileToolchain: customCompileToolchain,
434+
customCompileSDK: customCompileSDK,
435+
architectures: archs,
436+
store: store,
437+
observabilityScope: system.topScope,
438+
fileSystem: fileSystem
439+
)
440+
XCTAssertEqual(targetSwiftSDK.architectures, archs)
441+
XCTAssertEqual(targetSwiftSDK.pathsConfiguration.sdkRootPath, customCompileSDK)
442+
}
443+
}
344444
}

0 commit comments

Comments
 (0)