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 3f963cb9..c875d9da 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 f9c87139..e5b9fcd0 100644 --- a/Sources/SWBCore/LinkageDependencyResolver.swift +++ b/Sources/SWBCore/LinkageDependencyResolver.swift @@ -375,7 +375,7 @@ actor LinkageDependencyResolver { buildRequestContext.getCachedSettings($0.parameters, target: $0.target).globalScope.evaluate(BuiltinMacros.PRODUCT_MODULE_NAME) }) - for moduleDependencyName in configuredTargetSettings.moduleDependencies.map { $0.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]))) } diff --git a/Sources/SWBCore/Settings/BuiltinMacros.swift b/Sources/SWBCore/Settings/BuiltinMacros.swift index 618b3ca5..67584152 100644 --- a/Sources/SWBCore/Settings/BuiltinMacros.swift +++ b/Sources/SWBCore/Settings/BuiltinMacros.swift @@ -2631,7 +2631,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 c0080e91..047ae6c6 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) @@ -896,6 +898,7 @@ public final class Settings: PlatformBuildContext, Sendable { } self.supportedBuildVersionPlatforms = effectiveSupportedPlatforms(sdkRegistry: sdkRegistry) + self.moduleDependencies = builder.moduleDependencies self.constructionComponents = builder.constructionComponents } @@ -1281,6 +1284,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. @@ -1615,6 +1620,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. @@ -5335,23 +5347,3 @@ 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/SpecImplementations/Tools/SwiftCompiler.swift b/Sources/SWBCore/SpecImplementations/Tools/SwiftCompiler.swift index 133a215b..bfd6b408 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() } } @@ -2289,7 +2294,6 @@ public final class SwiftCompilerSpec : CompilerSpec, SpecIdentifierType, SwiftDi ] } - // BUILT_PRODUCTS_DIR here is guaranteed to be absolute by `getCommonTargetTaskOverrides`. let payload = SwiftTaskPayload( moduleName: moduleName, @@ -2306,7 +2310,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/TaskGeneration.swift b/Sources/SWBCore/TaskGeneration.swift index fc475ed2..82b871d7 100644 --- a/Sources/SWBCore/TaskGeneration.swift +++ b/Sources/SWBCore/TaskGeneration.swift @@ -269,6 +269,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/MacroValueAssignmentTable.swift b/Sources/SWBMacro/MacroValueAssignmentTable.swift index 7eb83402..cb1204b6 100644 --- a/Sources/SWBMacro/MacroValueAssignmentTable.swift +++ b/Sources/SWBMacro/MacroValueAssignmentTable.swift @@ -183,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. @@ -191,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) @@ -333,7 +333,7 @@ public final class MacroValueAssignment: Serializable, CustomStringConvertible, private let _location: InternedMacroValueAssignmentLocation? private static let macroConfigPaths = SWBMutex>(OrderedSet()) - var location: MacroValueAssignmentLocation? { + public var location: MacroValueAssignmentLocation? { if let _location { return .init( path: Self.macroConfigPaths.withLock { $0[_location.pathRef] }, @@ -511,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 01cda675..28473edd 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 83a7e342..a208163b 100644 --- a/Sources/SWBTestSupport/DummyCommandProducer.swift +++ b/Sources/SWBTestSupport/DummyCommandProducer.swift @@ -238,4 +238,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 b32b506f..f8c4e797 100644 --- a/Tests/SWBCoreTests/SettingsTests.swift +++ b/Tests/SWBCoreTests/SettingsTests.swift @@ -4881,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(