Skip to content

Commit adcaa76

Browse files
committed
SwiftPM 6.4 support - Swift Build support
1 parent f07264a commit adcaa76

7 files changed

Lines changed: 9351 additions & 19 deletions

File tree

CHANGELOG.md

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,9 @@
99
* New syntax, attribute and declaration kinds introduced in Swift 6.4.
1010
[John Fairhurst](https://github.com/johnfairh)
1111

12+
* Docs generation and code completion with Swift PM 6.4.
13+
[John Fairhurst](https://github.com/johnfairh)
14+
1215
#### Bug Fixes
1316

1417
* None.

Source/SourceKittenFramework/Module.swift

Lines changed: 66 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -29,24 +29,34 @@ public struct Module {
2929
/**
3030
Failable initializer to create a Module from a Swift Package Manager build record.
3131

32-
Use this initializer when the package has already been built and the `.build` directory exists.
32+
Use this initializer when the package has already been built and the `.build` directory exists
33+
and the build system is not Swift Build.
34+
35+
This initializer does not work with the Swift Build backend that is the default in Swift PM 6.4.
36+
Use `init(spmArguments:spmName:inPath:)` instead, which will build the package.
3337

3438
- parameter spmName: Module name. Will use some non-Test module that is part of the
3539
package if `nil`.
3640
- parameter path: Path of the directory containing the SPM `.build` directory.
3741
Uses the current directory by default.
3842
*/
43+
@available(*, deprecated, message: """
44+
Use init(spmArguments:spmName:inPath:) instead.
45+
This initializer does not support the Swift Build backend that is the default for SwiftPM 6.4
46+
""")
3947
public init?(spmName: String? = nil, inPath path: String = FileManager.default.currentDirectoryPath) {
4048
guard let results = SwiftPM.fromDebugYaml(moduleName: spmName, inPath: path) else {
4149
return nil
4250
}
51+
fputs("Using module data from debug.yaml\n", stderr)
4352
self.init(name: results.0, compilerArguments: results.1)
4453
}
4554

4655
/**
4756
Failable initializer to create a Module by building a Swift Package Manager project.
4857

49-
Use this initializer if the package has not been built or may have changed since last built.
58+
As of SwiftPM 6.4 this is likely to build the package even it has been previously built because
59+
of the build system changing to Swift Build.
5060

5161
- parameter spmArguments: Additional arguments to pass to `swift build`
5262
- parameter spmName: Module name. Will use some non-Test module that is part of the
@@ -55,12 +65,64 @@ public struct Module {
5565
Uses the current directory by default.
5666
*/
5767
public init?(spmArguments: [String], spmName: String? = nil, inPath path: String = FileManager.default.currentDirectoryPath) {
68+
/*
69+
1. `build -v` to get a build directory and maybe some compile commands for (3).
70+
Fast if already built.
71+
2. If there is a `debug.yaml` file then use it -- pre-6.4 / swiftbuild back-end. Done.
72+
3. Check for compiler flags in the build log from (1) -- if successful then Done.
73+
4. `package clean` and `build -v` --- hope that (3) failed because module already built
74+
before (1) so it did not rebuild anything.
75+
5. Check for compiler flags in the build log from (4) -- if successful then Done.
76+
6. Fail.
77+
78+
The swiftbuild backend uses a proprietary binary-ish msgpack format instead of the debug.yaml.
79+
We could decode it and save the extra clean-build-verbose step but would mean heavier
80+
dependencies and more fragility.
81+
82+
The swiftbuild backend produces an XCBuildData/manifest.json that looks promising but does not
83+
contain compiler arguments.
84+
*/
85+
86+
// 1. Initial build check
5887
fputs("Running swift build\n", stderr)
59-
guard SwiftPM.runBuild(arguments: spmArguments, inPath: path) != nil else {
88+
guard let buildResults = SwiftPM.runVerboseBuild(arguments: spmArguments, inPath: path) else {
89+
return nil
90+
}
91+
92+
// 2. Pre-Swift Build solution
93+
if SwiftPM.hasDebugYaml(inPath: path) {
94+
if let info = SwiftPM.fromDebugYaml(moduleName: spmName, inPath: path) {
95+
fputs("Using module data from debug.yaml\n", stderr)
96+
self.init(name: info.0, compilerArguments: info.1)
97+
return
98+
}
99+
return nil
100+
}
101+
102+
// 3. See if the (1) build built our module
103+
if let info = SwiftPM.fromBuildResults(buildResults, moduleName: spmName) {
104+
fputs("Using module data from build output\n", stderr)
105+
self.init(name: info.0, compilerArguments: info.1)
106+
return
107+
}
108+
109+
// 4. Clean & Build
110+
fputs("Running swift package clean and build\n", stderr)
111+
guard SwiftPM.runClean(inPath: path) != nil,
112+
let secondBuildResults = SwiftPM.runVerboseBuild(arguments: spmArguments, inPath: path) else {
60113
return nil
61114
}
62115

63-
self.init(spmName: spmName, inPath: path)
116+
// 5. Should have our module now
117+
if let info = SwiftPM.fromBuildResults(secondBuildResults, moduleName: spmName) {
118+
fputs("Using module data from clean build output\n", stderr)
119+
self.init(name: info.0, compilerArguments: info.1)
120+
return
121+
}
122+
123+
let path = secondBuildResults.save(prefix: "swift-build")
124+
fputs("Could not parse module name '\(spmName ?? "(any)")' from swift build output: \(path)\n", stderr)
125+
return nil
64126
}
65127

66128
/**

Source/SourceKittenFramework/SwiftPM.swift

Lines changed: 37 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,11 @@ enum SwiftPM {
1111
return URL(fileURLWithPath: path).appendingPathComponent(".build/debug.yaml").path
1212
}
1313

14+
/// Is there a `debug.yaml` file in the .bulid directory
15+
static func hasDebugYaml(inPath path: String) -> Bool {
16+
return FileManager.default.fileExists(atPath: debugYamlPath(inPath: path))
17+
}
18+
1419
/**
1520
Get module build flags and optionally name from the `debug.yaml` file produced by the default SwiftPM build
1621
system earlier than Swift 6.4.
@@ -55,7 +60,7 @@ enum SwiftPM {
5560
// MARK: Build commands
5661

5762
/**
58-
Run a build command. If it works then return the results otherwise report the error and save the output.
63+
Run a build command. If it works then return the results otherwise report the error and save the output.
5964

6065
- parameter arguments: Program name and arguments
6166
- parameter path: Working directory for program
@@ -75,6 +80,37 @@ enum SwiftPM {
7580
static func runBuild(arguments: [String], inPath path: String) -> Exec.Results? {
7681
return runCheckedCommand(arguments: ["swift", "build"] + arguments, inPath: path, msgName: "swift build")
7782
}
83+
84+
/// Run `swift build -v`. Return the successful results or `nil` and report the error.
85+
static func runVerboseBuild(arguments: [String], inPath path: String) -> Exec.Results? {
86+
return runBuild(arguments: ["-v"] + arguments, inPath: path)
87+
}
88+
89+
/// Run `swift package clean`. Return the successful results or `nil` and report the error.
90+
static func runClean(inPath path: String) -> Exec.Results? {
91+
return runCheckedCommand(arguments: ["swift", "package", "clean"], inPath: path, msgName: "swift package clean")
92+
}
93+
94+
// MARK: Build output parsing
95+
96+
/**
97+
Given some results from `swift build -v`, find the compiler arguments for the module.
98+
99+
- parameter swiftBuildOutput: Output of `swift build -v
100+
- parameter moduleName: Module of interest, `nil` to use any
101+
- returns: tuple of module name and compiler args, or `nil` on failure
102+
*/
103+
static func fromBuildResults(_ results: Exec.Results, moduleName: String?) -> (String, [String])? {
104+
guard let buildOutput = results.string,
105+
let swiftArgs = SourceKittenFramework.parseCompilerArguments(
106+
xcodebuildOutput: buildOutput,
107+
language: .swift,
108+
moduleName: moduleName),
109+
let actualModuleName = SourceKittenFramework.moduleName(fromArguments: swiftArgs) else {
110+
return nil
111+
}
112+
return (actualModuleName, swiftArgs)
113+
}
78114
}
79115

80116
// MARK: Yams.Node helpers for SwiftPM's debug.yaml

Source/SourceKittenFramework/Xcode.swift

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -119,7 +119,8 @@ func filterForSourceKit(arguments args: [String]) -> [String] {
119119
"-use-frontend-parseable-output",
120120
"-incremental",
121121
"-serialize-diagnostics",
122-
"-emit-dependencies"
122+
"-emit-dependencies",
123+
"-v"
123124
].contains($0)
124125
}.map {
125126
if $0 == "-O" {

Source/sourcekitten/Complete.swift

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -43,7 +43,7 @@ extension SourceKitten {
4343
args.append(contentsOf: ["-sdk", sdkPath()])
4444
}
4545
} else {
46-
guard let module = Module(spmName: spmModule) else {
46+
guard let module = Module(spmArguments: [], spmName: spmModule) else {
4747
throw SourceKittenError.invalidArgument(description: "Bad module name")
4848
}
4949
args = module.compilerArguments

0 commit comments

Comments
 (0)