Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 2 additions & 2 deletions .github/workflows/swiftpm.yml
Original file line number Diff line number Diff line change
Expand Up @@ -10,8 +10,8 @@ jobs:
Xcode:
strategy:
matrix:
xcode_version: ['26.5']
runs-on: macos-26
xcode_version: ['27.0.0-beta']
runs-on: xcode-27
steps:
- uses: actions/checkout@v7
- uses: maxim-lobanov/setup-xcode@v1
Expand Down
4 changes: 2 additions & 2 deletions .github/workflows/xcodebuild.yml
Original file line number Diff line number Diff line change
Expand Up @@ -10,8 +10,8 @@ jobs:
Xcode:
strategy:
matrix:
xcode_version: ['26.5']
runs-on: macos-26
xcode_version: ['27.0.0-beta']
runs-on: xcode-27
steps:
- uses: actions/checkout@v7
- uses: maxim-lobanov/setup-xcode@v1
Expand Down
8 changes: 7 additions & 1 deletion CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,11 @@

#### Enhancements

* None.
* New syntax, attribute and declaration kinds introduced in Swift 6.4.
[John Fairhurst](https://github.com/johnfairh)

* Docs generation and code completion with Swift PM 6.4.
[John Fairhurst](https://github.com/johnfairh)

#### Bug Fixes

Expand All @@ -22,8 +26,10 @@

* New syntax, attribute and declaration kinds introduced in Swift 6.1-6.3.
[John Fairhurst](https://github.com/johnfairh)

* Improve reporting of `sourcekitdInProc` loading failures.
[Daniel Sunarjo](https://github.com/sunarjodaniel)

* Avoid `getcwd` in `absolutePathRepresentation()` for absolute paths.
[Brett Best](https://github.com/Brett-Best)

Expand Down
8 changes: 8 additions & 0 deletions Source/SourceKittenFramework/Exec.swift
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,14 @@ enum Exec {
let trimmed = encoded.trimmingCharacters(in: .whitespacesAndNewlines)
return trimmed.isEmpty ? nil : trimmed
}

/// Save `data` to a new temporary file and return its path
/// - parameter prefix: Prefix for the name of the temporary file
func save(prefix: String) -> String {
let file = URL(fileURLWithPath: NSTemporaryDirectory()).appendingPathComponent("\(prefix)-\(UUID().uuidString).log")
_ = try? data.write(to: file)
return file.path
}
}

/**
Expand Down
178 changes: 72 additions & 106 deletions Source/SourceKittenFramework/Module.swift
Original file line number Diff line number Diff line change
Expand Up @@ -29,50 +29,34 @@ public struct Module {
/**
Failable initializer to create a Module from a Swift Package Manager build record.

Use this initializer when the package has already been built and the `.build` directory exists.
Use this initializer when the package has already been built and the `.build` directory exists
and the build system is not Swift Build.

This initializer does not work with the Swift Build backend that is the default in Swift PM 6.4.
Use `init(spmArguments:spmName:inPath:)` instead, which will build the package.

- parameter spmName: Module name. Will use some non-Test module that is part of the
package if `nil`.
- parameter path: Path of the directory containing the SPM `.build` directory.
Uses the current directory by default.
*/
@available(*, deprecated, message: """
Use init(spmArguments:spmName:inPath:) instead.
This initializer does not support the Swift Build backend that is the default for SwiftPM 6.4
""")
public init?(spmName: String? = nil, inPath path: String = FileManager.default.currentDirectoryPath) {
let yamlPath = URL(fileURLWithPath: path).appendingPathComponent(".build/debug.yaml").path
guard let yaml = try? Yams.compose(yaml: String(contentsOfFile: yamlPath, encoding: .utf8)),
let commands = (yaml as Node?)?["commands"]?.mapping?.values else {
fputs("SPM build manifest does not exist at `\(yamlPath)` or does not match expected format.\n", stderr)
return nil
}

func matchModuleName(node: Node) -> Bool {
guard let nodeModuleName = node.swiftModuleName else { return false }
if let spmName = spmName {
return nodeModuleName == spmName
}
let inputs = node["inputs"]?.array(of: String.self) ?? []
return inputs.allSatisfy({ !$0.contains(".build/checkouts/") }) && !nodeModuleName.hasSuffix("Tests")
}

guard let moduleCommand = commands.first(where: matchModuleName) else {
fputs("Could not find SPM module '\(spmName ?? "(any)")'. Here are the modules available:\n", stderr)
let availableModules = commands.compactMap(\.swiftModuleName)
fputs("\(availableModules.map({ " - " + $0 }).joined(separator: "\n"))\n", stderr)
return nil
}

guard let moduleName = moduleCommand.swiftModuleName,
let compilerArguments = moduleCommand.swiftCompilerArguments else {
fputs("SPM build manifest '\(yamlPath)` does not match expected format.\n", stderr)
guard let results = SwiftPM.fromDebugYaml(moduleName: spmName, inPath: path) else {
return nil
}

self.init(name: moduleName, compilerArguments: compilerArguments)
fputs("Using module data from debug.yaml\n", stderr)
self.init(name: results.0, compilerArguments: results.1)
}

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

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

- parameter spmArguments: Additional arguments to pass to `swift build`
- parameter spmName: Module name. Will use some non-Test module that is part of the
Expand All @@ -81,16 +65,64 @@ public struct Module {
Uses the current directory by default.
*/
public init?(spmArguments: [String], spmName: String? = nil, inPath path: String = FileManager.default.currentDirectoryPath) {
/*
1. `build -v` to get a build directory and maybe some compile commands for (3).
Fast if already built.
2. If there is a `debug.yaml` file then use it -- pre-6.4 / swiftbuild back-end. Done.
3. Check for compiler flags in the build log from (1) -- if successful then Done.
4. `package clean` and `build -v` --- hope that (3) failed because module already built
before (1) so it did not rebuild anything.
5. Check for compiler flags in the build log from (4) -- if successful then Done.
6. Fail.

The swiftbuild backend uses a proprietary binary-ish msgpack format instead of the debug.yaml.
We could decode it and save the extra clean-build-verbose step but would mean heavier
dependencies and more fragility.

The swiftbuild backend produces an XCBuildData/manifest.json that looks promising but does not
contain compiler arguments.
*/

// 1. Initial build check
fputs("Running swift build\n", stderr)
let buildResults = Exec.run("/usr/bin/env", ["swift", "build"] + spmArguments, currentDirectory: path, stderr: .merge)
guard buildResults.terminationStatus == 0 else {
let file = URL(fileURLWithPath: NSTemporaryDirectory()).appendingPathComponent("swift-build-\(UUID().uuidString).log")
_ = try? buildResults.data.write(to: file)
fputs("Build failed, saved `swift build` log file: \(file.path)\n", stderr)
guard let buildResults = SwiftPM.runVerboseBuild(arguments: spmArguments, inPath: path) else {
return nil
}

self.init(spmName: spmName, inPath: path)
// 2. Pre-Swift Build solution
if SwiftPM.hasDebugYaml(inPath: path) {
if let info = SwiftPM.fromDebugYaml(moduleName: spmName, inPath: path) {
fputs("Using module data from debug.yaml\n", stderr)
self.init(name: info.0, compilerArguments: info.1)
return
}
return nil
}

// 3. See if the (1) build built our module
if let info = SwiftPM.fromBuildResults(buildResults, moduleName: spmName) {
fputs("Using module data from build output\n", stderr)
self.init(name: info.0, compilerArguments: info.1)
return
}

// 4. Clean & Build
fputs("Running swift package clean and build\n", stderr)
guard SwiftPM.runClean(inPath: path) != nil,
let secondBuildResults = SwiftPM.runVerboseBuild(arguments: spmArguments, inPath: path) else {
return nil
}

// 5. Should have our module now
if let info = SwiftPM.fromBuildResults(secondBuildResults, moduleName: spmName) {
fputs("Using module data from clean build output\n", stderr)
self.init(name: info.0, compilerArguments: info.1)
return
}

let path = secondBuildResults.save(prefix: "swift-build")
fputs("Could not parse module name '\(spmName ?? "(any)")' from swift build output: \(path)\n", stderr)
return nil
}

/**
Expand All @@ -114,9 +146,8 @@ public struct Module {
if results.terminationStatus != 0 {
fputs("Could not successfully run `xcodebuild`.\n", stderr)
fputs("Please check the build arguments.\n", stderr)
let file = URL(fileURLWithPath: NSTemporaryDirectory()).appendingPathComponent("xcodebuild-\(NSUUID().uuidString).log")
_ = try? results.data.write(to: file)
fputs("Saved `xcodebuild` log file: \(file.path)\n", stderr)
let path = results.save(prefix: "xcodebuild")
fputs("Saved `xcodebuild` log file: \(path)\n", stderr)
return nil
}
if let output = results.string,
Expand All @@ -138,9 +169,8 @@ public struct Module {
guard let arguments = parseCompilerArguments(xcodebuildOutput: xcodeBuildOutput, language: .swift, moduleName: name) else {
fputs("Could not parse compiler arguments from `xcodebuild` output.\n", stderr)
fputs("Please confirm that `xcodebuild` is building a Swift module.\n", stderr)
let file = URL(fileURLWithPath: NSTemporaryDirectory()).appendingPathComponent("xcodebuild-\(NSUUID().uuidString).log")
_ = try? xcodeBuildOutput.data(using: .utf8)?.write(to: file)
fputs("Saved `xcodebuild` log file: \(file.path)\n", stderr)
let path = results.save(prefix: "xcodebuild")
fputs("Saved `xcodebuild` log file: \(path)\n", stderr)
return nil
}
guard let moduleName = moduleName(fromArguments: arguments) else {
Expand Down Expand Up @@ -192,67 +222,3 @@ private extension Collection where Element == XcodeBuildSetting {
return lazy.compactMap(getterClosure).first
}
}

// MARK: Yams.Node helpers for SwiftPM

// The yaml structure changed in Xcode 15.3 / SwiftPM 5.10. This extension decodes both formats.
private extension Node {
// SwiftPM < 5.10: 'module-name' string
// SwiftPM 5.10: buried inside compiler args
var swiftModuleName: String? {
if let moduleNameNode = self["module-name"] {
return moduleNameNode.string
}
if let description = self["description"]?.string,
description.hasPrefix("Compiling Swift Module"),
let arguments = self["args"]?.array(of: String.self) {
return moduleName(fromArguments: arguments)
}
return nil
}

// SwiftPM < 5.10: 'sources' array of Swift files
// SwiftPM 5.10: 'inputs' array of various things including Swift files
var swiftSources: [String]? {
(self["sources"] ?? self["inputs"])?
.array(of: String.self)
.filter { $0.isSwiftFile() }
}

// SwiftPM < 5.10: 'other-args' and 'import-paths' arrays
// SwiftPM 5.10: 'args' is the entire command line that needs filtering
// for SourceKit. Additionally it contains a response file that may or
// may not contain the list of source files - guessing a window in the
// way we use this unofficial interface to SwiftPM. Use the separate
// 'inputs' node, but we must remove the response file in case it *does*
// contain the files which would cause duplicate file processing...
var swiftOtherCompilerArguments: [String]? {
if let buildCommandArguments = self["args"]?
.array(of: String.self)
.filter({ !$0.hasPrefix("@") }) {
// Drop the initial "/usr/bin/swiftc"
return filterForSourceKit(arguments: Array(buildCommandArguments.dropFirst()))
}

guard let imports = self["import-paths"]?.array(of: String.self),
let otherArguments = self["other-args"]?.array(of: String.self),
let moduleName = swiftModuleName else {
return nil
}

var arguments = ["-module-name", moduleName]
arguments.append(contentsOf: otherArguments)
arguments.append(contentsOf: ["-I"])
arguments.append(contentsOf: imports)
return arguments
}

var swiftCompilerArguments: [String]? {
guard let sources = swiftSources,
let otherCompilerArguments = swiftOtherCompilerArguments else {
return nil
}

return sources + otherCompilerArguments
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -236,4 +236,9 @@ public enum SwiftDeclarationAttributeKind: String, CaseIterable {
case _neverEmitIntoClient = "source.decl.attribute._neverEmitIntoClient"
case nonexhaustive = "source.decl.attribute.nonexhaustive"
case sensitive = "source.decl.attribute.sensitive"

// Only available in Swift >= 6.4
case diagnose = "source.decl.attribute.diagnose"
case _owned = "source.decl.attribute._owned"
case reparentable = "source.decl.attribute.reparentable"
}
Loading
Loading