diff --git a/Sources/SWBBuildSystem/DependencyCycleFormatter.swift b/Sources/SWBBuildSystem/DependencyCycleFormatter.swift index b0d7f6fa..88e83e99 100644 --- a/Sources/SWBBuildSystem/DependencyCycleFormatter.swift +++ b/Sources/SWBBuildSystem/DependencyCycleFormatter.swift @@ -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)'" @@ -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)'" diff --git a/Sources/SWBCore/LinkageDependencyResolver.swift b/Sources/SWBCore/LinkageDependencyResolver.swift index 2758f545..f9c87139 100644 --- a/Sources/SWBCore/LinkageDependencyResolver.swift +++ b/Sources/SWBCore/LinkageDependencyResolver.swift @@ -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 { @@ -79,11 +80,15 @@ actor LinkageDependencyResolver { /// Sets of targets mapped by product name stem. private let targetsByProductNameStem: [String: Set] + /// Sets of targets mapped by module name (computed using parameters from the build request). + private let targetsByUnconfiguredModuleName: [String: Set] + internal let resolver: DependencyResolver init(workspaceContext: WorkspaceContext, buildRequest: BuildRequest, buildRequestContext: BuildRequestContext, delegate: any TargetDependencyResolverDelegate) { var targetsByProductName = [String: Set]() var targetsByProductNameStem = [String: Set]() + var targetsByUnconfiguredModuleName = [String: Set]() 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: Swift Build doesn't support product references with non-constant basenames @@ -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) } @@ -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 @@ -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 } @@ -360,6 +371,16 @@ actor LinkageDependencyResolver { } } + let moduleNamesOfExplicitDependencies = Set(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 } @@ -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 @@ -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, _): @@ -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)" } } } @@ -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 diff --git a/Sources/SWBCore/Settings/BuiltinMacros.swift b/Sources/SWBCore/Settings/BuiltinMacros.swift index bf7cc068..bc5da2ac 100644 --- a/Sources/SWBCore/Settings/BuiltinMacros.swift +++ b/Sources/SWBCore/Settings/BuiltinMacros.swift @@ -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 public static let MODULE_VERIFIER_LSV = BuiltinMacros.declareBooleanMacro("MODULE_VERIFIER_LSV") public static let MODULE_VERIFIER_SUPPORTED_LANGUAGES = BuiltinMacros.declareStringListMacro("MODULE_VERIFIER_SUPPORTED_LANGUAGES") @@ -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, diff --git a/Sources/SWBCore/Settings/Settings.swift b/Sources/SWBCore/Settings/Settings.swift index eb3b72cc..b642acec 100644 --- a/Sources/SWBCore/Settings/Settings.swift +++ b/Sources/SWBCore/Settings/Settings.swift @@ -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" + ) + } + } +} diff --git a/Sources/SWBCore/Specs/CoreBuildSystem.xcspec b/Sources/SWBCore/Specs/CoreBuildSystem.xcspec index d89f2142..2f9992c8 100644 --- a/Sources/SWBCore/Specs/CoreBuildSystem.xcspec +++ b/Sources/SWBCore/Specs/CoreBuildSystem.xcspec @@ -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; diff --git a/Sources/SWBCore/Specs/en.lproj/CoreBuildSystem.strings b/Sources/SWBCore/Specs/en.lproj/CoreBuildSystem.strings index a33cfc4a..7b03e48d 100644 --- a/Sources/SWBCore/Specs/en.lproj/CoreBuildSystem.strings +++ b/Sources/SWBCore/Specs/en.lproj/CoreBuildSystem.strings @@ -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."; diff --git a/Sources/SWBCore/TargetDependencyResolver.swift b/Sources/SWBCore/TargetDependencyResolver.swift index 9a0e4dc0..018abdc2 100644 --- a/Sources/SWBCore/TargetDependencyResolver.swift +++ b/Sources/SWBCore/TargetDependencyResolver.swift @@ -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) } @@ -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)'" diff --git a/Tests/SWBCoreTests/TargetDependencyResolverTests.swift b/Tests/SWBCoreTests/TargetDependencyResolverTests.swift index 69a6a6ae..0b43a0dc 100644 --- a/Tests/SWBCoreTests/TargetDependencyResolverTests.swift +++ b/Tests/SWBCoreTests/TargetDependencyResolverTests.swift @@ -4591,6 +4591,84 @@ fileprivate enum TargetPlatformSpecializationMode { XCTAssertEqualSequences(buildGraph.allTargets.map({ $0.target.name }).sorted(), ["AppTarget", "AlwaysUsedDependency"].sorted()) } } + + @Test + func appAndFrameworkModuleDependencies() async throws { + let core = try await getCore() + + let workspace = try TestWorkspace( + "Workspace", + projects: [ + TestProject( + "P1", + groupTree: TestGroup( + "G1", + children: [ + TestFile("aFramework.framework"), + ] + ), + buildConfigurations: [ + TestBuildConfiguration("Debug", buildSettings: [:]), + ], + targets: [ + TestStandardTarget( + "anApp", + type: .application, + buildConfigurations: [ + TestBuildConfiguration("Debug", buildSettings: [ + "PRODUCT_NAME": "anApp", + "MODULE_DEPENDENCIES": "'public aFramework' nonExisting", + ]), + ] + ) + ] + ), + TestProject( + "P2", + groupTree: TestGroup( + "G2", + children:[ + ] + ), + buildConfigurations: [ + TestBuildConfiguration("Debug", buildSettings: [:]), + ], + targets: [ + TestStandardTarget( + "aFramework", + type: .framework, + buildConfigurations: [ + TestBuildConfiguration("Debug", buildSettings: ["PRODUCT_NAME": "aFramework"]), + ] + ), + ] + ), + ] + ).load(core) + let workspaceContext = WorkspaceContext(core: core, workspace: workspace, processExecutionCache: .sharedForTesting) + + // Perform some simple correctness tests. + #expect(workspace.projects.count == 2) + let appProject = workspace.projects[0] + let fwkProject = workspace.projects[1] + + // Configure the targets and create a BuildRequest. + let buildParameters = BuildParameters(configuration: "Debug") + let appTarget = BuildRequest.BuildTargetInfo(parameters: buildParameters, target: appProject.targets[0]) + let fwkTarget = BuildRequest.BuildTargetInfo(parameters: buildParameters, target: fwkProject.targets[0]) + let buildRequest = BuildRequest(parameters: buildParameters, buildTargets: [appTarget], continueBuildingAfterErrors: true, useParallelTargets: false, useImplicitDependencies: true, useDryRun: false) + let buildRequestContext = BuildRequestContext(workspaceContext: workspaceContext) + + let delegate = EmptyTargetDependencyResolverDelegate(workspace: workspaceContext.workspace) + + // Get the dependency closure for the build request and examine it. + let buildGraph = await TargetGraphFactory(workspaceContext: workspaceContext, buildRequest: buildRequest, buildRequestContext: buildRequestContext, delegate: delegate).graph(type: .dependency) + let dependencyClosure = buildGraph.allTargets + #expect(dependencyClosure.map({ $0.target.name }) == ["aFramework", "anApp"]) + #expect(try buildGraph.dependencies(appTarget) == [try buildGraph.target(for: fwkTarget)]) + #expect(try buildGraph.dependencies(fwkTarget) == []) + delegate.checkNoDiagnostics() + } } @Suite fileprivate struct SuperimposedPropertiesTests: CoreBasedTests {