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/CMakeLists.txt b/Sources/SWBCore/CMakeLists.txt index 9dd6a4bb..3833e042 100644 --- a/Sources/SWBCore/CMakeLists.txt +++ b/Sources/SWBCore/CMakeLists.txt @@ -34,6 +34,7 @@ add_library(SWBCore ConfiguredTarget.swift Core.swift CustomTaskTypeDescription.swift + Dependencies.swift DependencyInfoEditPayload.swift DependencyResolution.swift DiagnosticSupport.swift diff --git a/Sources/SWBCore/Dependencies.swift b/Sources/SWBCore/Dependencies.swift new file mode 100644 index 00000000..08efa2af --- /dev/null +++ b/Sources/SWBCore/Dependencies.swift @@ -0,0 +1,184 @@ +//===----------------------------------------------------------------------===// +// +// This source file is part of the Swift open source project +// +// Copyright (c) 2025 Apple Inc. and the Swift project authors +// Licensed under Apache License v2.0 with Runtime Library Exception +// +// See http://swift.org/LICENSE.txt for license information +// See http://swift.org/CONTRIBUTORS.txt for the list of Swift project authors +// +//===----------------------------------------------------------------------===// + +public import SWBUtil +import SWBMacro + +public struct ModuleDependency: Hashable, Sendable, SerializableCodable { + public let name: String + public let accessLevel: AccessLevel + + public enum AccessLevel: String, Hashable, Sendable, CaseIterable, Codable, Serializable { + case Private = "private" + case Package = "package" + case Public = "public" + + public init(_ string: String) throws { + guard let accessLevel = AccessLevel(rawValue: string) else { + throw StubError.error("unexpected access modifier '\(string)', expected one of: \(AccessLevel.allCases.map { $0.rawValue }.joined(separator: ", "))") + } + + self = accessLevel + } + } + + public init(name: String, accessLevel: AccessLevel) { + self.name = name + self.accessLevel = accessLevel + } + + public init(entry: String) throws { + var it = entry.split(separator: " ").makeIterator() + switch (it.next(), it.next(), it.next()) { + case (let .some(name), nil, nil): + self.name = String(name) + self.accessLevel = .Private + + case (let .some(accessLevel), let .some(name), nil): + self.name = String(name) + self.accessLevel = try AccessLevel(String(accessLevel)) + + default: + throw StubError.error("expected 1 or 2 space-separated components in: \(entry)") + } + } + + public var asBuildSettingEntry: String { + "\(accessLevel == .Private ? "" : "\(accessLevel.rawValue) ")\(name)" + } + + public var asBuildSettingEntryQuotedIfNeeded: String { + let e = asBuildSettingEntry + return e.contains(" ") ? "\"\(e)\"" : e + } +} + +public struct ModuleDependenciesContext: Sendable, SerializableCodable { + var validate: BooleanWarningLevel + var moduleDependencies: [ModuleDependency] + var fixItContext: FixItContext? + + init(validate: BooleanWarningLevel, moduleDependencies: [ModuleDependency], fixItContext: FixItContext? = nil) { + self.validate = validate + self.moduleDependencies = moduleDependencies + self.fixItContext = fixItContext + } + + public init?(settings: Settings) { + let validate = settings.globalScope.evaluate(BuiltinMacros.VALIDATE_MODULE_DEPENDENCIES) + guard validate != .no else { return nil } + let fixItContext = ModuleDependenciesContext.FixItContext(settings: settings) + self.init(validate: validate, moduleDependencies: settings.moduleDependencies, fixItContext: fixItContext) + } + + /// Nil `imports` means the current toolchain doesn't have the features to gather imports. This is temporarily required to support running against older toolchains. + public func makeDiagnostics(imports: [(ModuleDependency, importLocations: [Diagnostic.Location])]?) -> [Diagnostic] { + guard validate != .no else { return [] } + guard let imports else { + return [Diagnostic( + behavior: .error, + location: .unknown, + data: DiagnosticData("The current toolchain does not support \(BuiltinMacros.VALIDATE_MODULE_DEPENDENCIES.name)"))] + } + + let missingDeps = imports.filter { + // ignore module deps without source locations, these are inserted by swift / swift-build and we should treat them as implementation details which we can track without needing the user to declare them + if $0.importLocations.isEmpty { return false } + + // TODO: if the difference is just the access modifier, we emit a new entry, but ultimately our fixit should update the existing entry or emit an error about a conflict + if moduleDependencies.contains($0.0) { return false } + return true + } + + guard !missingDeps.isEmpty else { return [] } + + let behavior: Diagnostic.Behavior = validate == .yesError ? .error : .warning + + let fixIt = fixItContext?.makeFixIt(newModules: missingDeps.map { $0.0 }) + let fixIts = fixIt.map { [$0] } ?? [] + + let importDiags: [Diagnostic] = missingDeps + .flatMap { dep in + dep.1.map { + return Diagnostic( + behavior: behavior, + location: $0, + data: DiagnosticData("Missing entry in \(BuiltinMacros.MODULE_DEPENDENCIES.name): \(dep.0.asBuildSettingEntryQuotedIfNeeded)"), + fixIts: fixIts) + } + } + + let message = "Missing entries in \(BuiltinMacros.MODULE_DEPENDENCIES.name): \(missingDeps.map { $0.0.asBuildSettingEntryQuotedIfNeeded }.sorted().joined(separator: " "))" + + let location: Diagnostic.Location = fixIt.map { + Diagnostic.Location.path($0.sourceRange.path, line: $0.sourceRange.endLine, column: $0.sourceRange.endColumn) + } ?? Diagnostic.Location.buildSetting(BuiltinMacros.MODULE_DEPENDENCIES) + + return [Diagnostic( + behavior: behavior, + location: location, + data: DiagnosticData(message), + fixIts: fixIts, + childDiagnostics: importDiags)] + } + + struct FixItContext: Sendable, SerializableCodable { + var sourceRange: Diagnostic.SourceRange + var modificationStyle: ModificationStyle + + init(sourceRange: Diagnostic.SourceRange, modificationStyle: ModificationStyle) { + self.sourceRange = sourceRange + self.modificationStyle = modificationStyle + } + + init?(settings: Settings) { + guard let target = settings.target else { return nil } + let thisTargetCondition = MacroCondition(parameter: BuiltinMacros.targetNameCondition, valuePattern: target.name) + + if let assignment = (settings.globalScope.table.lookupMacro(BuiltinMacros.MODULE_DEPENDENCIES)?.sequence.first { + $0.location != nil && ($0.conditions?.conditions == [thisTargetCondition] || ($0.conditions?.conditions.isEmpty ?? true)) + }), + let location = assignment.location + { + self.init(sourceRange: .init(path: location.path, startLine: location.endLine, startColumn: location.endColumn, endLine: location.endLine, endColumn: location.endColumn), modificationStyle: .appendToExistingAssignment) + } + else if let path = settings.constructionComponents.targetXcconfigPath { + self.init(sourceRange: .init(path: path, startLine: 0, startColumn: 0, endLine: 0, endColumn: 0), modificationStyle: .insertNewAssignment(targetNameCondition: nil)) + } + else if let path = settings.constructionComponents.projectXcconfigPath { + self.init(sourceRange: .init(path: path, startLine: 0, startColumn: 0, endLine: 0, endColumn: 0), modificationStyle: .insertNewAssignment(targetNameCondition: target.name)) + } + else { + return nil + } + } + + enum ModificationStyle: Sendable, SerializableCodable, Hashable { + case appendToExistingAssignment + case insertNewAssignment(targetNameCondition: String?) + } + + func makeFixIt(newModules: [ModuleDependency]) -> Diagnostic.FixIt { + let stringValue = newModules.map { $0.asBuildSettingEntryQuotedIfNeeded }.sorted().joined(separator: " ") + let newText: String + switch modificationStyle { + case .appendToExistingAssignment: + newText = " \(stringValue)" + case .insertNewAssignment(let targetNameCondition): + let targetCondition = targetNameCondition.map { "[target=\($0)]" } ?? "" + newText = "\n\(BuiltinMacros.MODULE_DEPENDENCIES.name)\(targetCondition) = $(inherited) \(stringValue)\n" + } + + return Diagnostic.FixIt(sourceRange: sourceRange, newText: newText) + } + } +} diff --git a/Sources/SWBCore/LibSwiftDriver/LibSwiftDriver.swift b/Sources/SWBCore/LibSwiftDriver/LibSwiftDriver.swift index bd6cd847..d8f5683d 100644 --- a/Sources/SWBCore/LibSwiftDriver/LibSwiftDriver.swift +++ b/Sources/SWBCore/LibSwiftDriver/LibSwiftDriver.swift @@ -238,6 +238,24 @@ public final class SwiftModuleDependencyGraph: SwiftGlobalExplicitDependencyGrap return fileDependencies } + func mainModule(for key: String) async throws -> SwiftDriver.ModuleInfo? { + let graph = try await registryQueue.sync { + guard let driver = self.registry[key] else { + throw StubError.error("Unable to find jobs for key \(key). Be sure to plan the build ahead of fetching results.") + } + return driver.intermoduleDependencyGraph + } + guard let graph else { return nil } + return graph.mainModule + } + + /// Nil result means the current toolchain / libSwiftScan does not support importInfos + public func mainModuleImportModuleDependencies(for key: String) async throws -> [(ModuleDependency, importLocations: [SWBUtil.Diagnostic.Location])]? { + try await mainModule(for: key)?.importInfos?.map { + (ModuleDependency($0), $0.sourceLocations.map { Diagnostic.Location($0) }) + } + } + public func queryTransitiveDependencyModuleNames(for key: String) async throws -> [String] { let graph = try await registryQueue.sync { guard let driver = self.registry[key] else { @@ -849,3 +867,29 @@ extension SWBUtil.Diagnostic.Behavior { } } } + +extension SWBUtil.Diagnostic.Location { + init(_ loc: ScannerDiagnosticSourceLocation) { + self = .path(Path(loc.bufferIdentifier), line: loc.lineNumber, column: loc.columnNumber) + } +} + +extension ModuleDependency.AccessLevel { + init(_ accessLevel: ImportInfo.ImportAccessLevel) { + switch accessLevel { + case .Private, .FilePrivate, .Internal: + self = .Private + case .Package: + self = .Package + case .Public: + self = .Public + } + } +} + +extension ModuleDependency { + init(_ importInfo: ImportInfo) { + self.name = importInfo.importIdentifier + self.accessLevel = .init(importInfo.accessLevel) + } +} diff --git a/Sources/SWBCore/LinkageDependencyResolver.swift b/Sources/SWBCore/LinkageDependencyResolver.swift index 2758f545..e5b9fcd0 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/MacroConfigFileLoader.swift b/Sources/SWBCore/MacroConfigFileLoader.swift index 2d5e4248..cbb184b4 100644 --- a/Sources/SWBCore/MacroConfigFileLoader.swift +++ b/Sources/SWBCore/MacroConfigFileLoader.swift @@ -242,7 +242,7 @@ final class MacroConfigFileLoader: Sendable { return MacroConfigFileParser(byteString: data, path: path, delegate: delegate) } - mutating func foundMacroValueAssignment(_ macroName: String, conditions: [(param: String, pattern: String)], value: String, parser: MacroConfigFileParser) { + mutating func foundMacroValueAssignment(_ macroName: String, conditions: [(param: String, pattern: String)], value: String, path: Path, startLine: Int, endLine: Int, startColumn: Int, endColumn: Int, parser: MacroConfigFileParser) { // Look up the macro name, creating it as a user-defined macro if it isn’t already known. let macro = table.namespace.lookupOrDeclareMacro(UserDefinedMacroDeclaration.self, macroName) @@ -253,7 +253,8 @@ final class MacroConfigFileLoader: Sendable { } // Parse the value in a manner consistent with the macro definition. - table.push(macro, table.namespace.parseForMacro(macro, value: value), conditions: conditionSet) + let location = MacroValueAssignmentLocation(path: path, startLine: startLine, endLine: endLine, startColumn: startColumn, endColumn: endColumn) + table.push(macro, table.namespace.parseForMacro(macro, value: value), conditions: conditionSet, location: location) } func handleDiagnostic(_ diagnostic: MacroConfigFileDiagnostic, parser: MacroConfigFileParser) { @@ -301,8 +302,8 @@ fileprivate final class MacroValueAssignmentTableRef { table.namespace } - func push(_ macro: MacroDeclaration, _ value: MacroExpression, conditions: MacroConditionSet? = nil) { - table.push(macro, value, conditions: conditions) + func push(_ macro: MacroDeclaration, _ value: MacroExpression, conditions: MacroConditionSet? = nil, location: MacroValueAssignmentLocation? = nil) { + table.push(macro, value, conditions: conditions, location: location) } } diff --git a/Sources/SWBCore/Settings/BuiltinMacros.swift b/Sources/SWBCore/Settings/BuiltinMacros.swift index deae0f54..77c4a7f3 100644 --- a/Sources/SWBCore/Settings/BuiltinMacros.swift +++ b/Sources/SWBCore/Settings/BuiltinMacros.swift @@ -866,6 +866,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") @@ -1950,6 +1951,7 @@ public final class BuiltinMacros { MODULEMAP_PRIVATE_FILE, MODULES_FOLDER_PATH, MODULE_CACHE_DIR, + MODULE_DEPENDENCIES, MODULE_NAME, MODULE_START, MODULE_STOP, @@ -2623,7 +2625,7 @@ public extension BuiltinMacros { } /// Enumeration macro type for tri-state booleans, typically used for warnings which can be set to "No", "Yes", or "Yes (Error)". -public enum BooleanWarningLevel: String, Equatable, Hashable, Serializable, EnumerationMacroType, Encodable { +public enum BooleanWarningLevel: String, Equatable, Hashable, Serializable, EnumerationMacroType, Codable { public static let defaultValue = BooleanWarningLevel.no case yesError = "YES_ERROR" diff --git a/Sources/SWBCore/Settings/Settings.swift b/Sources/SWBCore/Settings/Settings.swift index 59be5552..693c493d 100644 --- a/Sources/SWBCore/Settings/Settings.swift +++ b/Sources/SWBCore/Settings/Settings.swift @@ -750,6 +750,8 @@ public final class Settings: PlatformBuildContext, Sendable { targetBuildVersionPlatforms(in: globalScope) } + public let moduleDependencies: [ModuleDependency] + public static func supportsMacCatalyst(scope: MacroEvaluationScope, core: Core) -> Bool { @preconcurrency @PluginExtensionSystemActor func sdkVariantInfoExtensions() -> [any SDKVariantInfoExtensionPoint.ExtensionProtocol] { core.pluginManager.extensions(of: SDKVariantInfoExtensionPoint.self) @@ -905,6 +907,7 @@ public final class Settings: PlatformBuildContext, Sendable { } self.supportedBuildVersionPlatforms = effectiveSupportedPlatforms(sdkRegistry: sdkRegistry) + self.moduleDependencies = builder.moduleDependencies self.constructionComponents = builder.constructionComponents } @@ -1290,6 +1293,8 @@ private class SettingsBuilder { /// The bound signing settings, once added in computeSigningSettings(). var signingSettings: Settings.SigningSettings? = nil + var moduleDependencies: [ModuleDependency] = [] + // Mutable state of the builder as we're building up the settings table. @@ -1624,6 +1629,13 @@ private class SettingsBuilder { } } + do { + self.moduleDependencies = try createScope(sdkToUse: boundProperties.sdk).evaluate(BuiltinMacros.MODULE_DEPENDENCIES).map { try ModuleDependency(entry: $0) } + } + catch { + errors.append("Failed to parse \(BuiltinMacros.MODULE_DEPENDENCIES.name): \(error)") + } + // At this point settings construction is finished. // Analyze the settings to generate any issues about them. diff --git a/Sources/SWBCore/SpecImplementations/Tools/SwiftCompiler.swift b/Sources/SWBCore/SpecImplementations/Tools/SwiftCompiler.swift index 983538cf..8e7e6459 100644 --- a/Sources/SWBCore/SpecImplementations/Tools/SwiftCompiler.swift +++ b/Sources/SWBCore/SpecImplementations/Tools/SwiftCompiler.swift @@ -446,7 +446,9 @@ public struct SwiftTaskPayload: ParentTaskPayload { /// The preview build style in effect (dynamic replacement or XOJIT), if any. public let previewStyle: PreviewStyleMessagePayload? - init(moduleName: String, indexingPayload: SwiftIndexingPayload, previewPayload: SwiftPreviewPayload?, localizationPayload: SwiftLocalizationPayload?, numExpectedCompileSubtasks: Int, driverPayload: SwiftDriverPayload?, previewStyle: PreviewStyle?) { + public let moduleDependenciesContext: ModuleDependenciesContext? + + init(moduleName: String, indexingPayload: SwiftIndexingPayload, previewPayload: SwiftPreviewPayload?, localizationPayload: SwiftLocalizationPayload?, numExpectedCompileSubtasks: Int, driverPayload: SwiftDriverPayload?, previewStyle: PreviewStyle?, moduleDependenciesContext: ModuleDependenciesContext?) { self.moduleName = moduleName self.indexingPayload = indexingPayload self.previewPayload = previewPayload @@ -461,10 +463,11 @@ public struct SwiftTaskPayload: ParentTaskPayload { case nil: self.previewStyle = nil } + self.moduleDependenciesContext = moduleDependenciesContext } public func serialize(to serializer: T) { - serializer.serializeAggregate(7) { + serializer.serializeAggregate(8) { serializer.serialize(moduleName) serializer.serialize(indexingPayload) serializer.serialize(previewPayload) @@ -472,11 +475,12 @@ public struct SwiftTaskPayload: ParentTaskPayload { serializer.serialize(numExpectedCompileSubtasks) serializer.serialize(driverPayload) serializer.serialize(previewStyle) + serializer.serialize(moduleDependenciesContext) } } public init(from deserializer: any Deserializer) throws { - try deserializer.beginAggregate(7) + try deserializer.beginAggregate(8) self.moduleName = try deserializer.deserialize() self.indexingPayload = try deserializer.deserialize() self.previewPayload = try deserializer.deserialize() @@ -484,6 +488,7 @@ public struct SwiftTaskPayload: ParentTaskPayload { self.numExpectedCompileSubtasks = try deserializer.deserialize() self.driverPayload = try deserializer.deserialize() self.previewStyle = try deserializer.deserialize() + self.moduleDependenciesContext = try deserializer.deserialize() } } @@ -2279,7 +2284,6 @@ public final class SwiftCompilerSpec : CompilerSpec, SpecIdentifierType, SwiftDi ] } - // BUILT_PRODUCTS_DIR here is guaranteed to be absolute by `getCommonTargetTaskOverrides`. let payload = SwiftTaskPayload( moduleName: moduleName, @@ -2296,7 +2300,9 @@ public final class SwiftCompilerSpec : CompilerSpec, SpecIdentifierType, SwiftDi previewPayload: previewPayload, localizationPayload: localizationPayload, numExpectedCompileSubtasks: isUsingWholeModuleOptimization ? 1 : cbc.inputs.count, - driverPayload: await driverPayload(uniqueID: String(args.hashValue), scope: cbc.scope, delegate: delegate, compilationMode: compilationMode, isUsingWholeModuleOptimization: isUsingWholeModuleOptimization, args: args, tempDirPath: objectFileDir, explicitModulesTempDirPath: Path(cbc.scope.evaluate(BuiltinMacros.SWIFT_EXPLICIT_MODULES_OUTPUT_PATH)), variant: variant, arch: arch + compilationMode.moduleBaseNameSuffix, commandLine: ["builtin-SwiftDriver", "--"] + args, ruleInfo: ruleInfo(compilationMode.ruleNameIntegratedDriver, targetName), casOptions: casOptions, linkerResponseFilePath: moduleLinkerArgsPath), previewStyle: cbc.scope.previewStyle + driverPayload: await driverPayload(uniqueID: String(args.hashValue), scope: cbc.scope, delegate: delegate, compilationMode: compilationMode, isUsingWholeModuleOptimization: isUsingWholeModuleOptimization, args: args, tempDirPath: objectFileDir, explicitModulesTempDirPath: Path(cbc.scope.evaluate(BuiltinMacros.SWIFT_EXPLICIT_MODULES_OUTPUT_PATH)), variant: variant, arch: arch + compilationMode.moduleBaseNameSuffix, commandLine: ["builtin-SwiftDriver", "--"] + args, ruleInfo: ruleInfo(compilationMode.ruleNameIntegratedDriver, targetName), casOptions: casOptions, linkerResponseFilePath: moduleLinkerArgsPath), + previewStyle: cbc.scope.previewStyle, + moduleDependenciesContext: cbc.producer.moduleDependenciesContext ) // Finally, assemble the input and output paths and create the Swift compiler command. diff --git a/Sources/SWBCore/Specs/CoreBuildSystem.xcspec b/Sources/SWBCore/Specs/CoreBuildSystem.xcspec index 0514414a..748ecfab 100644 --- a/Sources/SWBCore/Specs/CoreBuildSystem.xcspec +++ b/Sources/SWBCore/Specs/CoreBuildSystem.xcspec @@ -1598,6 +1598,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 4aeafd33..a33a5dd9 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/Sources/SWBCore/TaskGeneration.swift b/Sources/SWBCore/TaskGeneration.swift index 6d267632..f67801f2 100644 --- a/Sources/SWBCore/TaskGeneration.swift +++ b/Sources/SWBCore/TaskGeneration.swift @@ -271,6 +271,8 @@ public protocol CommandProducer: PlatformBuildContext, SpecLookupContext, Refere var userPreferences: UserPreferences { get } var hostOperatingSystem: OperatingSystem { get } + + var moduleDependenciesContext: ModuleDependenciesContext? { get } } extension CommandProducer { diff --git a/Sources/SWBMacro/MacroConfigFileParser.swift b/Sources/SWBMacro/MacroConfigFileParser.swift index a1fa7ebf..7497f867 100644 --- a/Sources/SWBMacro/MacroConfigFileParser.swift +++ b/Sources/SWBMacro/MacroConfigFileParser.swift @@ -276,6 +276,7 @@ public final class MacroConfigFileParser { // MARK: Parsing of value assignment starts here. /// Parses a macro value assignment line of the form MACRONAME [ optional conditions ] ... = VALUE ';'? private func parseMacroValueAssignment() { + let startOfLine = currIdx - 1 // First skip over any whitespace and comments. skipWhitespaceAndComments() @@ -361,6 +362,8 @@ public final class MacroConfigFileParser { // Skip over the equals sign. assert(currChar == /* '=' */ 61) advance() + let startLine = currLine + let startColumn = currIdx - startOfLine var chunks : [String] = [] while let chunk = parseNonListAssignmentRHS() { @@ -383,7 +386,7 @@ public final class MacroConfigFileParser { } // Finally, now that we have the name, conditions, and value, we tell the delegate about it. let value = chunks.joined(separator: " ") - delegate?.foundMacroValueAssignment(name, conditions: conditions, value: value, parser: self) + delegate?.foundMacroValueAssignment(name, conditions: conditions, value: value, path: path, startLine: startLine, endLine: currLine, startColumn: startColumn, endColumn: currIdx - startOfLine, parser: self) } public func parseNonListAssignmentRHS() -> String? { @@ -518,7 +521,7 @@ public final class MacroConfigFileParser { } func endPreprocessorInclusion() { } - func foundMacroValueAssignment(_ macroName: String, conditions: [(param: String, pattern: String)], value: String, parser: MacroConfigFileParser) { + func foundMacroValueAssignment(_ macroName: String, conditions: [(param: String, pattern: String)], value: String, path: Path, startLine: Int, endLine: Int, startColumn: Int, endColumn: Int, parser: MacroConfigFileParser) { self.macroName = macroName self.conditions = conditions.isEmpty ? nil : conditions } @@ -565,7 +568,7 @@ public protocol MacroConfigFileParserDelegate { func endPreprocessorInclusion() /// Invoked once for each macro value assignment. The `macroName` is guaranteed to be non-empty, but `value` may be empty. Any macro conditions are passed as tuples in the `conditions`; parameters are guaranteed to be non-empty strings, but patterns may be empty. - mutating func foundMacroValueAssignment(_ macroName: String, conditions: [(param: String, pattern: String)], value: String, parser: MacroConfigFileParser) + mutating func foundMacroValueAssignment(_ macroName: String, conditions: [(param: String, pattern: String)], value: String, path: Path, startLine: Int, endLine: Int, startColumn: Int, endColumn: Int, parser: MacroConfigFileParser) /// Invoked if an error, warning, or other diagnostic is detected. func handleDiagnostic(_ diagnostic: MacroConfigFileDiagnostic, parser: MacroConfigFileParser) diff --git a/Sources/SWBMacro/MacroEvaluationScope.swift b/Sources/SWBMacro/MacroEvaluationScope.swift index 98d6e692..f8c739e2 100644 --- a/Sources/SWBMacro/MacroEvaluationScope.swift +++ b/Sources/SWBMacro/MacroEvaluationScope.swift @@ -17,7 +17,7 @@ private extension MacroValueAssignmentTable { func lookupMacro(_ macro: MacroDeclaration, overrideLookup: ((MacroDeclaration) -> MacroExpression?)? = nil) -> MacroValueAssignment? { // See if we have an overriding binding. if let override = overrideLookup?(macro) { - return MacroValueAssignment(expression: override, conditions: nil, next: lookupMacro(macro)) + return MacroValueAssignment(expression: override, conditions: nil, next: lookupMacro(macro), location: nil) } // Otherwise, return the normal lookup. diff --git a/Sources/SWBMacro/MacroValueAssignmentTable.swift b/Sources/SWBMacro/MacroValueAssignmentTable.swift index 84a6f962..cb1204b6 100644 --- a/Sources/SWBMacro/MacroValueAssignmentTable.swift +++ b/Sources/SWBMacro/MacroValueAssignmentTable.swift @@ -11,6 +11,7 @@ //===----------------------------------------------------------------------===// public import SWBUtil +import Synchronization /// A mapping from macro declarations to corresponding macro value assignments, each of which is a linked list of macro expressions in precedence order. At the moment it doesn’t support conditional assignments, but that functionality will be implemented soon. public struct MacroValueAssignmentTable: Serializable, Sendable { @@ -77,11 +78,11 @@ public struct MacroValueAssignmentTable: Serializable, Sendable { /// Adds a mapping from `macro` to `value`, inserting it ahead of any already existing assignment for the same macro. Unless the value refers to the lower-precedence expression (using `$(inherited)` notation), any existing assignments are shadowed but not removed. - public mutating func push(_ macro: MacroDeclaration, _ value: MacroExpression, conditions: MacroConditionSet? = nil) { + public mutating func push(_ macro: MacroDeclaration, _ value: MacroExpression, conditions: MacroConditionSet? = nil, location: MacroValueAssignmentLocation? = nil) { assert(namespace.lookupMacroDeclaration(macro.name) === macro) // Validate the type. assert(macro.type.matchesExpressionType(value)) - valueAssignments[macro] = MacroValueAssignment(expression: value, conditions: conditions, next: valueAssignments[macro]) + valueAssignments[macro] = MacroValueAssignment(expression: value, conditions: conditions, next: valueAssignments[macro], location: location) } /// Adds a mapping from each of the macro-to-value mappings in `otherTable`, inserting them ahead of any already existing assignments in the receiving table. The other table isn’t affected in any way (in particular, no reference is kept from the receiver to the other table). @@ -106,6 +107,10 @@ public struct MacroValueAssignmentTable: Serializable, Sendable { return valueAssignments.isEmpty } + public func location(of macro: MacroDeclaration) -> MacroValueAssignmentLocation? { + return lookupMacro(macro)?.location + } + public func bindConditionParameter(_ parameter: MacroConditionParameter, _ conditionValues: [String]) -> MacroValueAssignmentTable { return bindConditionParameter(parameter, conditionValues.map { .string($0) }) } @@ -178,7 +183,7 @@ public struct MacroValueAssignmentTable: Serializable, Sendable { if effectiveConditionValue.evaluate(condition) == true { // Condition evaluates to true, so we push an assignment with a condition set that excludes the condition. let filteredConditions = conditions.conditions.filter{ $0.parameter != parameter } - table.push(macro, assignment.expression, conditions: filteredConditions.isEmpty ? nil : MacroConditionSet(conditions: filteredConditions)) + table.push(macro, assignment.expression, conditions: filteredConditions.isEmpty ? nil : MacroConditionSet(conditions: filteredConditions), location: assignment.location) } else { // Condition evaluates to false, so we elide the assignment. @@ -186,7 +191,7 @@ public struct MacroValueAssignmentTable: Serializable, Sendable { } else { // Assignment isn't conditioned on the specified parameter, so we just push it as-is. - table.push(macro, assignment.expression, conditions: assignment.conditions) + table.push(macro, assignment.expression, conditions: assignment.conditions, location: assignment.location) } } bindAndPushAssignment(firstAssignment) @@ -325,11 +330,40 @@ public final class MacroValueAssignment: Serializable, CustomStringConvertible, /// Reference to the next (lower precedence) assignment in the linked list, or nil if this is the last one. public let next: MacroValueAssignment? + private let _location: InternedMacroValueAssignmentLocation? + private static let macroConfigPaths = SWBMutex>(OrderedSet()) + + public var location: MacroValueAssignmentLocation? { + if let _location { + return .init( + path: Self.macroConfigPaths.withLock { $0[_location.pathRef] }, + startLine: _location.startLine, + endLine: _location.endLine, + startColumn: _location.startColumn, + endColumn: _location.endColumn + ) + } else { + return nil + } + } + /// Initializes the macro value assignment to represent `expression`, with the next existing macro value assignment (if any). - init(expression: MacroExpression, conditions: MacroConditionSet? = nil, next: MacroValueAssignment?) { + init(expression: MacroExpression, conditions: MacroConditionSet? = nil, next: MacroValueAssignment?, location: MacroValueAssignmentLocation?) { self.expression = expression self.conditions = conditions self.next = next + + if let location { + self._location = InternedMacroValueAssignmentLocation( + pathRef: Self.macroConfigPaths.withLock({ $0.append(location.path).index }), + startLine: location.startLine, + endLine: location.endLine, + startColumn: location.startColumn, + endColumn: location.endColumn + ) + } else { + self._location = nil + } } /// Returns the first macro value assignment that is reachable from the receiver and whose conditions match the given set of parameter values, or nil if there is no such assignment value. The returned assignment may be the receiver itself, or it may be any assignment that’s downstream in the linked list of macro value assignments, or it may be nil if there is none. Unconditional macro value assignments are considered to match any conditions. Conditions that reference parameters that don’t have a value in `paramValues` are only considered to match if the match pattern is `*`, i.e. the “match-anything” pattern (which is effectively a no-op). @@ -381,18 +415,71 @@ public final class MacroValueAssignment: Serializable, CustomStringConvertible, // MARK: Serialization public func serialize(to serializer: T) { - serializer.beginAggregate(3) + serializer.beginAggregate(4) serializer.serialize(expression) serializer.serialize(conditions) serializer.serialize(next) + serializer.serialize(_location) serializer.endAggregate() } public init(from deserializer: any Deserializer) throws { - try deserializer.beginAggregate(3) + try deserializer.beginAggregate(4) self.expression = try deserializer.deserialize() self.conditions = try deserializer.deserialize() self.next = try deserializer.deserialize() + self._location = try deserializer.deserialize() + } +} + +public struct MacroValueAssignmentLocation: Sendable, Equatable { + public let path: Path + public let startLine: Int + public let endLine: Int + public let startColumn: Int + public let endColumn: Int + + public init(path: Path, startLine: Int, endLine: Int, startColumn: Int, endColumn: Int) { + self.path = path + self.startLine = startLine + self.endLine = endLine + self.startColumn = startColumn + self.endColumn = endColumn + } +} + +private struct InternedMacroValueAssignmentLocation: Serializable, Sendable { + let pathRef: OrderedSet.Index + public let startLine: Int + public let endLine: Int + let startColumn: Int + let endColumn: Int + + init(pathRef: OrderedSet.Index, startLine: Int, endLine: Int, startColumn: Int, endColumn: Int) { + self.pathRef = pathRef + self.startLine = startLine + self.endLine = endLine + self.startColumn = startColumn + self.endColumn = endColumn + } + + public func serialize(to serializer: T) where T : SWBUtil.Serializer { + serializer.beginAggregate(5) + serializer.serialize(pathRef) + serializer.serialize(startLine) + serializer.serialize(endLine) + serializer.serialize(startColumn) + serializer.serialize(endColumn) + serializer.endAggregate() + } + + public init(from deserializer: any SWBUtil.Deserializer) throws { + try deserializer.beginAggregate(5) + self.pathRef = try deserializer.deserialize() + self.startLine = try deserializer.deserialize() + self.endLine = try deserializer.deserialize() + self.startColumn = try deserializer.deserialize() + self.endColumn = try deserializer.deserialize() } } @@ -411,10 +498,10 @@ private func insertCopiesOfMacroValueAssignmentNodes(_ srcAsgn: MacroValueAssign } if let srcNext = srcAsgn.next { - return MacroValueAssignment(expression: srcAsgn.expression, conditions:srcAsgn.conditions, next: insertCopiesOfMacroValueAssignmentNodes(srcNext, inFrontOf: dstAsgn)) + return MacroValueAssignment(expression: srcAsgn.expression, conditions:srcAsgn.conditions, next: insertCopiesOfMacroValueAssignmentNodes(srcNext, inFrontOf: dstAsgn), location: srcAsgn.location) } else { - return MacroValueAssignment(expression: srcAsgn.expression, conditions:srcAsgn.conditions, next: dstAsgn) + return MacroValueAssignment(expression: srcAsgn.expression, conditions:srcAsgn.conditions, next: dstAsgn, location: srcAsgn.location) } } @@ -424,3 +511,21 @@ private extension MacroValueAssignment { return (expression.isLiteral && conditions == nil) || (next?.containsUnconditionalLiteralInChain ?? false) } } + +// MARK: - Sequence Utilities + +extension MacroValueAssignment { + /// Returns a sequence that iterates through the linked list of `next` assignments starting from this node + public var sequence: some Sequence { + struct Seq: Sequence, IteratorProtocol { + var current: MacroValueAssignment? + + mutating func next() -> MacroValueAssignment? { + defer { current = current?.next } + return current + } + } + + return Seq(current: self) + } +} diff --git a/Sources/SWBTaskConstruction/TaskProducers/TaskProducer.swift b/Sources/SWBTaskConstruction/TaskProducers/TaskProducer.swift index 38501ae7..28bb01d8 100644 --- a/Sources/SWBTaskConstruction/TaskProducers/TaskProducer.swift +++ b/Sources/SWBTaskConstruction/TaskProducers/TaskProducer.swift @@ -121,6 +121,8 @@ public class TaskProducerContext: StaleFileRemovalContext, BuildFileResolution /// Whether a task planned by this producer has requested frontend command line emission. var emitFrontendCommandLines: Bool + public let moduleDependenciesContext: ModuleDependenciesContext? + private struct State: Sendable { fileprivate var onDemandResourcesAssetPacks: [ODRTagSet: ODRAssetPackInfo] = [:] fileprivate var onDemandResourcesAssetPackSubPaths: [String: Set] = [:] @@ -433,6 +435,8 @@ public class TaskProducerContext: StaleFileRemovalContext, BuildFileResolution for note in settings.notes { delegate.note(context, note) } + + self.moduleDependenciesContext = ModuleDependenciesContext(settings: settings) } /// The set of all known deployment target macro names, even if the platforms that use those settings are not installed. diff --git a/Sources/SWBTaskExecution/TaskActions/SwiftDriverTaskAction.swift b/Sources/SWBTaskExecution/TaskActions/SwiftDriverTaskAction.swift index 512673c1..6b6b5a1a 100644 --- a/Sources/SWBTaskExecution/TaskActions/SwiftDriverTaskAction.swift +++ b/Sources/SWBTaskExecution/TaskActions/SwiftDriverTaskAction.swift @@ -14,6 +14,8 @@ public import SWBCore import SWBLibc import SWBUtil import Foundation +internal import SwiftDriver +internal import SWBMacro final public class SwiftDriverTaskAction: TaskAction, BuildValueValidatingTaskAction { public override class var toolIdentifier: String { @@ -93,6 +95,20 @@ final public class SwiftDriverTaskAction: TaskAction, BuildValueValidatingTaskAc outputDelegate.emitNote(message) } + if driverPayload.explicitModulesEnabled, + let moduleDependenciesContext = payload.moduleDependenciesContext + { + let imports = try await dependencyGraph.mainModuleImportModuleDependencies(for: driverPayload.uniqueID) + let diagnostics = moduleDependenciesContext.makeDiagnostics(imports: imports) + for diagnostic in diagnostics { + outputDelegate.emit(diagnostic) + } + + if (diagnostics.contains { $0.behavior == .error }) { + return .failed + } + } + if driverPayload.reportRequiredTargetDependencies != .no && driverPayload.explicitModulesEnabled, let target = task.forTarget { let dependencyModuleNames = try await dependencyGraph.queryTransitiveDependencyModuleNames(for: driverPayload.uniqueID) for dependencyModuleName in dependencyModuleNames { diff --git a/Sources/SWBTestSupport/DummyCommandProducer.swift b/Sources/SWBTestSupport/DummyCommandProducer.swift index 021ee8fd..bdfb5e2d 100644 --- a/Sources/SWBTestSupport/DummyCommandProducer.swift +++ b/Sources/SWBTestSupport/DummyCommandProducer.swift @@ -242,4 +242,8 @@ package struct MockCommandProducer: CommandProducer, Sendable { package func lookupPlatformInfo(platform: BuildVersion.Platform) -> (any PlatformInfoProvider)? { core.lookupPlatformInfo(platform: platform) } + + package var moduleDependenciesContext: SWBCore.ModuleDependenciesContext? { + nil + } } diff --git a/Tests/SWBBuildSystemTests/DependencyValidationTests.swift b/Tests/SWBBuildSystemTests/DependencyValidationTests.swift index 3eb2afc8..bb3a1f85 100644 --- a/Tests/SWBBuildSystemTests/DependencyValidationTests.swift +++ b/Tests/SWBBuildSystemTests/DependencyValidationTests.swift @@ -15,6 +15,7 @@ import SWBTestSupport import SWBUtil import Testing import SWBProtocol +import SWBMacro @Suite fileprivate struct DependencyValidationTests: CoreBasedTests { @@ -325,4 +326,136 @@ fileprivate struct DependencyValidationTests: CoreBasedTests { } } } + + @Test(.requireSDKs(.host)) + func validateModuleDependencies() async throws { + try await withTemporaryDirectory { tmpDir in + let testWorkspace = try await TestWorkspace( + "Test", + sourceRoot: tmpDir.join("Test"), + projects: [ + TestProject( + "Project", + groupTree: TestGroup( + "Sources", + children: [ + TestFile("Swift.swift"), + TestFile("Project.xcconfig"), + ]), + buildConfigurations: [TestBuildConfiguration( + "Debug", + baseConfig: "Project.xcconfig", + buildSettings: [ + "PRODUCT_NAME": "$(TARGET_NAME)", + "CLANG_ENABLE_MODULES": "YES", + "CLANG_ENABLE_EXPLICIT_MODULES": "YES", + "SWIFT_ENABLE_EXPLICIT_MODULES": "YES", + "SWIFT_UPCOMING_FEATURE_INTERNAL_IMPORTS_BY_DEFAULT": "YES", + "SWIFT_VERSION": swiftVersion, + "DEFINES_MODULE": "YES", + "DSTROOT": tmpDir.join("dstroot").str, + "VALIDATE_MODULE_DEPENDENCIES": "YES_ERROR", + "SDKROOT": "$(HOST_PLATFORM)", + "SUPPORTED_PLATFORMS": "$(HOST_PLATFORM)", + + // Temporarily override to use the latest toolchain in CI because we depend on swift and swift-driver changes which aren't in the baseline tools yet + "TOOLCHAINS": "swift", + ])], + targets: [ + TestStandardTarget( + "TargetA", + type: .framework, + buildPhases: [ + TestSourcesBuildPhase(["Swift.swift"]), + ]), + TestStandardTarget( + "TargetB", + type: .framework, + buildPhases: [ + TestSourcesBuildPhase(["Swift.swift"]), + ]), + ]), + ]) + + let tester = try await BuildOperationTester(getCore(), testWorkspace, simulated: false) + + let swiftSourcePath = testWorkspace.sourceRoot.join("Project/Swift.swift") + try await tester.fs.writeFileContents(swiftSourcePath) { stream in + stream <<< + """ + import Foundation + """ + } + + let projectXCConfigPath = testWorkspace.sourceRoot.join("Project/Project.xcconfig") + try await tester.fs.writeFileContents(projectXCConfigPath) { stream in + stream <<< + """ + MODULE_DEPENDENCIES[target=TargetA] = Dispatch + """ + } + + let expectedDiagsByTarget: [String: [Diagnostic]] = [ + "TargetA": [ + Diagnostic( + behavior: .error, + location: Diagnostic.Location.path(projectXCConfigPath, line: 1, column: 47), + data: DiagnosticData("Missing entries in MODULE_DEPENDENCIES: Foundation"), + fixIts: [ + Diagnostic.FixIt( + sourceRange: Diagnostic.SourceRange(path: projectXCConfigPath, startLine: 1, startColumn: 47, endLine: 1, endColumn: 47), + newText: " Foundation"), + ], + childDiagnostics: [ + Diagnostic( + behavior: .error, + location: Diagnostic.Location.path(swiftSourcePath, line: 1, column: 8), + data: DiagnosticData("Missing entry in MODULE_DEPENDENCIES: Foundation"), + fixIts: [Diagnostic.FixIt( + sourceRange: Diagnostic.SourceRange(path: projectXCConfigPath, startLine: 1, startColumn: 47, endLine: 1, endColumn: 47), + newText: " Foundation")], + ), + ]), + ], + "TargetB": [ + Diagnostic( + behavior: .error, + location: Diagnostic.Location.path(projectXCConfigPath, line: 0, column: 0), + data: DiagnosticData("Missing entries in MODULE_DEPENDENCIES: Foundation"), + fixIts: [ + Diagnostic.FixIt( + sourceRange: Diagnostic.SourceRange(path: projectXCConfigPath, startLine: 0, startColumn: 0, endLine: 0, endColumn: 0), + newText: "\nMODULE_DEPENDENCIES[target=TargetB] = $(inherited) Foundation\n"), + ], + childDiagnostics: [ + Diagnostic( + behavior: .error, + location: Diagnostic.Location.path(swiftSourcePath, line: 1, column: 8), + data: DiagnosticData("Missing entry in MODULE_DEPENDENCIES: Foundation"), + fixIts: [Diagnostic.FixIt( + sourceRange: Diagnostic.SourceRange(path: projectXCConfigPath, startLine: 0, startColumn: 0, endLine: 0, endColumn: 0), + newText: "\nMODULE_DEPENDENCIES[target=TargetB] = $(inherited) Foundation\n")], + ), + ]), + ], + ] + + for (targetName, expectedDiags) in expectedDiagsByTarget { + let target = try #require(tester.workspace.projects.only?.targets.first { $0.name == targetName }) + let parameters = BuildParameters(configuration: "Debug") + let buildRequest = BuildRequest(parameters: parameters, buildTargets: [BuildRequest.BuildTargetInfo(parameters: parameters, target: target)], continueBuildingAfterErrors: false, useParallelTargets: true, useImplicitDependencies: true, useDryRun: false) + + try await tester.checkBuild(runDestination: .host, buildRequest: buildRequest, persistent: true) { results in + guard !results.checkError(.prefix("The current toolchain does not support VALIDATE_MODULE_DEPENDENCIES"), failIfNotFound: false) else { return } + + for expectedDiag in expectedDiags { + _ = results.check(.contains(expectedDiag.data.description), kind: expectedDiag.behavior, failIfNotFound: true, sourceLocation: #_sourceLocation) { diag in + #expect(expectedDiag == diag) + return true + } + } + } + } + } + } } diff --git a/Tests/SWBCoreTests/SettingsTests.swift b/Tests/SWBCoreTests/SettingsTests.swift index 47f6cf6a..06e133cc 100644 --- a/Tests/SWBCoreTests/SettingsTests.swift +++ b/Tests/SWBCoreTests/SettingsTests.swift @@ -134,6 +134,7 @@ import SWBMacro // Verify that the settings from the xcconfig were added. let XCCONFIG_USER_SETTING = try #require(settings.userNamespace.lookupMacroDeclaration("XCCONFIG_USER_SETTING")) #expect(settings.tableForTesting.lookupMacro(XCCONFIG_USER_SETTING)?.expression.stringRep == "from-xcconfig") + #expect(settings.tableForTesting.location(of: XCCONFIG_USER_SETTING) == MacroValueAssignmentLocation(path: .init("/tmp/xcconfigs/Base0.xcconfig"), startLine: 1, endLine: 1, startColumn: 24, endColumn: 38)) // Verify the user project settings. let USER_PROJECT_SETTING = try #require(settings.userNamespace.lookupMacroDeclaration("USER_PROJECT_SETTING")) @@ -4880,6 +4881,72 @@ import SWBMacro } } + @Test func targetConditionalLocation() async throws { + try await withTemporaryDirectory { (tmpDir: Path) in + let testWorkspace = TestWorkspace( + "Workspace", + sourceRoot: tmpDir.join("Test"), + projects: [TestPackageProject( + "aProject", + groupTree: TestGroup("SomeFiles", children: [ + TestFile("Project.xcconfig"), + ]), + buildConfigurations: [ + TestBuildConfiguration( + "Debug", + baseConfig: "Project.xcconfig", + buildSettings: [ + "SDKROOT": "macosx", + "OTHER_CFLAGS": "$(inherited) Project", + "OTHER_LDFLAGS[target=Target]": "$(inherited) Project", + "SUPPORTED_PLATFORMS": "$(AVAILABLE_PLATFORMS)", + "SUPPORTS_MACCATALYST": "YES", + ]) + ], + targets: [ + TestStandardTarget("Target", type: .application), + ]) + ]) + let workspace = try await testWorkspace.load(getCore()) + + let context = try await contextForTestData(workspace) + let buildRequestContext = BuildRequestContext(workspaceContext: context) + let testProject = context.workspace.projects[0] + let parameters = BuildParameters(action: .build, configuration: "Debug", activeRunDestination: .macOS) + + let projectXcconfigPath = testWorkspace.sourceRoot.join("aProject/Project.xcconfig") + try await context.fs.writeFileContents(projectXcconfigPath) { stream in + stream <<< + """ + OTHER_CFLAGS = XCConfig + OTHER_LDFLAGS[target=Target] = XCConfig + """ + } + + do { + let settings = Settings(workspaceContext: context, buildRequestContext: buildRequestContext, parameters: parameters, project: testProject, target: testProject.targets[0]) + + do { + #expect(settings.globalScope.evaluate(BuiltinMacros.OTHER_CFLAGS) == ["XCConfig", "Project"]) + let macro = settings.globalScope.table.lookupMacro(BuiltinMacros.OTHER_CFLAGS) + #expect(macro != nil) + #expect(macro?.location == nil) + #expect(macro?.next?.location == .init(path: projectXcconfigPath, startLine: 1, endLine: 1, startColumn: 15, endColumn: 24)) + #expect(macro?.next?.next == nil) + } + + do { + #expect(settings.globalScope.evaluate(BuiltinMacros.OTHER_LDFLAGS) == ["XCConfig", "Project"]) + let macro = settings.globalScope.table.lookupMacro(BuiltinMacros.OTHER_LDFLAGS) + #expect(macro != nil) + #expect(macro?.location == nil) + #expect(macro?.next?.location == .init(path: projectXcconfigPath, startLine: 2, endLine: 2, startColumn: 31, endColumn: 40)) + #expect(macro?.next?.next == nil) + } + } + } + } + @Test(.requireSDKs(.macOS, .iOS)) func platformConditionals() async throws { let testWorkspace = try await TestWorkspace( diff --git a/Tests/SWBCoreTests/TargetDependencyResolverTests.swift b/Tests/SWBCoreTests/TargetDependencyResolverTests.swift index 6fcb15df..ca0b1e04 100644 --- a/Tests/SWBCoreTests/TargetDependencyResolverTests.swift +++ b/Tests/SWBCoreTests/TargetDependencyResolverTests.swift @@ -4643,6 +4643,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 { diff --git a/Tests/SWBMacroTests/MacroParsingTests.swift b/Tests/SWBMacroTests/MacroParsingTests.swift index 7e6778b7..4c4c71cf 100644 --- a/Tests/SWBMacroTests/MacroParsingTests.swift +++ b/Tests/SWBMacroTests/MacroParsingTests.swift @@ -790,7 +790,7 @@ fileprivate let testFileData = [ } func endPreprocessorInclusion() { } - func foundMacroValueAssignment(_ macroName: String, conditions: [(param: String, pattern: String)], value: String, parser: MacroConfigFileParser) { + func foundMacroValueAssignment(_ macroName: String, conditions: [(param: String, pattern: String)], value: String, path: Path, startLine: Int, endLine: Int, startColumn: Int, endColumn: Int, parser: MacroConfigFileParser) { } func handleDiagnostic(_ diagnostic: MacroConfigFileDiagnostic, parser: MacroConfigFileParser) { @@ -804,19 +804,41 @@ fileprivate let testFileData = [ MacroConfigFileParser(byteString: "// [-Wnullability-completeness-on-arrays] \t\t\t(on) Warns about missing nullability annotations on array parameters.", path: Path(""), delegate: delegate).parse() #expect(delegate.diagnosticMessages == [String]()) } + + @Test + func parserProvidesLocationInformation() throws { + TestMacroConfigFileParser("#include \"Multiline.xcconfig\"", + expectedAssignments: [ + (macro: "FEATURE_DEFINES_A", conditions: [], value: "$(A) $(B) $(C)"), + (macro: "FEATURE_DEFINES_B", conditions: [], value: "$(D) $(E) $(F)"), + (macro: "FEATURE_DEFINES_C", conditions: [], value: "$(G) $(H)"), + (macro: "FEATURE_DEFINES_D", conditions: [], value: "$(I)") + ], + expectedDiagnostics: [], + expectedLocations: [ + (macro: "FEATURE_DEFINES_A", path: .init("Multiline.xcconfig"), startLine: 1, endLine: 2, startColumn: 20, endColumn: 37), + (macro: "FEATURE_DEFINES_B", path: .init("Multiline.xcconfig"), startLine: 3, endLine: 5, startColumn: 20, endColumn: 87), + (macro: "FEATURE_DEFINES_C", path: .init("Multiline.xcconfig"), startLine: 6, endLine: 9, startColumn: 20, endColumn: 61), + (macro: "FEATURE_DEFINES_D", path: .init("Multiline.xcconfig"), startLine: 10, endLine: 11, startColumn: 20, endColumn: 45), + ], + expectedIncludeDirectivesCount: 1 + ) + } } // We used typealiased tuples for simplicity and readability. typealias ConditionInfo = (param: String, pattern: String) typealias AssignmentInfo = (macro: String, conditions: [ConditionInfo], value: String) typealias DiagnosticInfo = (level: MacroConfigFileDiagnostic.Level, kind: MacroConfigFileDiagnostic.Kind, line: Int) +typealias LocationInfo = (macro: String, path: Path, startLine: Int, endLine: Int, startColumn: Int, endColumn: Int) -private func TestMacroConfigFileParser(_ string: String, expectedAssignments: [AssignmentInfo], expectedDiagnostics: [DiagnosticInfo], expectedIncludeDirectivesCount: Int, sourceLocation: SourceLocation = #_sourceLocation) { +private func TestMacroConfigFileParser(_ string: String, expectedAssignments: [AssignmentInfo], expectedDiagnostics: [DiagnosticInfo], expectedLocations: [LocationInfo]? = nil, expectedIncludeDirectivesCount: Int, sourceLocation: SourceLocation = #_sourceLocation) { /// We use a custom delegate to test that we’re getting the expected results, which for the sake of convenience are just kept in (name, conds:[(cond-param, cond-value)], value) tuples, i.e. conditions is an array of two-element tuples. class ConfigFileParserTestDelegate : MacroConfigFileParserDelegate { var assignments = Array() var diagnostics = Array() + var locations = Array() var includeDirectivesCount = 0 @@ -834,9 +856,10 @@ private func TestMacroConfigFileParser(_ string: String, expectedAssignments: [A func endPreprocessorInclusion() { self.includeDirectivesCount += 1 } - func foundMacroValueAssignment(_ macroName: String, conditions: [(param: String, pattern: String)], value: String, parser: MacroConfigFileParser) { + func foundMacroValueAssignment(_ macroName: String, conditions: [(param: String, pattern: String)], value: String, path: Path, startLine: Int, endLine: Int, startColumn: Int, endColumn: Int, parser: MacroConfigFileParser) { // print("\(parser.lineNumber): \(macroName)\(conditions.map({ "[\($0.param)=\($0.pattern)]" }).joinWithSeparator(""))=\(value)") assignments.append((macro: macroName, conditions: conditions, value: value)) + locations.append((macro: macroName, path: path, startLine: startLine, endLine: endLine, startColumn: startColumn, endColumn: endColumn)) } func handleDiagnostic(_ diagnostic: MacroConfigFileDiagnostic, parser: MacroConfigFileParser) { // print("\(parser.lineNumber): \(diagnostic)") @@ -857,6 +880,10 @@ private func TestMacroConfigFileParser(_ string: String, expectedAssignments: [A // Check the diagnostics that the delegate saw against the expected ones. #expect(delegate.diagnostics == expectedDiagnostics, "expected parse diagnostics \(expectedDiagnostics), but instead got \(delegate.diagnostics)", sourceLocation: sourceLocation) + if let expectedLocations { + #expect(delegate.locations == expectedLocations, "expected parse locations \(expectedLocations), but instead ogt \(delegate.locations)", sourceLocation: sourceLocation) + } + #expect(delegate.includeDirectivesCount == expectedIncludeDirectivesCount, "expected number of configs parsed to be \(expectedIncludeDirectivesCount), but instead got \(delegate.includeDirectivesCount)", sourceLocation: sourceLocation) } @@ -885,6 +912,14 @@ func ==(lhs: [DiagnosticInfo], rhs: [DiagnosticInfo]) -> Bool { return lhs.count == rhs.count && zip(lhs, rhs).filter({ return !($0.0 == $0.1) }).isEmpty } +func ==(lhs: LocationInfo, rhs: LocationInfo) -> Bool { + return (lhs.macro == rhs.macro) && (lhs.path == rhs.path) && (lhs.startLine == rhs.startLine) && (lhs.endLine == rhs.endLine) && (lhs.startColumn == rhs.startColumn) && (lhs.endColumn == rhs.endColumn) +} + +func ==(lhs: [LocationInfo], rhs: [LocationInfo]) -> Bool { + return lhs.count == rhs.count && zip(lhs, rhs).filter({ return !($0.0 == $0.1) }).isEmpty +} + /// Private helper function that parses a string representation as either a string or a string list (depending on the parameter), and checks the resulting parser delegate method call sequence and diagnostics (if applicable) against what’s expected. This is a private function that’s called by the two internal test functions TestMacroStringParsing() and TestMacroStringListParsing(). The original file name and line number are passed in so that Xcode diagnostics will refer to the call site. Each diagnostic is provided by the unit test as a tuple containing the level, kind, and associated range (expressed as start and end “distances”, in the manner of Int.Distance, into the original string). private func TestMacroParsing(_ string: String, asList: Bool, expectedCallLogEntries: [ParseDelegateCallLogEntry], expectedDiagnosticInfos: [(level: MacroExpressionDiagnostic.Level, kind: MacroExpressionDiagnostic.Kind, start: Int, end: Int)], sourceLocation: SourceLocation = #_sourceLocation) {