Skip to content

Generalize the Swift version regex #519

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 1 commit into from
May 22, 2025
Merged
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
Original file line number Diff line number Diff line change
Expand Up @@ -84,12 +84,6 @@ final public class DocumentationCompilerSpec: GenericCompilerSpec, SpecIdentifie

switch DocumentationType(from: cbc) {
case .executable:
guard swiftCompilerInfo.supportsSymbolGraphMinimumAccessLevelFlag else {
// The swift compiler doesn't support specifying a minimum access level,
// so just return an empty array.
return additionalFlags
}

// When building executable types (like applications and command-line tools), include
// internal symbols in the generated symbol graph.
return additionalFlags.appending(contentsOf: ["-symbol-graph-minimum-access-level", "internal"])
Expand Down Expand Up @@ -476,26 +470,6 @@ extension DocumentationCompilerSpec {
}
}

private extension DiscoveredSwiftCompilerToolSpecInfo {
/// A Boolean value that is true if the Swift compiler supports specifying a minimum
/// access level for symbol graph generation.
///
/// The `-symbol-graph-minimum-access-level` flag was added in `swiftlang-5.6.0.316.14`.
var supportsSymbolGraphMinimumAccessLevelFlag: Bool {
// We're explicitly checking the swiftlangVersion here instead of a value in the
// the toolchain's `features.json` because a `features.json` flag wasn't originally
// added when support for `-symbol-graph-minimum-access-level` was added.
//
// Instead of regressing the current documentation build experience while waiting for a
// submission that includes the `features.json` flag, we're checking the raw version here.
//
// For future coordinated changes like this between the Swift-DocC infrastructure
// in Swift Build and the Swift compiler, we'll rely on the `features.json` instead of
// raw version numbers.
return swiftlangVersion >= Version(5, 6, 0, 316, 14)
}
}

// MARK: - Diagnostics

/// An output parser which forwards all output unchanged, then generates diagnostics from a serialized diagnostics file passed in the payload once it is closed.
Expand Down
44 changes: 12 additions & 32 deletions Sources/SWBCore/SpecImplementations/Tools/SwiftCompiler.swift
Original file line number Diff line number Diff line change
Expand Up @@ -1027,16 +1027,14 @@ public struct DiscoveredSwiftCompilerToolSpecInfo: DiscoveredCommandLineToolSpec
public let toolPath: Path
/// The version of the Swift language in the tool.
public let swiftVersion: Version
/// The version of swiftlang in the tool.
public let swiftlangVersion: Version
/// The name that this Swift was tagged with.
public let swiftTag: String
/// The version of the stable ABI for the Swift language in the tool.
public let swiftABIVersion: String?
/// The version of clang in the tool.
public let clangVersion: Version?
/// `compilerClientsConfig` blocklists for Swift
public let blocklists: SwiftBlocklists

public var toolVersion: Version? { return self.swiftlangVersion }
public var toolVersion: Version? { return self.swiftVersion }

public var hostLibraryDirectory: Path {
toolPath.dirname.dirname.join("lib/swift/host")
Expand Down Expand Up @@ -1066,12 +1064,11 @@ public struct DiscoveredSwiftCompilerToolSpecInfo: DiscoveredCommandLineToolSpec
return toolFeatures.has(flag)
}

public init(toolPath: Path, swiftVersion: Version, swiftlangVersion: Version, swiftABIVersion: String?, clangVersion: Version?, blocklists: SwiftBlocklists, toolFeatures: ToolFeatures<DiscoveredSwiftCompilerToolSpecInfo.FeatureFlag>) {
public init(toolPath: Path, swiftVersion: Version, swiftTag: String, swiftABIVersion: String?, blocklists: SwiftBlocklists, toolFeatures: ToolFeatures<DiscoveredSwiftCompilerToolSpecInfo.FeatureFlag>) {
self.toolPath = toolPath
self.swiftVersion = swiftVersion
self.swiftlangVersion = swiftlangVersion
self.swiftTag = swiftTag
self.swiftABIVersion = swiftABIVersion
self.clangVersion = clangVersion
self.blocklists = blocklists
self.toolFeatures = toolFeatures
}
Expand Down Expand Up @@ -2213,7 +2210,7 @@ public final class SwiftCompilerSpec : CompilerSpec, SpecIdentifierType, SwiftDi
if !toolchains.isEmpty {
environment.append(("TOOLCHAINS", toolchains))
}
let additionalSignatureData = "SWIFTC: \(toolSpecInfo.swiftlangVersion.description)"
let additionalSignatureData = "SWIFTC: \(toolSpecInfo.swiftTag)"
let environmentBindings = EnvironmentBindings(environment)

let indexingInputReplacements = Dictionary(uniqueKeysWithValues: cbc.inputs.compactMap { ftb -> (Path, Path)? in
Expand Down Expand Up @@ -3600,35 +3597,18 @@ public func discoveredSwiftCompilerInfo(_ producer: any CommandProducer, _ deleg

// Values we will parse. If we end up not parsing any values, then we return an empty info struct.
var swiftVersion: Version? = nil
var swiftlangVersion: Version? = nil
var clangVersion: Version? = nil
var swiftTag: String? = nil
var swiftABIVersion: String? = nil

// Note that Swift toolchains downloaded from swift.org have a swiftc with a different version format than those built by Apple; the 'releaseVersionRegex' reflects that format. c.f. <rdar://problem/34956869>
let versionRegex = #/Apple Swift version (?<swiftVersion>[\d.]+) \(swiftlang-(?<swiftlangVersion>[\d.]+) clang-(?<clangVersion>[\d.]+)\)/#
let releaseVersionRegex = #/(?:Apple )?Swift version (?<swiftVersion>[\d.]+) \(swift-(?<swiftlangVersion>[\d.]+)-RELEASE\)/#
let developmentVersionRegex = #/Swift version (?<swiftVersion>[\d.]+)-dev \(LLVM (?:\b[0-9a-f]+), Swift (?:\b[0-9a-f]+)\)/#
let versionRegex = #/Swift version (?<swiftVersion>[\d.]+).*\((?<swiftTag>.*)\)/#
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Just confirming: works for both Xcode and swift.org toolchains?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yes. The different versions we have are:

Apple Swift version 6.1 (swiftlang-6.1.x.x.x clang-1x00.x.x.x) # apple release
Swift version 6.1 (swift-6.1-RELEASE) # swift.org release
Swift version 6.1-dev (LLVM hash, Swift hash) # swift.org nightlies and at desk

And I'm about to put up a PR to make it:

Swift version 6.1 (swift-6.1-DEVELOPMENT-SNAPSHOT-year-month-day-a) # nightlies
Swift version 6.1-dev (Swift hash LLVM hash) # at desk

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

let abiVersionRegex = #/ABI version: (?<abiVersion>[\d.]+)/#

// Iterate over each line and add any discovered info to the info object.
for line in outputString.components(separatedBy: "\n") {
if swiftlangVersion == nil {
if swiftVersion == nil {
if let groups = try versionRegex.firstMatch(in: line) {
swiftVersion = try? Version(String(groups.output.swiftVersion))
swiftlangVersion = try? Version(String(groups.output.swiftlangVersion))
clangVersion = try? Version(String(groups.output.clangVersion))
}
else if let groups = try releaseVersionRegex.firstMatch(in: line) {
swiftVersion = try? Version(String(groups.output.swiftVersion))
swiftlangVersion = try? Version(String(groups.output.swiftlangVersion))
// This form has no clang version.
} else if let groups = try developmentVersionRegex.firstMatch(in: line) {
swiftVersion = try? Version(String(groups.output.swiftVersion))
guard let swiftVersion else {
throw StubError.error("Could not parse Swift version from: \(outputString)")
}
clangVersion = try? Version(swiftVersion.description + ".999.999")
swiftlangVersion = try? Version(swiftVersion.description + ".999.999")
swiftTag = String(groups.output.swiftTag)
}
}
if swiftABIVersion == nil {
Expand All @@ -3638,7 +3618,7 @@ public func discoveredSwiftCompilerInfo(_ producer: any CommandProducer, _ deleg
}
}

guard let swiftVersion, let swiftlangVersion else {
guard let swiftVersion, let swiftTag else {
throw StubError.error("Could not parse Swift versions from: \(outputString)")
}

Expand Down Expand Up @@ -3674,7 +3654,7 @@ public func discoveredSwiftCompilerInfo(_ producer: any CommandProducer, _ deleg
blocklists.installAPILazyTypecheck = getBlocklist(type: SwiftBlocklists.InstallAPILazyTypecheckInfo.self, toolchainFilename: "swift-lazy-installapi.json", delegate: delegate)
blocklists.caching = getBlocklist(type: SwiftBlocklists.CachingBlockList.self, toolchainFilename: "swift-caching.json", delegate: delegate)
blocklists.languageFeatureEnablement = getBlocklist(type: SwiftBlocklists.LanguageFeatureEnablementInfo.self, toolchainFilename: "swift-language-feature-enablement.json", delegate: delegate)
return DiscoveredSwiftCompilerToolSpecInfo(toolPath: toolPath, swiftVersion: swiftVersion, swiftlangVersion: swiftlangVersion, swiftABIVersion: swiftABIVersion, clangVersion: clangVersion, blocklists: blocklists, toolFeatures: getFeatures(at: toolPath))
return DiscoveredSwiftCompilerToolSpecInfo(toolPath: toolPath, swiftVersion: swiftVersion, swiftTag: swiftTag, swiftABIVersion: swiftABIVersion, blocklists: blocklists, toolFeatures: getFeatures(at: toolPath))
})
}

Expand Down
29 changes: 16 additions & 13 deletions Tests/SWBCoreTests/CommandLineToolSpecDiscoveredInfoTests.swift
Original file line number Diff line number Diff line change
Expand Up @@ -112,35 +112,38 @@ import SWBMacro
try await withSpec(SwiftCompilerSpec.self, .deferred) { (info: DiscoveredSwiftCompilerToolSpecInfo) in
#expect(info.toolPath.basename == core.hostOperatingSystem.imageFormat.executableName(basename: "swiftc"))
#expect(info.swiftVersion > Version(0, 0, 0))
#expect(info.swiftlangVersion > Version(0, 0, 0))
#expect(!info.swiftTag.isEmpty)
#expect(info.swiftABIVersion == nil)
#if canImport(Darwin)
#expect(info.clangVersion != nil)
#endif
if let clangVersion = info.clangVersion {
#expect(clangVersion > Version(0, 0, 0))
}
}

try await withSpec(SwiftCompilerSpec.self, .result(status: .exit(0), stdout: Data("Swift version 5.9-dev (LLVM fd31e7eab45779f, Swift 86e6bda88e47178)\n".utf8), stderr: Data())) { (info: DiscoveredSwiftCompilerToolSpecInfo) in
#expect(info.toolPath.basename == core.hostOperatingSystem.imageFormat.executableName(basename: "swiftc"))
#expect(info.swiftVersion == Version(5, 9))
#expect(info.swiftlangVersion == Version(5, 9, 999, 999))
#expect(info.clangVersion == Version(5, 9, 999, 999))
#expect(info.swiftTag == "LLVM fd31e7eab45779f, Swift 86e6bda88e47178")
}

try await withSpec(SwiftCompilerSpec.self, .result(status: .exit(0), stdout: Data("Swift version 5.9 (Swift 86e6bda88e47178 LLVM fd31e7eab45779f)\n".utf8), stderr: Data())) { (info: DiscoveredSwiftCompilerToolSpecInfo) in
#expect(info.toolPath.basename == core.hostOperatingSystem.imageFormat.executableName(basename: "swiftc"))
#expect(info.swiftVersion == Version(5, 9))
#expect(info.swiftTag == "Swift 86e6bda88e47178 LLVM fd31e7eab45779f")
}

try await withSpec(SwiftCompilerSpec.self, .result(status: .exit(0), stdout: Data("Swift version 6.2 (swift-6.2-DEVELOPMENT-SNAPSHOT-2025-05-15-a)\n".utf8), stderr: Data())) { (info: DiscoveredSwiftCompilerToolSpecInfo) in
#expect(info.toolPath.basename == core.hostOperatingSystem.imageFormat.executableName(basename: "swiftc"))
#expect(info.swiftVersion == Version(6, 2))
#expect(info.swiftTag == "swift-6.2-DEVELOPMENT-SNAPSHOT-2025-05-15-a")
}

try await withSpec(SwiftCompilerSpec.self, .result(status: .exit(0), stdout: Data("Apple Swift version 5.9 (swiftlang-5.9.0.106.53 clang-1500.0.13.6)\n".utf8), stderr: Data("swift-driver version: 1.80 ".utf8))) { (info: DiscoveredSwiftCompilerToolSpecInfo) in
#expect(info.toolPath.basename == core.hostOperatingSystem.imageFormat.executableName(basename: "swiftc"))
#expect(info.swiftVersion == Version(5, 9))
#expect(info.swiftlangVersion == Version(5, 9, 0, 106, 53))
#expect(info.clangVersion == Version(1500, 0, 13, 6))
#expect(info.swiftTag == "swiftlang-5.9.0.106.53 clang-1500.0.13.6")
}

try await withSpec(SwiftCompilerSpec.self, .result(status: .exit(0), stdout: Data("Swift version 5.10.1 (swift-5.10.1-RELEASE)\nTarget: aarch64-unknown-linux-gnu\n".utf8), stderr: Data())) { (info: DiscoveredSwiftCompilerToolSpecInfo) in
#expect(info.toolPath.basename == core.hostOperatingSystem.imageFormat.executableName(basename: "swiftc"))
#expect(info.swiftVersion == Version(5, 10, 1))
#expect(info.swiftlangVersion == Version(5, 10, 1))
#expect(info.clangVersion == nil)
#expect(info.swiftTag == "swift-5.10.1-RELEASE")
}
}

Expand Down
69 changes: 18 additions & 51 deletions Tests/SWBCoreTests/DocumentationCompilerSpecTests.swift
Original file line number Diff line number Diff line change
Expand Up @@ -19,80 +19,47 @@ import SWBMacro

@Suite fileprivate struct DocumentationCompilerSpecTests: CoreBasedTests {
// Tests that `DocumentationCompilerSpec.additionalSymbolGraphGenerationArgs` only returns
// flags that are compatible with the given Swift compiler version.
// flags that are compatible with the given context.
@Test(.requireSDKs(.macOS))
func additionalSymbolGraphGenerationArgs() async throws {
// Support for the `-symbol-graph-minimum-access-level` flag was first introduced in
// swiftlang-5.6.0.316.14 (rdar://79099869).
let swiftVersionsThatSupportMinimumAccessLevel = [
"5.6.0.316.14",
"5.6.0.318.15",
"6.0.0.123.10",
]

// Any version prior to swiftlang-5.6.0.316.14 does not have support for
// the `-symbol-graph-minimum-access-level` flag.
let swiftVersionsThatDoNotSupportMinimumAccessLevel = [
"5.5.0.123.10",
"5.6.0.0.0",
"5.6.0.113.6",
"5.6.0.315.13",
"5.6.0.316.13",
]

// Confirm that when requesting additional symbol graph generation args for
// swift versions that _do_ support minimum access level, we
// get that flag.
for version in swiftVersionsThatSupportMinimumAccessLevel {
let symbolGraphGenerationArgs = await DocumentationCompilerSpec.additionalSymbolGraphGenerationArgs(
try mockApplicationBuildContext(),
swiftCompilerInfo: try mockSwiftCompilerSpec(swiftVersion: "5.6", swiftLangVersion: version)
)

#expect(symbolGraphGenerationArgs == ["-symbol-graph-minimum-access-level", "internal"])
}

// Confirm that when requesting additional symbol graph generation args for
// swift versions that do _not_ support minimum access level, we
// do not get that flag.
for version in swiftVersionsThatDoNotSupportMinimumAccessLevel {
let symbolGraphGenerationArgs = await DocumentationCompilerSpec.additionalSymbolGraphGenerationArgs(
try mockApplicationBuildContext(),
swiftCompilerInfo: try mockSwiftCompilerSpec(swiftVersion: "5.6", swiftLangVersion: version)
)
let applicationArgs = await DocumentationCompilerSpec.additionalSymbolGraphGenerationArgs(
try mockApplicationBuildContext(application: true),
swiftCompilerInfo: try mockSwiftCompilerSpec(swiftVersion: "5.6", swiftTag: "swiftlang-5.6.0.0")
)
#expect(applicationArgs == ["-symbol-graph-minimum-access-level", "internal"])

#expect(symbolGraphGenerationArgs.isEmpty,
"""
'swiftlang-\(version)' does not support the minimum-access-level flag that \
was introduced in 'swiftlang-5.6.0.316.14'.
""")
}
let frameworkArgs = await DocumentationCompilerSpec.additionalSymbolGraphGenerationArgs(
try mockApplicationBuildContext(application: false),
swiftCompilerInfo: try mockSwiftCompilerSpec(swiftVersion: "5.6", swiftTag: "swiftlang-5.6.0.0")
)
#expect(frameworkArgs == [])
}

private func mockApplicationBuildContext() async throws -> CommandBuildContext {
private func mockApplicationBuildContext(application: Bool) async throws -> CommandBuildContext {
let core = try await getCore()

let producer = try MockCommandProducer(
core: core,
productTypeIdentifier: "com.apple.product-type.application",
productTypeIdentifier: application ? "com.apple.product-type.application" : "com.apple.product-type.framework",
platform: "macosx"
)

var mockTable = MacroValueAssignmentTable(namespace: core.specRegistry.internalMacroNamespace)
mockTable.push(BuiltinMacros.MACH_O_TYPE, literal: "mh_execute")
if application {
mockTable.push(BuiltinMacros.MACH_O_TYPE, literal: "mh_execute")
}

let mockScope = MacroEvaluationScope(table: mockTable)

return CommandBuildContext(producer: producer, scope: mockScope, inputs: [])
}

private func mockSwiftCompilerSpec(swiftVersion: String, swiftLangVersion: String) throws -> DiscoveredSwiftCompilerToolSpecInfo {
private func mockSwiftCompilerSpec(swiftVersion: String, swiftTag: String) throws -> DiscoveredSwiftCompilerToolSpecInfo {
return DiscoveredSwiftCompilerToolSpecInfo(
toolPath: .root,
swiftVersion: try Version(swiftVersion),
swiftlangVersion: try Version(swiftLangVersion),
swiftTag: swiftTag,
swiftABIVersion: nil,
clangVersion: nil,
blocklists: SwiftBlocklists(),
toolFeatures: ToolFeatures([])
)
Expand Down
Loading