Skip to content
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
4 changes: 2 additions & 2 deletions Sources/SWBBuildSystem/DependencyCycleFormatter.swift
Original file line number Diff line number Diff line change
Expand Up @@ -379,7 +379,7 @@ struct DependencyCycleFormatter {
message = "Target '\(previousTargetName)' has an explicit dependency on Target '\(targetName)'"
case let .implicitBuildPhaseLinkage(filename, _, buildPhase)?:
message = "Target '\(previousTargetName)' has an implicit dependency on Target '\(targetName)' because '\(previousTargetName)' references the file '\(filename)' in the build phase '\(buildPhase)'"
case let .implicitBuildSettingLinkage(settingName, options)?:
case let .implicitBuildSetting(settingName, options)?:
message = "Target '\(previousTargetName)' has an implicit dependency on Target '\(targetName)' because '\(previousTargetName)' defines the option '\(options.joined(separator: " "))' in the build setting '\(settingName)'"
case let .impliedByTransitiveDependencyViaRemovedTargets(intermediateTargetName: intermediateTargetName):
message = "Target '\(previousTargetName)' has a dependency on Target '\(targetName)' via its transitive dependency through '\(intermediateTargetName)'"
Expand Down Expand Up @@ -501,7 +501,7 @@ struct DependencyCycleFormatter {
suffix = " via the “Target Dependencies“ build phase"
case let .implicitBuildPhaseLinkage(filename, _, buildPhase)?:
suffix = " because the scheme has implicit dependencies enabled and the Target '\(lastTargetsName)' references the file '\(filename)' in the build phase '\(buildPhase)'"
case let .implicitBuildSettingLinkage(settingName, options)?:
case let .implicitBuildSetting(settingName, options)?:
suffix = " because the scheme has implicit dependencies enabled and the Target '\(lastTargetsName)' defines the options '\(options.joined(separator: " "))' in the build setting '\(settingName)'"
case let .impliedByTransitiveDependencyViaRemovedTargets(intermediateTargetName: intermediateTargetName):
suffix = " via its transitive dependency through '\(intermediateTargetName)'"
Expand Down
56 changes: 54 additions & 2 deletions Sources/SWBCore/LinkageDependencyResolver.swift
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@

public import SWBUtil
import SWBMacro
internal import Foundation

/// A completely resolved graph of configured targets for use in a build.
public struct TargetLinkageGraph: TargetGraph {
Expand Down Expand Up @@ -79,11 +80,15 @@ actor LinkageDependencyResolver {
/// Sets of targets mapped by product name stem.
private let targetsByProductNameStem: [String: Set<StandardTarget>]

/// Sets of targets mapped by module name (computed using parameters from the build request).
private let targetsByUnconfiguredModuleName: [String: Set<StandardTarget>]

internal let resolver: DependencyResolver

init(workspaceContext: WorkspaceContext, buildRequest: BuildRequest, buildRequestContext: BuildRequestContext, delegate: any TargetDependencyResolverDelegate) {
var targetsByProductName = [String: Set<StandardTarget>]()
var targetsByProductNameStem = [String: Set<StandardTarget>]()
var targetsByUnconfiguredModuleName = [String: Set<StandardTarget>]()
for case let target as StandardTarget in workspaceContext.workspace.allTargets {
// FIXME: We are relying on the product reference name being constant here. This is currently true, given how our path resolver works, but it is possible to construct an Xcode project for which this doesn't work (Xcode doesn't, however, handle that situation very well). We should resolve this: <rdar://problem/29410050> Swift Build doesn't support product references with non-constant basenames

Expand All @@ -95,11 +100,17 @@ actor LinkageDependencyResolver {
if let stem = Path(productName).stem, stem != productName {
targetsByProductNameStem[stem, default: []].insert(target)
}

let moduleName = buildRequestContext.getCachedSettings(buildRequest.parameters, target: target).globalScope.evaluate(BuiltinMacros.PRODUCT_MODULE_NAME)
if !moduleName.isEmpty {
targetsByUnconfiguredModuleName[moduleName, default: []].insert(target)
}
}

// Remember the mappings we created.
self.targetsByProductName = targetsByProductName
self.targetsByProductNameStem = targetsByProductNameStem
self.targetsByUnconfiguredModuleName = targetsByUnconfiguredModuleName

resolver = DependencyResolver(workspaceContext: workspaceContext, buildRequest: buildRequest, buildRequestContext: buildRequestContext, delegate: delegate)
}
Expand Down Expand Up @@ -333,7 +344,7 @@ actor LinkageDependencyResolver {

// Skip this flag if its corresponding product name is the same as the product of one of our explicit dependencies. This effectively matches the flag to an explicit dependency.
if !productNamesOfExplicitDependencies.contains(productName), let implicitDependency = await implicitDependency(forProductName: productName, from: configuredTarget, imposedParameters: imposedParameters, source: .frameworkLinkerFlag(flag: flag, frameworkName: stem, buildSetting: macro)) {
await result.append(ResolvedTargetDependency(target: implicitDependency, reason: .implicitBuildSettingLinkage(settingName: macro.name, options: [flag, stem])))
await result.append(ResolvedTargetDependency(target: implicitDependency, reason: .implicitBuildSetting(settingName: macro.name, options: [flag, stem])))
return
}
} addLibrary: { macro, prefix, stem in
Expand All @@ -349,7 +360,7 @@ actor LinkageDependencyResolver {
if productNamesOfExplicitDependencies.intersection(productNames).isEmpty {
for productName in productNames {
if let implicitDependency = await implicitDependency(forProductName: productName, from: configuredTarget, imposedParameters: imposedParameters, source: .libraryLinkerFlag(flag: prefix, libraryName: stem, buildSetting: macro)) {
await result.append(ResolvedTargetDependency(target: implicitDependency, reason: .implicitBuildSettingLinkage(settingName: macro.name, options: ["\(prefix)\(stem)"])))
await result.append(ResolvedTargetDependency(target: implicitDependency, reason: .implicitBuildSetting(settingName: macro.name, options: ["\(prefix)\(stem)"])))
// We only match one.
return
}
Expand All @@ -360,6 +371,16 @@ actor LinkageDependencyResolver {
}
}

let moduleNamesOfExplicitDependencies = Set<String>(immediateDependencies.compactMap{
buildRequestContext.getCachedSettings($0.parameters, target: $0.target).globalScope.evaluate(BuiltinMacros.PRODUCT_MODULE_NAME)
})

for moduleDependencyName in configuredTargetSettings.moduleDependencies.map { $0.name } {
if !moduleNamesOfExplicitDependencies.contains(moduleDependencyName), let implicitDependency = await implicitDependency(forModuleName: moduleDependencyName, from: configuredTarget, imposedParameters: imposedParameters, source: .moduleDependency(name: moduleDependencyName, buildSetting: BuiltinMacros.MODULE_DEPENDENCIES)) {
await result.append(ResolvedTargetDependency(target: implicitDependency, reason: .implicitBuildSetting(settingName: BuiltinMacros.MODULE_DEPENDENCIES.name, options: [moduleDependencyName])))
}
}

return await result.value
}

Expand Down Expand Up @@ -444,6 +465,30 @@ actor LinkageDependencyResolver {
return resolver.lookupConfiguredTarget(candidateDependencyTarget, parameters: candidateParameters, imposedParameters: effectiveImposedParameters)
}

private func implicitDependency(forModuleName moduleName: String, from configuredTarget: ConfiguredTarget, imposedParameters: SpecializationParameters?, source: ImplicitDependencySource) async -> ConfiguredTarget? {
let candidateConfiguredTargets = await (targetsByUnconfiguredModuleName[moduleName] ?? []).asyncMap { [self] candidateTarget -> ConfiguredTarget? in
// Prefer overriding build parameters from the build request, if present.
let buildParameters = resolver.buildParametersByTarget[candidateTarget] ?? configuredTarget.parameters

// Validate the module name using concrete parameters.
let configuredModuleName = buildRequestContext.getCachedSettings(buildParameters, target: candidateTarget).globalScope.evaluate(BuiltinMacros.PRODUCT_MODULE_NAME)
if configuredModuleName != moduleName {
return nil
}

// Get a configured target for this target, and use it as the implicit dependency.
if let candidateConfiguredTarget = await implicitDependency(candidate: candidateTarget, parameters: buildParameters, isValidFor: configuredTarget, imposedParameters: imposedParameters, resolver: resolver) {
return candidateConfiguredTarget
}

return nil
}.compactMap { $0 }.sorted()

emitAmbiguousImplicitDependencyWarningIfNeeded(for: configuredTarget, dependencies: candidateConfiguredTargets, from: source)

return candidateConfiguredTargets.first
}

/// Search for an implicit dependency by full product name.
nonisolated private func implicitDependency(forProductName productName: String, from configuredTarget: ConfiguredTarget, imposedParameters: SpecializationParameters?, source: ImplicitDependencySource) async -> ConfiguredTarget? {
let candidateConfiguredTargets = await (targetsByProductName[productName] ?? []).asyncMap { [self] candidateTarget -> ConfiguredTarget? in
Expand Down Expand Up @@ -506,6 +551,9 @@ actor LinkageDependencyResolver {
/// The dependency's product name matched the basename of a build file in the target's build phases.
case productNameStem(_ stem: String, buildFile: BuildFile, buildPhase: BuildPhase)

/// The dependency's module name matched a declared module dependency of the client target.
case moduleDependency(name: String, buildSetting: MacroDeclaration)

var valueForDisplay: String {
switch self {
case let .frameworkLinkerFlag(flag, frameworkName, _):
Expand All @@ -516,6 +564,8 @@ actor LinkageDependencyResolver {
return "product reference '\(productName)'"
case let .productNameStem(stem, _, _):
return "product bundle executable reference '\(stem)'"
case let .moduleDependency(name, _):
return "module dependency \(name)"
}
}
}
Expand All @@ -530,6 +580,8 @@ actor LinkageDependencyResolver {
case let .productReference(_, buildFile, buildPhase),
let .productNameStem(_, buildFile, buildPhase):
location = .buildFile(buildFileGUID: buildFile.guid, buildPhaseGUID: buildPhase.guid, targetGUID: configuredTarget.target.guid)
case let .moduleDependency(_, buildSetting):
location = .buildSettings([buildSetting])
}

delegate.emit(.overrideTarget(configuredTarget), SWBUtil.Diagnostic(behavior: .warning, location: location, data: DiagnosticData("Multiple targets match implicit dependency for \(source.valueForDisplay). Consider adding an explicit dependency on the intended target to resolve this ambiguity.", component: .targetIntegrity), childDiagnostics: candidateConfiguredTargets.map({ dependency -> Diagnostic in
Expand Down
2 changes: 2 additions & 0 deletions Sources/SWBCore/Settings/BuiltinMacros.swift
Original file line number Diff line number Diff line change
Expand Up @@ -865,6 +865,7 @@ public final class BuiltinMacros {
public static let MODULEMAP_PATH = BuiltinMacros.declareStringMacro("MODULEMAP_PATH")
public static let MODULEMAP_PRIVATE_FILE = BuiltinMacros.declareStringMacro("MODULEMAP_PRIVATE_FILE")
public static let MODULES_FOLDER_PATH = BuiltinMacros.declarePathMacro("MODULES_FOLDER_PATH")
public static let MODULE_DEPENDENCIES = BuiltinMacros.declareStringListMacro("MODULE_DEPENDENCIES")
public static let MODULE_VERIFIER_KIND = BuiltinMacros.declareEnumMacro("MODULE_VERIFIER_KIND") as EnumMacroDeclaration<ModuleVerifierKind>
public static let MODULE_VERIFIER_LSV = BuiltinMacros.declareBooleanMacro("MODULE_VERIFIER_LSV")
public static let MODULE_VERIFIER_SUPPORTED_LANGUAGES = BuiltinMacros.declareStringListMacro("MODULE_VERIFIER_SUPPORTED_LANGUAGES")
Expand Down Expand Up @@ -1944,6 +1945,7 @@ public final class BuiltinMacros {
MODULEMAP_PRIVATE_FILE,
MODULES_FOLDER_PATH,
MODULE_CACHE_DIR,
MODULE_DEPENDENCIES,
MODULE_NAME,
MODULE_START,
MODULE_STOP,
Expand Down
20 changes: 20 additions & 0 deletions Sources/SWBCore/Settings/Settings.swift
Original file line number Diff line number Diff line change
Expand Up @@ -5335,3 +5335,23 @@ extension MacroEvaluationScope {
}
}
}

extension Settings {
public struct ModuleDependencyInfo {
let name: String
let isPublic: Bool
}

public var moduleDependencies: [ModuleDependencyInfo] {
self.globalScope.evaluate(BuiltinMacros.MODULE_DEPENDENCIES).compactMap {
let components = $0.components(separatedBy: " ")
guard let name = components.last else {
return nil
}
return ModuleDependencyInfo(
name: name,
isPublic: components.count > 1 && components.first == "public"
)
}
}
}
10 changes: 10 additions & 0 deletions Sources/SWBCore/Specs/CoreBuildSystem.xcspec
Original file line number Diff line number Diff line change
Expand Up @@ -1597,6 +1597,16 @@ When `GENERATE_INFOPLIST_FILE` is enabled, sets the value of the [CFBundleIdenti
sdk,
);
},
{
Name = "MODULE_DEPENDENCIES";
Type = StringList;
Category = BuildOptions;
DefaultValue = "";
ConditionFlavors = (
arch,
sdk,
);
},
{
Name = "GENERATE_PRELINK_OBJECT_FILE";
Type = Boolean;
Expand Down
3 changes: 3 additions & 0 deletions Sources/SWBCore/Specs/en.lproj/CoreBuildSystem.strings
Original file line number Diff line number Diff line change
Expand Up @@ -397,6 +397,9 @@ Generally you should not specify an order file in Debug or Development configura
"[OTHER_LDFLAGS]-name" = "Other Linker Flags";
"[OTHER_LDFLAGS]-description" = "Options defined in this setting are passed to invocations of the linker.";

"[MODULE_DEPENDENCIES]-name" = "Module Dependencies";
"[MODULE_DEPENDENCIES]-description" = "Other modules this target depends on.";

"[OTHER_LIBTOOLFLAGS]-name" = "Other Librarian Flags";
"[OTHER_LIBTOOLFLAGS]-description" = "Options defined in this setting are passed to all invocations of the archive librarian, which is used to generate static libraries.";

Expand Down
4 changes: 2 additions & 2 deletions Sources/SWBCore/TargetDependencyResolver.swift
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,7 @@ public enum TargetDependencyReason: Sendable {
/// - parameter buildPhase: The name of the build phase used to find this linkage. This is used for diagnostics.
case implicitBuildPhaseLinkage(filename: String, buildableItem: BuildFile.BuildableItem, buildPhase: String)
/// The upstream target has an implicit dependency on the target due to options being passed via a build setting.
case implicitBuildSettingLinkage(settingName: String, options: [String])
case implicitBuildSetting(settingName: String, options: [String])
/// The upstream target has a transitive dependency on the target via target(s) which were removed from the build graph.
case impliedByTransitiveDependencyViaRemovedTargets(intermediateTargetName: String)
}
Expand Down Expand Up @@ -213,7 +213,7 @@ public struct TargetBuildGraph: TargetGraph, Sendable {
dependencyString = "Explicit dependency on \(dependencyDescription)"
case .implicitBuildPhaseLinkage(filename: let filename, buildableItem: _, buildPhase: let buildPhase):
dependencyString = "Implicit dependency on \(dependencyDescription) via file '\(filename)' in build phase '\(buildPhase)'"
case .implicitBuildSettingLinkage(settingName: let settingName, options: let options):
case .implicitBuildSetting(settingName: let settingName, options: let options):
dependencyString = "Implicit dependency on \(dependencyDescription) via options '\(options.joined(separator: " "))' in build setting '\(settingName)'"
case .impliedByTransitiveDependencyViaRemovedTargets(let intermediateTargetName):
dependencyString = "Dependency on \(dependencyDescription) via transitive dependency through '\(intermediateTargetName)'"
Expand Down
Loading