Skip to content

Commit 5f48133

Browse files
committed
Implement swift-get-version in SwiftPM
The changes in #6585 meant that we are no longer using LLBuild's built-in tracking of Swift compiler versions. We are lacking the infrastructure to really use that for the same purpose since we are now running the Swift compiler as a shell-tool, so this is adding a poor man's version of that. We have a task that writes the output of `swift -version` for a particular Swift compiler path to the build directory and all Swift tasks that are using that compiler depend on that file as an input. This should give us the desired behavior of the tasks re-running if the Swift version changes. rdar://114047018
1 parent 667a006 commit 5f48133

File tree

10 files changed

+170
-9
lines changed

10 files changed

+170
-9
lines changed

Package.swift

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -666,6 +666,13 @@ if ProcessInfo.processInfo.environment["SWIFTCI_DISABLE_SDK_DEPENDENT_TESTS"] ==
666666
]
667667
),
668668

669+
.executableTarget(
670+
name: "dummy-swiftc",
671+
dependencies: [
672+
"Basics",
673+
]
674+
),
675+
669676
.testTarget(
670677
name: "CommandsTests",
671678
dependencies: [
@@ -681,6 +688,7 @@ if ProcessInfo.processInfo.environment["SWIFTCI_DISABLE_SDK_DEPENDENT_TESTS"] ==
681688
"SourceControl",
682689
"SPMTestSupport",
683690
"Workspace",
691+
"dummy-swiftc",
684692
]
685693
),
686694
])

Sources/Build/BuildOperationBuildSystemDelegateHandler.swift

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -473,9 +473,11 @@ public final class BuildExecutionContext {
473473
final class WriteAuxiliaryFileCommand: CustomLLBuildCommand {
474474
override func getSignature(_ command: SPMLLBuild.Command) -> [UInt8] {
475475
guard let buildDescription = self.context.buildDescription else {
476+
self.context.observabilityScope.emit(error: "unknown build description")
476477
return []
477478
}
478-
guard let tool = buildDescription.copyCommands[command.name] else {
479+
guard let tool = buildDescription.writeCommands[command.name] else {
480+
self.context.observabilityScope.emit(error: "command \(command.name) not registered")
479481
return []
480482
}
481483

Sources/Build/LLBuildManifestBuilder.swift

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -53,6 +53,9 @@ public class LLBuildManifestBuilder {
5353
var buildParameters: BuildParameters { self.plan.buildParameters }
5454
var buildEnvironment: BuildEnvironment { self.buildParameters.buildEnvironment }
5555

56+
/// Mapping from Swift compiler path to Swift get version files.
57+
var swiftGetVersionFiles = [AbsolutePath: AbsolutePath]()
58+
5659
/// Create a new builder with a build plan.
5760
public init(
5861
_ plan: BuildPlan,
@@ -71,6 +74,8 @@ public class LLBuildManifestBuilder {
7174
/// Generate manifest at the given path.
7275
@discardableResult
7376
public func generateManifest(at path: AbsolutePath) throws -> BuildManifest {
77+
self.swiftGetVersionFiles.removeAll()
78+
7479
self.manifest.createTarget(TargetKind.main.targetName)
7580
self.manifest.createTarget(TargetKind.test.targetName)
7681
self.manifest.defaultTarget = TargetKind.main.targetName
@@ -608,6 +613,9 @@ extension LLBuildManifestBuilder {
608613
) throws -> [Node] {
609614
var inputs = target.sources.map(Node.file)
610615

616+
let swiftVersionFilePath = addSwiftGetVersionCommand(buildParameters: target.buildParameters)
617+
inputs.append(.file(swiftVersionFilePath))
618+
611619
// Add resources node as the input to the target. This isn't great because we
612620
// don't need to block building of a module until its resources are assembled but
613621
// we don't currently have a good way to express that resources should be built
@@ -733,6 +741,22 @@ extension LLBuildManifestBuilder {
733741
arguments: moduleWrapArgs
734742
)
735743
}
744+
745+
private func addSwiftGetVersionCommand(buildParameters: BuildParameters) -> AbsolutePath {
746+
let swiftCompilerPath = buildParameters.toolchain.swiftCompilerPath
747+
748+
// If we are already tracking this compiler, we can re-use the existing command by just returning the tracking file.
749+
if let swiftVersionFilePath = swiftGetVersionFiles[swiftCompilerPath] {
750+
return swiftVersionFilePath
751+
}
752+
753+
// Otherwise, come up with a path for the new file and generate a command to populate it.
754+
let swiftCompilerPathHash = String(swiftCompilerPath.pathString.hash, radix: 16, uppercase: true)
755+
let swiftVersionFilePath = buildParameters.buildPath.appending(component: "swift-version-\(swiftCompilerPathHash).txt")
756+
self.manifest.addSwiftGetVersionCommand(swiftCompilerPath: swiftCompilerPath, swiftVersionFilePath: swiftVersionFilePath)
757+
swiftGetVersionFiles[swiftCompilerPath] = swiftVersionFilePath
758+
return swiftVersionFilePath
759+
}
736760
}
737761

738762
extension SwiftDriver.Job {

Sources/LLBuildManifest/BuildManifest.swift

Lines changed: 33 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -12,14 +12,16 @@
1212

1313
import Basics
1414

15+
import class TSCBasic.Process
16+
1517
public protocol AuxiliaryFileType {
1618
static var name: String { get }
1719

1820
static func getFileContents(inputs: [Node]) throws -> String
1921
}
2022

2123
public enum WriteAuxiliary {
22-
public static let fileTypes: [AuxiliaryFileType.Type] = [LinkFileList.self, SourcesFileList.self]
24+
public static let fileTypes: [AuxiliaryFileType.Type] = [LinkFileList.self, SourcesFileList.self, SwiftGetVersion.self]
2325

2426
public struct LinkFileList: AuxiliaryFileType {
2527
public static let name = "link-file-list"
@@ -76,6 +78,26 @@ public enum WriteAuxiliary {
7678
return contents
7779
}
7880
}
81+
82+
public struct SwiftGetVersion: AuxiliaryFileType {
83+
public static let name = "swift-get-version"
84+
85+
public static func computeInputs(swiftCompilerPath: AbsolutePath) -> [Node] {
86+
return [.virtual(Self.name), .file(swiftCompilerPath)]
87+
}
88+
89+
public static func getFileContents(inputs: [Node]) throws -> String {
90+
guard let swiftCompilerPathString = inputs.first(where: { $0.kind == .file })?.name else {
91+
throw Error.unknownSwiftCompilerPath
92+
}
93+
let swiftCompilerPath = try AbsolutePath(validating: swiftCompilerPathString)
94+
return try TSCBasic.Process.checkNonZeroExit(args: swiftCompilerPath.pathString, "-version")
95+
}
96+
97+
private enum Error: Swift.Error {
98+
case unknownSwiftCompilerPath
99+
}
100+
}
79101
}
80102

81103
public struct BuildManifest {
@@ -173,6 +195,16 @@ public struct BuildManifest {
173195
commands[name] = Command(name: name, tool: tool)
174196
}
175197

198+
public mutating func addSwiftGetVersionCommand(
199+
swiftCompilerPath: AbsolutePath,
200+
swiftVersionFilePath: AbsolutePath
201+
) {
202+
let inputs = WriteAuxiliary.SwiftGetVersion.computeInputs(swiftCompilerPath: swiftCompilerPath)
203+
let tool = WriteAuxiliaryFile(inputs: inputs, outputFilePath: swiftVersionFilePath, alwaysOutOfDate: true)
204+
let name = swiftVersionFilePath.pathString
205+
commands[name] = Command(name: name, tool: tool)
206+
}
207+
176208
public mutating func addPkgStructureCmd(
177209
name: String,
178210
inputs: [Node],

Sources/LLBuildManifest/ManifestWriter.swift

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -72,6 +72,10 @@ public struct ManifestWriter {
7272
manifestToolWriter["inputs"] = tool.inputs
7373
manifestToolWriter["outputs"] = tool.outputs
7474

75+
if tool.alwaysOutOfDate {
76+
manifestToolWriter["always-out-of-date"] = "true"
77+
}
78+
7579
tool.write(to: manifestToolWriter)
7680

7781
stream.send("\n")

Sources/LLBuildManifest/Tools.swift

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,9 @@ public protocol ToolProtocol: Codable {
1717
/// The name of the tool.
1818
static var name: String { get }
1919

20+
/// Whether or not the tool should run on every build instead of using dependency tracking.
21+
var alwaysOutOfDate: Bool { get }
22+
2023
/// The list of inputs to declare.
2124
var inputs: [Node] { get }
2225

@@ -28,6 +31,8 @@ public protocol ToolProtocol: Codable {
2831
}
2932

3033
extension ToolProtocol {
34+
public var alwaysOutOfDate: Bool { return false }
35+
3136
public func write(to stream: ManifestToolStream) {}
3237
}
3338

@@ -155,10 +160,12 @@ public struct WriteAuxiliaryFile: ToolProtocol {
155160

156161
public let inputs: [Node]
157162
private let outputFilePath: AbsolutePath
163+
public let alwaysOutOfDate: Bool
158164

159-
public init(inputs: [Node], outputFilePath: AbsolutePath) {
165+
public init(inputs: [Node], outputFilePath: AbsolutePath, alwaysOutOfDate: Bool = false) {
160166
self.inputs = inputs
161167
self.outputFilePath = outputFilePath
168+
self.alwaysOutOfDate = alwaysOutOfDate
162169
}
163170

164171
public var outputs: [Node] {

Sources/SPMTestSupport/SwiftPMProduct.swift

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -46,14 +46,18 @@ extension SwiftPM {
4646

4747
/// Path to currently built binary.
4848
public var path: AbsolutePath {
49+
return Self.testBinaryPath(for: self.executableName)
50+
}
51+
52+
public static func testBinaryPath(for executableName: RelativePath) -> AbsolutePath {
4953
#if canImport(Darwin)
5054
for bundle in Bundle.allBundles where bundle.bundlePath.hasSuffix(".xctest") {
51-
return try! AbsolutePath(AbsolutePath(validating: bundle.bundlePath).parentDirectory, self.executableName)
55+
return try! AbsolutePath(AbsolutePath(validating: bundle.bundlePath).parentDirectory, executableName)
5256
}
5357
fatalError()
5458
#else
5559
return try! AbsolutePath(validating: CommandLine.arguments.first!, relativeTo: localFileSystem.currentWorkingDirectory!)
56-
.parentDirectory.appending(self.executableName)
60+
.parentDirectory.appending(executableName)
5761
#endif
5862
}
5963
}

Sources/dummy-swiftc/main.swift

Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,27 @@
1+
// This program can be used as `swiftc` in order to influence `-version` output
2+
3+
import Foundation
4+
5+
import class TSCBasic.Process
6+
7+
let info = ProcessInfo.processInfo
8+
let env = info.environment
9+
10+
if info.arguments.last == "-version" {
11+
if let customSwiftVersion = env["CUSTOM_SWIFT_VERSION"] {
12+
print(customSwiftVersion)
13+
} else {
14+
print("999.0")
15+
}
16+
} else {
17+
let swiftPath: String
18+
if let swiftOriginalPath = env["SWIFT_ORIGINAL_PATH"] {
19+
swiftPath = swiftOriginalPath
20+
} else {
21+
swiftPath = "/usr/bin/swiftc"
22+
}
23+
24+
let result = try Process.popen(arguments: [swiftPath] + info.arguments.dropFirst())
25+
print(try result.utf8Output())
26+
print(try result.utf8stderrOutput())
27+
}

Tests/BuildTests/BuildPlanTests.swift

Lines changed: 8 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -867,8 +867,9 @@ final class BuildPlanTests: XCTestCase {
867867
let llbuild = LLBuildManifestBuilder(plan, fileSystem: fs, observabilityScope: observability.topScope)
868868
try llbuild.generateManifest(at: yaml)
869869
let contents: String = try fs.readFileContents(yaml)
870+
let swiftGetVersionFilePath = try XCTUnwrap(llbuild.swiftGetVersionFiles.first?.value)
870871
XCTAssertMatch(contents, .contains("""
871-
inputs: ["\(Pkg.appending(components: "Sources", "exe", "main.swift").escapedPathString())","\(buildPath.appending(components: "PkgLib.swiftmodule").escapedPathString())","\(buildPath.appending(components: "exe.build", "sources").escapedPathString())"]
872+
inputs: ["\(Pkg.appending(components: "Sources", "exe", "main.swift").escapedPathString())","\(swiftGetVersionFilePath.escapedPathString())","\(buildPath.appending(components: "PkgLib.swiftmodule").escapedPathString())","\(buildPath.appending(components: "exe.build", "sources").escapedPathString())"]
872873
"""))
873874

874875
}
@@ -896,8 +897,9 @@ final class BuildPlanTests: XCTestCase {
896897
try llbuild.generateManifest(at: yaml)
897898
let contents: String = try fs.readFileContents(yaml)
898899
let buildPath = plan.buildParameters.dataPath.appending(components: "debug")
900+
let swiftGetVersionFilePath = try XCTUnwrap(llbuild.swiftGetVersionFiles.first?.value)
899901
XCTAssertMatch(contents, .contains("""
900-
inputs: ["\(Pkg.appending(components: "Sources", "exe", "main.swift").escapedPathString())","\(buildPath.appending(components: "exe.build", "sources").escapedPathString())"]
902+
inputs: ["\(Pkg.appending(components: "Sources", "exe", "main.swift").escapedPathString())","\(swiftGetVersionFilePath.escapedPathString())","\(buildPath.appending(components: "exe.build", "sources").escapedPathString())"]
901903
"""))
902904
}
903905
}
@@ -3894,14 +3896,15 @@ final class BuildPlanTests: XCTestCase {
38943896
let llbuild = LLBuildManifestBuilder(plan, fileSystem: fs, observabilityScope: observability.topScope)
38953897
try llbuild.generateManifest(at: yaml)
38963898
let contents: String = try fs.readFileContents(yaml)
3899+
let swiftGetVersionFilePath = try XCTUnwrap(llbuild.swiftGetVersionFiles.first?.value)
38973900

38983901
#if os(Windows)
38993902
let suffix = ".exe"
39003903
#else // FIXME(5472) - the suffix is dropped
39013904
let suffix = ""
39023905
#endif
39033906
XCTAssertMatch(contents, .contains("""
3904-
inputs: ["\(PkgA.appending(components: "Sources", "swiftlib", "lib.swift").escapedPathString())","\(buildPath.appending(components: "exe\(suffix)").escapedPathString())","\(buildPath.appending(components: "swiftlib.build", "sources").escapedPathString())"]
3907+
inputs: ["\(PkgA.appending(components: "Sources", "swiftlib", "lib.swift").escapedPathString())","\(swiftGetVersionFilePath.escapedPathString())","\(buildPath.appending(components: "exe\(suffix)").escapedPathString())","\(buildPath.appending(components: "swiftlib.build", "sources").escapedPathString())"]
39053908
outputs: ["\(buildPath.appending(components: "swiftlib.build", "lib.swift.o").escapedPathString())","\(buildPath.escapedPathString())
39063909
"""))
39073910
}
@@ -4804,10 +4807,11 @@ final class BuildPlanTests: XCTestCase {
48044807
let yaml = buildPath.appending("release.yaml")
48054808
let llbuild = LLBuildManifestBuilder(plan, fileSystem: fs, observabilityScope: observability.topScope)
48064809
try llbuild.generateManifest(at: yaml)
4810+
let swiftGetVersionFilePath = try XCTUnwrap(llbuild.swiftGetVersionFiles.first?.value)
48074811

48084812
let yamlContents: String = try fs.readFileContents(yaml)
48094813
let inputs: SerializedJSON = """
4810-
inputs: ["\(AbsolutePath("/Pkg/Snippets/ASnippet.swift"))","\(AbsolutePath("/Pkg/.build/debug/Lib.swiftmodule"))"
4814+
inputs: ["\(AbsolutePath("/Pkg/Snippets/ASnippet.swift"))","\(swiftGetVersionFilePath.escapedPathString())","\(AbsolutePath("/Pkg/.build/debug/Lib.swiftmodule"))"
48114815
"""
48124816
XCTAssertMatch(yamlContents, .contains(inputs.underlying))
48134817
}

Tests/CommandsTests/BuildToolTests.swift

Lines changed: 49 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -400,4 +400,53 @@ final class BuildToolTests: CommandsTestCase {
400400
}
401401
}
402402
}
403+
404+
func testSwiftGetVersion() throws {
405+
try fixture(name: "Miscellaneous/Simple") { fixturePath in
406+
func findSwiftGetVersionFile() throws -> AbsolutePath {
407+
let buildArenaPath = fixturePath.appending(components: ".build", "debug")
408+
let files = try localFileSystem.getDirectoryContents(buildArenaPath)
409+
let filename = try XCTUnwrap(files.first { $0.hasPrefix("swift-version") })
410+
return buildArenaPath.appending(component: filename)
411+
}
412+
413+
let dummySwiftcPath = SwiftPM.testBinaryPath(for: "dummy-swiftc")
414+
let swiftCompilerPath = try UserToolchain.default.swiftCompilerPath
415+
416+
var environment = [
417+
"SWIFT_EXEC": dummySwiftcPath.pathString,
418+
// Environment variables used by `dummy-swiftc.sh`
419+
"SWIFT_ORIGINAL_PATH": swiftCompilerPath.pathString,
420+
"CUSTOM_SWIFT_VERSION": "1.0",
421+
]
422+
423+
// Build with a swiftc that returns version 1.0, we expect a successful build which compiles our one source file.
424+
do {
425+
let result = try execute(["--verbose"], environment: environment, packagePath: fixturePath)
426+
XCTAssertTrue(result.stdout.contains("\(dummySwiftcPath.pathString) -module-name"), "compilation task missing from build result: \(result.stdout)")
427+
XCTAssertTrue(result.stdout.contains("Build complete!"), "unexpected build result: \(result.stdout)")
428+
let swiftGetVersionFilePath = try findSwiftGetVersionFile()
429+
XCTAssertEqual(try String(contentsOfFile: swiftGetVersionFilePath.pathString).spm_chomp(), "1.0")
430+
}
431+
432+
// Build again with that same version, we do not expect any compilation tasks.
433+
do {
434+
let result = try execute(["--verbose"], environment: environment, packagePath: fixturePath)
435+
XCTAssertFalse(result.stdout.contains("\(dummySwiftcPath.pathString) -module-name"), "compilation task present in build result: \(result.stdout)")
436+
XCTAssertTrue(result.stdout.contains("Build complete!"), "unexpected build result: \(result.stdout)")
437+
let swiftGetVersionFilePath = try findSwiftGetVersionFile()
438+
XCTAssertEqual(try String(contentsOfFile: swiftGetVersionFilePath.pathString).spm_chomp(), "1.0")
439+
}
440+
441+
// Build again with a swiftc that returns version 2.0, we expect compilation happening once more.
442+
do {
443+
environment["CUSTOM_SWIFT_VERSION"] = "2.0"
444+
let result = try execute(["--verbose"], environment: environment, packagePath: fixturePath)
445+
XCTAssertTrue(result.stdout.contains("\(dummySwiftcPath.pathString) -module-name"), "compilation task missing from build result: \(result.stdout)")
446+
XCTAssertTrue(result.stdout.contains("Build complete!"), "unexpected build result: \(result.stdout)")
447+
let swiftGetVersionFilePath = try findSwiftGetVersionFile()
448+
XCTAssertEqual(try String(contentsOfFile: swiftGetVersionFilePath.pathString).spm_chomp(), "2.0")
449+
}
450+
}
451+
}
403452
}

0 commit comments

Comments
 (0)