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

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 2 additions & 2 deletions Plugins/launch-xcode/launch-xcode.swift
Original file line number Diff line number Diff line change
Expand Up @@ -40,8 +40,8 @@ struct LaunchXcode: CommandPlugin {

print("Launching Xcode...")
let process = Process()
process.executableURL = URL(fileURLWithPath: "/usr/bin/open")
process.arguments = ["-n", "-F", "-W", "--env", "XCBBUILDSERVICE_PATH=\(buildServiceURL.path())", "-b", "com.apple.dt.Xcode"]
process.executableURL = URL(fileURLWithPath: "/bin/sh")
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Not sure about this one.xcode-select -p does not necessarily return an Xcode instance, it could be Developer Command Line Tools, in which case this will just try to open /Developer and fail.

Could you run xcode-select -p in a separate Process() invocation, collect its output, and call open with that path if the return value ends in .app, otherwise fall back to -b com.apple.dt.Xcode?

Also suggest splitting into another PR so that concerns with that don't block your primary changes.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Removing this for now

process.arguments = ["-c", "open -n -F -W --env 'XCBBUILDSERVICE_PATH=\(buildServiceURL.path(percentEncoded: false))' $(xcode-select -p)/../.."]
process.standardOutput = nil
process.standardError = nil
try await process.run()
Expand Down
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
183 changes: 183 additions & 0 deletions Sources/SWBCore/Dependencies.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,183 @@
//===----------------------------------------------------------------------===//
//
// 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(name: String, accessLevel: AccessLevel) {
self.name = name
self.accessLevel = accessLevel
}

public init(entry: String) throws {
let components = entry.split(separator: " ")
Copy link
Collaborator

@jakepetroules jakepetroules Jun 27, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I suggest using Swift regex for this. Makes it much easier to read and understand the code flow (and no force unwraps that risk crashing if this becomes more complex in future).

guard let match = try #/(?<accessLevel>.+)? (?<moduleName>)/#.wholeMatch(in: entry)?.output else {
    throw StubError.error("expected 1 or 2 space-separated components in: \(entry)")
}

self.accessLevel = try match.accessLevel.map { accessLevel in
    if let a = AccessLevel(rawValue: String(accessLevel)) {
        return a
    } else {
        throw StubError.error("unexpected access modifier '\(accessLevel)', expected one of: \(AccessLevel.allCases.map { $0.rawValue }.joined(separator: ", "))")
    }
} ?? .Private
self.name = String(match.moduleName)

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I would like a better way to define this syntax instead of "manually" splitting like this, but I'm not sure about Swift Regex here because of https://forums.swift.org/t/should-regex-be-sendable/69529 (you can't construct the regex literal once and reuse it if it isn't Sendable). I think my preference is to keep it simple until it gets more complicated and then re-evaluate. WDYT?

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fair enough but then I'd at least consider doing this:

let components = entry.split(separator: " ")
switch components.count {
case 1:
    accessLevel = .Private
    self.name = components[0]
case 2:
    accessLevel = try AccessLevel(string: components[0])
    self.name = components[1]
default:
    throw StubError.error("expected 1 or 2 space-separated components in: \(entry)")
}

(elsewhere)

fileprivate extension AccessLevel {
    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
    }
}

The main point being to localize checking in a way that makes it as obvious as possible at a glance that it's correct (and avoiding a force unwrap).

guard (1...2).contains(components.count) else {
throw StubError.error("expected 1 or 2 space-separated components in: \(entry)")
}

let accessLevel: AccessLevel
if components.count > 1 {
if let a = AccessLevel(rawValue: String(components[0])) {
accessLevel = a
}
else {
throw StubError.error("unexpected access modifier '\(components[0])', expected one of: \(AccessLevel.allCases.map { $0.rawValue }.joined(separator: ", "))")
}
}
else {
accessLevel = .Private
}

self.name = String(components.last!)
self.accessLevel = accessLevel
}

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"
)
}
}
}
Loading
Loading