Skip to content

Commit c14ce02

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 c14ce02

File tree

7 files changed

+163
-8
lines changed

7 files changed

+163
-8
lines changed

Fixtures/Scripts/dummy-swiftc.sh

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
#!/bin/sh
2+
3+
# Script which can be used as `swiftc` in order to influence `-version` output
4+
5+
if [ "$1" == "-version" ]
6+
then
7+
if [ -n "$CUSTOM_SWIFT_VERSION" ]
8+
then
9+
echo "$CUSTOM_SWIFT_VERSION"
10+
else
11+
echo "999.0"
12+
fi
13+
else
14+
swiftc "$@"
15+
fi

Sources/Build/BuildOperationBuildSystemDelegateHandler.swift

Lines changed: 40 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -38,7 +38,7 @@ typealias LLBuildBuildSystemDelegate = llbuild.BuildSystemDelegate
3838
#endif
3939

4040

41-
class CustomLLBuildCommand: SPMLLBuild.ExternalCommand {
41+
class CustomLLBuildCommand: SPMLLBuild.ExternalCommand, SPMLLBuild.ProducesCustomBuildValue {
4242
let context: BuildExecutionContext
4343

4444
required init(_ context: BuildExecutionContext) {
@@ -55,6 +55,27 @@ class CustomLLBuildCommand: SPMLLBuild.ExternalCommand {
5555
) -> Bool {
5656
fatalError("subclass responsibility")
5757
}
58+
59+
func execute(
60+
_ command: SPMLLBuild.Command,
61+
_ commandInterface: SPMLLBuild.BuildSystemCommandInterface
62+
) -> BuildValue {
63+
let result: Bool = self.execute(command, commandInterface)
64+
// FIXME: having to construct a dummy output info here seems problematic.
65+
return result ? BuildValue.SuccessfulCommand(outputInfos: [.init(device: 0, inode: 0, mode: 0, size: 0, modTime: .init(seconds: 0, nanoseconds: 0))]) : BuildValue.FailedCommand()
66+
}
67+
68+
func isResultValid(_ command: SPMLLBuild.Command, _ buildValue: BuildValue) -> Bool {
69+
switch buildValue.kind {
70+
case .failedCommand:
71+
return false
72+
case .successfulCommand:
73+
return true
74+
@unknown default:
75+
self.context.observabilityScope.emit(error: "unexpected build value kind \(buildValue.kind)")
76+
return false
77+
}
78+
}
5879
}
5980

6081
extension IndexStore.TestCaseClass.TestMethod {
@@ -473,9 +494,11 @@ public final class BuildExecutionContext {
473494
final class WriteAuxiliaryFileCommand: CustomLLBuildCommand {
474495
override func getSignature(_ command: SPMLLBuild.Command) -> [UInt8] {
475496
guard let buildDescription = self.context.buildDescription else {
497+
self.context.observabilityScope.emit(error: "unknown build description")
476498
return []
477499
}
478-
guard let tool = buildDescription.copyCommands[command.name] else {
500+
guard let tool = buildDescription.writeCommands[command.name] else {
501+
self.context.observabilityScope.emit(error: "command \(command.name) not registered")
479502
return []
480503
}
481504

@@ -538,6 +561,21 @@ final class WriteAuxiliaryFileCommand: CustomLLBuildCommand {
538561

539562
throw InternalError("unhandled generated file type '\(generatedFileType)'")
540563
}
564+
565+
override func isResultValid(_ command: SPMLLBuild.Command, _ buildValue: BuildValue) -> Bool {
566+
guard let buildDescription = self.context.buildDescription else {
567+
self.context.observabilityScope.emit(error: "unknown build description")
568+
return false
569+
}
570+
guard let tool = buildDescription.writeCommands[command.name] else {
571+
self.context.observabilityScope.emit(error: "command \(command.name) not registered")
572+
return false
573+
}
574+
if tool.alwaysOutOfDate {
575+
return false
576+
}
577+
return super.isResultValid(command, buildValue)
578+
}
541579
}
542580

543581
public protocol PackageStructureDelegate {

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/Tools.swift

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -155,10 +155,12 @@ public struct WriteAuxiliaryFile: ToolProtocol {
155155

156156
public let inputs: [Node]
157157
private let outputFilePath: AbsolutePath
158+
public let alwaysOutOfDate: Bool
158159

159-
public init(inputs: [Node], outputFilePath: AbsolutePath) {
160+
public init(inputs: [Node], outputFilePath: AbsolutePath, alwaysOutOfDate: Bool = false) {
160161
self.inputs = inputs
161162
self.outputFilePath = outputFilePath
163+
self.alwaysOutOfDate = alwaysOutOfDate
162164
}
163165

164166
public var outputs: [Node] {

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: 40 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -400,4 +400,44 @@ 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 = AbsolutePath("../../../Fixtures", relativeTo: #file).appending(components: "Scripts", "dummy-swiftc.sh")
414+
415+
// Build with a swiftc that returns version 1.0, we expect a successful build which compiles our one source file.
416+
do {
417+
let result = try execute([], environment: ["SWIFT_EXEC": dummySwiftcPath.pathString, "CUSTOM_SWIFT_VERSION": "1.0"], packagePath: fixturePath)
418+
XCTAssertTrue(result.stdout.contains("Compiling Foo Foo.swift"), "compilation task missing from build result: \(result.stdout)")
419+
XCTAssertTrue(result.stdout.contains("Build complete!"), "unexpected build result: \(result.stdout)")
420+
let swiftGetVersionFilePath = try findSwiftGetVersionFile()
421+
XCTAssertEqual(try String(contentsOfFile: swiftGetVersionFilePath.pathString).spm_chomp(), "1.0")
422+
}
423+
424+
// Build again with that same version, we do not expect any compilation tasks.
425+
do {
426+
let result = try execute([], environment: ["SWIFT_EXEC": dummySwiftcPath.pathString, "CUSTOM_SWIFT_VERSION": "1.0"], packagePath: fixturePath)
427+
XCTAssertFalse(result.stdout.contains("Compiling Foo Foo.swift"), "compilation task present in build result: \(result.stdout)")
428+
XCTAssertTrue(result.stdout.contains("Build complete!"), "unexpected build result: \(result.stdout)")
429+
let swiftGetVersionFilePath = try findSwiftGetVersionFile()
430+
XCTAssertEqual(try String(contentsOfFile: swiftGetVersionFilePath.pathString).spm_chomp(), "1.0")
431+
}
432+
433+
// Build again with a swiftc that returns version 2.0, we expect compilation happening once more.
434+
do {
435+
let result = try execute([], environment: ["SWIFT_EXEC": dummySwiftcPath.pathString, "CUSTOM_SWIFT_VERSION": "2.0"], packagePath: fixturePath)
436+
XCTAssertTrue(result.stdout.contains("Compiling Foo Foo.swift"), "compilation task missing from build result: \(result.stdout)")
437+
XCTAssertTrue(result.stdout.contains("Build complete!"), "unexpected build result: \(result.stdout)")
438+
let swiftGetVersionFilePath = try findSwiftGetVersionFile()
439+
XCTAssertEqual(try String(contentsOfFile: swiftGetVersionFilePath.pathString).spm_chomp(), "2.0")
440+
}
441+
}
442+
}
403443
}

0 commit comments

Comments
 (0)