Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions Sources/SWBCore/CMakeLists.txt
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,7 @@ add_library(SWBCore
ConfiguredTarget.swift
Core.swift
CustomTaskTypeDescription.swift
Dependencies.swift
DependencyInfoEditPayload.swift
DependencyResolution.swift
DiagnosticSupport.swift
Expand Down
184 changes: 184 additions & 0 deletions Sources/SWBCore/Dependencies.swift
Original file line number Diff line number Diff line change
@@ -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)
}
}
}
44 changes: 44 additions & 0 deletions Sources/SWBCore/LibSwiftDriver/LibSwiftDriver.swift
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand Down Expand Up @@ -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)
}
}
2 changes: 1 addition & 1 deletion Sources/SWBCore/LinkageDependencyResolver.swift
Original file line number Diff line number Diff line change
Expand Up @@ -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])))
}
Expand Down
2 changes: 1 addition & 1 deletion Sources/SWBCore/Settings/BuiltinMacros.swift
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand Down
32 changes: 12 additions & 20 deletions Sources/SWBCore/Settings/Settings.swift
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down Expand Up @@ -896,6 +898,7 @@ public final class Settings: PlatformBuildContext, Sendable {
}

self.supportedBuildVersionPlatforms = effectiveSupportedPlatforms(sdkRegistry: sdkRegistry)
self.moduleDependencies = builder.moduleDependencies

self.constructionComponents = builder.constructionComponents
}
Expand Down Expand Up @@ -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.

Expand Down Expand Up @@ -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.
Expand Down Expand Up @@ -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"
)
}
}
}
16 changes: 11 additions & 5 deletions Sources/SWBCore/SpecImplementations/Tools/SwiftCompiler.swift
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -461,29 +463,32 @@ public struct SwiftTaskPayload: ParentTaskPayload {
case nil:
self.previewStyle = nil
}
self.moduleDependenciesContext = moduleDependenciesContext
}

public func serialize<T: Serializer>(to serializer: T) {
serializer.serializeAggregate(7) {
serializer.serializeAggregate(8) {
serializer.serialize(moduleName)
serializer.serialize(indexingPayload)
serializer.serialize(previewPayload)
serializer.serialize(localizationPayload)
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()
self.localizationPayload = try deserializer.deserialize()
self.numExpectedCompileSubtasks = try deserializer.deserialize()
self.driverPayload = try deserializer.deserialize()
self.previewStyle = try deserializer.deserialize()
self.moduleDependenciesContext = try deserializer.deserialize()
}
}

Expand Down Expand Up @@ -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,
Expand All @@ -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.
Expand Down
Loading
Loading