Skip to content

Allow setting LINKER_DRIVER=auto to choose an appropriate value for the target sources #622

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
Jun 30, 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
9 changes: 8 additions & 1 deletion Sources/SWBCore/PlannedTaskAction.swift
Original file line number Diff line number Diff line change
Expand Up @@ -264,7 +264,14 @@ public struct FileCopyTaskActionContext {
extension FileCopyTaskActionContext {
public init(_ cbc: CommandBuildContext) {
let compilerPath = cbc.producer.clangSpec.resolveExecutablePath(cbc, forLanguageOfFileType: cbc.producer.lookupFileType(languageDialect: .c))
let linkerPath = cbc.producer.ldLinkerSpec.resolveExecutablePath(cbc.producer, Path(cbc.producer.ldLinkerSpec.computeExecutablePath(cbc)))
let linkerPath = cbc.producer.ldLinkerSpec.resolveExecutablePath(cbc.producer, cbc.producer.ldLinkerSpec.computeLinkerPath(cbc, usedCXX: false, lookup: { macro in
switch macro {
case BuiltinMacros.LINKER_DRIVER:
return cbc.scope.namespace.parseString("clang")
default:
return nil
}
}))
let lipoPath = cbc.producer.lipoSpec.resolveExecutablePath(cbc.producer, Path(cbc.producer.lipoSpec.computeExecutablePath(cbc)))

// If we couldn't find clang, skip the special stub binary handling. We may be using an Open Source toolchain which only has Swift. Also skip it for installLoc builds.
Expand Down
1 change: 1 addition & 0 deletions Sources/SWBCore/Settings/BuiltinMacros.swift
Original file line number Diff line number Diff line change
Expand Up @@ -2701,6 +2701,7 @@ public enum LinkerDriverChoice: String, Equatable, Hashable, EnumerationMacroTyp

case clang
case swiftc
case auto
}

/// Enumeration macro type for the value of the `INFOPLIST_KEY_LSApplicationCategoryType` build setting.
Expand Down
24 changes: 14 additions & 10 deletions Sources/SWBCore/SpecImplementations/ProductTypes.swift
Original file line number Diff line number Diff line change
Expand Up @@ -260,31 +260,35 @@ public class ProductTypeSpec : Spec, SpecType, @unchecked Sendable {
}

/// Computes and returns additional arguments to pass to the linker appropriate for the product type. Also returns a list of additional paths to treat as inputs to the link command, if appropriate.
func computeAdditionalLinkerArgs(_ producer: any CommandProducer, scope: MacroEvaluationScope) -> (args: [String], inputs: [Path]) {
func computeAdditionalLinkerArgs(_ producer: any CommandProducer, scope: MacroEvaluationScope, lookup: @escaping ((MacroDeclaration) -> MacroStringExpression?)) -> (args: [String], inputs: [Path]) {
return ([], [])
}

fileprivate func computeDylibArgs(_ producer: any CommandProducer, _ scope: MacroEvaluationScope) -> [String] {
fileprivate func computeDylibArgs(_ producer: any CommandProducer, _ scope: MacroEvaluationScope, lookup: @escaping ((MacroDeclaration) -> MacroStringExpression?)) -> [String] {
var args = [String]()

if producer.isApplePlatform {
let compatibilityVersion = scope.evaluate(BuiltinMacros.DYLIB_COMPATIBILITY_VERSION)
let compatibilityVersion = scope.evaluate(BuiltinMacros.DYLIB_COMPATIBILITY_VERSION, lookup: lookup)
if !compatibilityVersion.isEmpty {
switch scope.evaluate(BuiltinMacros.LINKER_DRIVER) {
switch scope.evaluate(BuiltinMacros.LINKER_DRIVER, lookup: lookup) {
case .clang:
args += ["-compatibility_version", compatibilityVersion]
case .swiftc:
args += ["-Xlinker", "-compatibility_version", "-Xlinker", compatibilityVersion]
case .auto:
preconditionFailure("Expected LINKER_DRIVER to be bound to a concrete value")
}
}

let currentVersion = scope.evaluate(BuiltinMacros.DYLIB_CURRENT_VERSION)
let currentVersion = scope.evaluate(BuiltinMacros.DYLIB_CURRENT_VERSION, lookup: lookup)
if !currentVersion.isEmpty {
switch scope.evaluate(BuiltinMacros.LINKER_DRIVER) {
switch scope.evaluate(BuiltinMacros.LINKER_DRIVER, lookup: lookup) {
case .clang:
args += ["-current_version", currentVersion]
case .swiftc:
args += ["-Xlinker", "-current_version", "-Xlinker", currentVersion]
case .auto:
preconditionFailure("Expected LINKER_DRIVER to be bound to a concrete value")
}
}
}
Expand Down Expand Up @@ -563,9 +567,9 @@ public class FrameworkProductTypeSpec : BundleProductTypeSpec, @unchecked Sendab
])
*/

override func computeAdditionalLinkerArgs(_ producer: any CommandProducer, scope: MacroEvaluationScope) -> (args: [String], inputs: [Path]) {
override func computeAdditionalLinkerArgs(_ producer: any CommandProducer, scope: MacroEvaluationScope, lookup: @escaping ((MacroDeclaration) -> MacroStringExpression?)) -> (args: [String], inputs: [Path]) {
if scope.evaluate(BuiltinMacros.MACH_O_TYPE) != "staticlib" {
return (computeDylibArgs(producer, scope), [])
return (computeDylibArgs(producer, scope, lookup: lookup), [])
}
return ([], [])
}
Expand Down Expand Up @@ -801,9 +805,9 @@ public final class DynamicLibraryProductTypeSpec : LibraryProductTypeSpec, @unch
return true
}

override func computeAdditionalLinkerArgs(_ producer: any CommandProducer, scope: MacroEvaluationScope) -> (args: [String], inputs: [Path]) {
override func computeAdditionalLinkerArgs(_ producer: any CommandProducer, scope: MacroEvaluationScope, lookup: @escaping ((MacroDeclaration) -> MacroStringExpression?)) -> (args: [String], inputs: [Path]) {
if scope.evaluate(BuiltinMacros.MACH_O_TYPE) != "staticlib" {
return (computeDylibArgs(producer, scope), [])
return (computeDylibArgs(producer, scope, lookup: lookup), [])
}
return ([], [])
}
Expand Down
102 changes: 73 additions & 29 deletions Sources/SWBCore/SpecImplementations/Tools/LinkerTools.swift
Original file line number Diff line number Diff line change
Expand Up @@ -233,16 +233,6 @@ public struct DiscoveredLdLinkerToolSpecInfo: DiscoveredCommandLineToolSpecInfo
public final class LdLinkerSpec : GenericLinkerSpec, SpecIdentifierType, @unchecked Sendable {
public static let identifier = "com.apple.pbx.linkers.ld"

public override func computeExecutablePath(_ cbc: CommandBuildContext) -> String {
// TODO: We should also provide an "auto" option which chooses based on the source files in the target
switch cbc.scope.evaluate(BuiltinMacros.LINKER_DRIVER) {
case .clang:
return cbc.producer.hostOperatingSystem.imageFormat.executableName(basename: "clang")
case .swiftc:
return cbc.producer.hostOperatingSystem.imageFormat.executableName(basename: "swiftc")
}
}

override public var toolBasenameAliases: [String] {
// We use clang as our linker, so return ld and libtool in aliases in
// order to parse the errors from the actual linker.
Expand Down Expand Up @@ -281,7 +271,7 @@ public final class LdLinkerSpec : GenericLinkerSpec, SpecIdentifierType, @unchec
}

// FIXME: Is there a better way to figure out if we are linking Swift?
private func isUsingSwift(_ usedTools: [CommandLineToolSpec: Set<FileTypeSpec>]) -> Bool {
private static func isUsingSwift(_ usedTools: [CommandLineToolSpec: Set<FileTypeSpec>]) -> Bool {
return usedTools.keys.map({ type(of: $0) }).contains(where: { $0 == SwiftCompilerSpec.self })
}

Expand All @@ -304,10 +294,35 @@ public final class LdLinkerSpec : GenericLinkerSpec, SpecIdentifierType, @unchec
return runpathSearchPaths
}

static func resolveLinkerDriver(_ cbc: CommandBuildContext, usedTools: [CommandLineToolSpec: Set<FileTypeSpec>]) -> LinkerDriverChoice {
switch cbc.scope.evaluate(BuiltinMacros.LINKER_DRIVER) {
case .clang:
return .clang
case .swiftc:
return.swiftc
case .auto:
if Self.isUsingSwift(usedTools) {
return .swiftc
} else {
return .clang
}
}
}

override public func constructLinkerTasks(_ cbc: CommandBuildContext, _ delegate: any TaskGenerationDelegate, libraries: [LibrarySpecifier], usedTools: [CommandLineToolSpec: Set<FileTypeSpec>]) async {
let resolvedLinkerDriver = Self.resolveLinkerDriver(cbc, usedTools: usedTools)
let linkerDriverLookup: ((MacroDeclaration) -> MacroStringExpression?) = { macro in
switch macro {
case BuiltinMacros.LINKER_DRIVER:
return cbc.scope.namespace.parseString(resolvedLinkerDriver.rawValue)
default:
return nil
}
}

// Validate that OTHER_LDFLAGS doesn't contain flags for constructs which we have dedicated settings for. This should be expanded over time.
let dyldEnvDiagnosticBehavior: Diagnostic.Behavior = SWBFeatureFlag.useStrictLdEnvironmentBuildSetting.value ? .error : .warning
let originalLdFlags = cbc.scope.evaluate(BuiltinMacros.OTHER_LDFLAGS)
let originalLdFlags = cbc.scope.evaluate(BuiltinMacros.OTHER_LDFLAGS, lookup: linkerDriverLookup)
enumerateLinkerCommandLine(arguments: originalLdFlags) { arg, value in
switch arg {
case "-dyld_env":
Expand Down Expand Up @@ -354,7 +369,7 @@ public final class LdLinkerSpec : GenericLinkerSpec, SpecIdentifierType, @unchec
specialArgs.append(contentsOf: sparseSDKSearchPathArguments(cbc))

// Define the linker file list.
let fileListPath = cbc.scope.evaluate(BuiltinMacros.__INPUT_FILE_LIST_PATH__)
let fileListPath = cbc.scope.evaluate(BuiltinMacros.__INPUT_FILE_LIST_PATH__, lookup: linkerDriverLookup)
if !fileListPath.isEmpty {
let contents = OutputByteStream()
for input in cbc.inputs {
Expand Down Expand Up @@ -385,7 +400,7 @@ public final class LdLinkerSpec : GenericLinkerSpec, SpecIdentifierType, @unchec
}

// Add linker flags desired by the product type.
let productTypeArgs = cbc.producer.productType?.computeAdditionalLinkerArgs(cbc.producer, scope: cbc.scope)
let productTypeArgs = cbc.producer.productType?.computeAdditionalLinkerArgs(cbc.producer, scope: cbc.scope, lookup: linkerDriverLookup)
specialArgs += productTypeArgs?.args ?? []
inputPaths += productTypeArgs?.inputs ?? []

Expand Down Expand Up @@ -425,7 +440,7 @@ public final class LdLinkerSpec : GenericLinkerSpec, SpecIdentifierType, @unchec
inputPaths.append(contentsOf: inputs)
}

let isLinkUsingSwift = isUsingSwift(usedTools)
let isLinkUsingSwift = Self.isUsingSwift(usedTools)
if !isLinkUsingSwift {
// Check if we need to link with Swift's standard library
// when linking a pure Objective-C/C++ target. This might be needed
Expand Down Expand Up @@ -483,6 +498,9 @@ public final class LdLinkerSpec : GenericLinkerSpec, SpecIdentifierType, @unchec
let frameworkSearchPathsExpr = cbc.scope.namespace.parseStringList(frameworkSearchPaths)

func lookup(_ macro: MacroDeclaration) -> MacroExpression? {
if let result = linkerDriverLookup(macro) {
return result
}
switch macro {
case BuiltinMacros.LD_RUNPATH_SEARCH_PATHS:
return runpathSearchPathsExpr
Expand Down Expand Up @@ -589,7 +607,7 @@ public final class LdLinkerSpec : GenericLinkerSpec, SpecIdentifierType, @unchec

// Select the driver to use based on the input file types, replacing the value computed by commandLineFromTemplate().
let usedCXX = usedTools.values.contains(where: { $0.contains(where: { $0.languageDialect?.isPlusPlus ?? false }) })
commandLine[0] = await resolveExecutablePath(cbc, computeLinkerPath(cbc, usedCXX: usedCXX), delegate: delegate).str
commandLine[0] = await resolveExecutablePath(cbc, computeLinkerPath(cbc, usedCXX: usedCXX, lookup: linkerDriverLookup), delegate: delegate).str

let entitlementsSection = cbc.scope.evaluate(BuiltinMacros.LD_ENTITLEMENTS_SECTION)
if !entitlementsSection.isEmpty {
Expand Down Expand Up @@ -763,6 +781,15 @@ public final class LdLinkerSpec : GenericLinkerSpec, SpecIdentifierType, @unchec
}

public func constructPreviewShimLinkerTasks(_ cbc: CommandBuildContext, _ delegate: any TaskGenerationDelegate, libraries: [LibrarySpecifier], usedTools: [CommandLineToolSpec: Set<FileTypeSpec>], rpaths: [String], ldflags: [String]?) async {
let resolvedLinkerDriver = Self.resolveLinkerDriver(cbc, usedTools: usedTools)
let linkerDriverLookup: ((MacroDeclaration) -> MacroStringExpression?) = { macro in
switch macro {
case BuiltinMacros.LINKER_DRIVER:
return cbc.scope.namespace.parseString(resolvedLinkerDriver.rawValue)
default:
return nil
}
}
// Construct the "special args".
var specialArgs = [String]()
var inputPaths = cbc.inputs.map({ $0.absolutePath })
Expand All @@ -782,6 +809,9 @@ public final class LdLinkerSpec : GenericLinkerSpec, SpecIdentifierType, @unchec
}

func lookup(_ macro: MacroDeclaration) -> MacroExpression? {
if let result = linkerDriverLookup(macro) {
return result
}
switch macro {
case BuiltinMacros.LD_ENTRY_POINT where cbc.scope.previewStyle == .xojit:
return cbc.scope.namespace.parseLiteralString("___debug_blank_executor_main")
Expand Down Expand Up @@ -835,7 +865,7 @@ public final class LdLinkerSpec : GenericLinkerSpec, SpecIdentifierType, @unchec

// Select the driver to use based on the input file types, replacing the value computed by commandLineFromTemplate().
let usedCXX = usedTools.values.contains(where: { $0.contains(where: { $0.languageDialect?.isPlusPlus ?? false }) })
commandLine[0] = await resolveExecutablePath(cbc, computeLinkerPath(cbc, usedCXX: usedCXX), delegate: delegate).str
commandLine[0] = await resolveExecutablePath(cbc, computeLinkerPath(cbc, usedCXX: usedCXX, lookup: linkerDriverLookup), delegate: delegate).str

let entitlementsSection = cbc.scope.evaluate(BuiltinMacros.LD_ENTITLEMENTS_SECTION)
if !entitlementsSection.isEmpty {
Expand Down Expand Up @@ -1105,31 +1135,45 @@ public final class LdLinkerSpec : GenericLinkerSpec, SpecIdentifierType, @unchec
]
}

private func computeLinkerPath(_ cbc: CommandBuildContext, usedCXX: Bool) -> Path {
public override func computeExecutablePath(_ cbc: CommandBuildContext) -> String {
// Placeholder fallback
return cbc.producer.hostOperatingSystem.imageFormat.executableName(basename: "clang")
}

public func computeLinkerPath(_ cbc: CommandBuildContext, usedCXX: Bool, lookup: @escaping ((MacroDeclaration) -> MacroStringExpression?)) -> Path {
if usedCXX {
let perArchValue = cbc.scope.evaluate(BuiltinMacros.PER_ARCH_LDPLUSPLUS)
let perArchValue = cbc.scope.evaluate(BuiltinMacros.PER_ARCH_LDPLUSPLUS, lookup: lookup)
if !perArchValue.isEmpty {
return Path(perArchValue)
return Path(cbc.producer.hostOperatingSystem.imageFormat.executableName(basename: perArchValue))
}

let value = cbc.scope.evaluate(BuiltinMacros.LDPLUSPLUS)
let value = cbc.scope.evaluate(BuiltinMacros.LDPLUSPLUS, lookup: lookup)
if !value.isEmpty {
return Path(value)
return Path(cbc.producer.hostOperatingSystem.imageFormat.executableName(basename: value))
}

return Path("clang++")
} else {
let perArchValue = cbc.scope.evaluate(BuiltinMacros.PER_ARCH_LD)
let perArchValue = cbc.scope.evaluate(BuiltinMacros.PER_ARCH_LD, lookup: lookup)
if !perArchValue.isEmpty {
return Path(perArchValue)
return Path(cbc.producer.hostOperatingSystem.imageFormat.executableName(basename: perArchValue))
}

let value = cbc.scope.evaluate(BuiltinMacros.LD)
let value = cbc.scope.evaluate(BuiltinMacros.LD, lookup: lookup)
if !value.isEmpty {
return Path(value)
return Path(cbc.producer.hostOperatingSystem.imageFormat.executableName(basename: value))
}
}

return Path(computeExecutablePath(cbc))
switch cbc.scope.evaluate(BuiltinMacros.LINKER_DRIVER, lookup: lookup) {
case .clang:
if usedCXX {
return Path(cbc.producer.hostOperatingSystem.imageFormat.executableName(basename: "clang++"))
} else {
return Path(cbc.producer.hostOperatingSystem.imageFormat.executableName(basename: "clang"))
}
case .swiftc:
return Path(cbc.producer.hostOperatingSystem.imageFormat.executableName(basename: "swiftc"))
case .auto:
preconditionFailure("LINKER_DRIVER was expected to be bound to a concrete value")
}
}

Expand Down
6 changes: 3 additions & 3 deletions Tests/SWBCoreTests/CommandLineSpecTests.swift
Original file line number Diff line number Diff line change
Expand Up @@ -1560,23 +1560,23 @@ import SWBMacro
}

// Check with just LD.
for (name, expected) in [("file.c", "SomeCLinker"), ("file.cpp", "clang++")] {
for (name, expected) in [("file.c", core.hostOperatingSystem.imageFormat.executableName(basename:"SomeCLinker")), ("file.cpp", core.hostOperatingSystem.imageFormat.executableName(basename:"clang++"))] {
try await check(name: name, expectedLinker: expected, macros: [
BuiltinMacros.LD: "SomeCLinker"
// NOTE: One wonders whether this shouldn't change the C++ linker.
])
}

// Check with LD & LDPLUSPLUS.
for (name, expected) in [("file.c", "SomeCLinker"), ("file.cpp", "SomeC++Linker")] {
for (name, expected) in [("file.c", core.hostOperatingSystem.imageFormat.executableName(basename:"SomeCLinker")), ("file.cpp", core.hostOperatingSystem.imageFormat.executableName(basename:"SomeC++Linker"))] {
try await check(name: name, expectedLinker: expected, macros: [
BuiltinMacros.LD: "SomeCLinker",
BuiltinMacros.LDPLUSPLUS: "SomeC++Linker"
])
}

// Check with arch specific LD.
for (name, expected) in [("file.c", "SomeCLinker_x86_64"), ("file.cpp", "SomeC++Linker_x86_64")] {
for (name, expected) in [("file.c", core.hostOperatingSystem.imageFormat.executableName(basename:"SomeCLinker_x86_64")), ("file.cpp", core.hostOperatingSystem.imageFormat.executableName(basename:"SomeC++Linker_x86_64"))] {
try await check(name: name, expectedLinker: expected, macros: [
BuiltinMacros.CURRENT_ARCH: "x86_64",
try core.specRegistry.internalMacroNamespace.declareStringMacro("LD_x86_64"): "SomeCLinker_x86_64",
Expand Down
Loading
Loading