Files
swift-mirror/utils/swift-xcodegen/Sources/SwiftXcodeGen/BuildArgs/SwiftTargets.swift
Hamish Knight 27ec6e3a45 [xcodegen] realpath Swift target sources
Make sure we call `realpath` for the Swift target sources read from
the build settings to ensure we canonicalize their casing.
2025-06-15 20:29:17 +01:00

261 lines
8.7 KiB
Swift

//===--- SwiftTargets.swift -----------------------------------------------===//
//
// This source file is part of the Swift.org open source project
//
// Copyright (c) 2024 Apple Inc. and the Swift project authors
// Licensed under Apache License v2.0 with Runtime Library Exception
//
// See https://swift.org/LICENSE.txt for license information
// See https://swift.org/CONTRIBUTORS.txt for the list of Swift project authors
//
//===----------------------------------------------------------------------===//
struct SwiftTargets {
private var targets: [SwiftTarget] = []
private var outputAliases: [String: [String]] = [:]
private var dependenciesByTargetName: [String: Set<String>] = [:]
private var targetsByName: [String: SwiftTarget] = [:]
private var targetsByOutput: [String: SwiftTarget] = [:]
// Track some state for debugging
private var debugLogUnknownFlags: Set<String> = []
init(for buildDir: RepoBuildDir) throws {
log.debug("[*] Reading Swift targets from build.ninja")
for rule in try buildDir.ninjaFile.buildEdges {
try tryAddTarget(for: rule, buildDir: buildDir)
}
targets.sort(by: { $0.name < $1.name })
log.debug("-------- SWIFT TARGET DEPS --------")
for target in targets {
var deps: Set<SwiftTarget> = []
for dep in dependenciesByTargetName[target.name] ?? [] {
for output in allOutputs(for: dep) {
guard let depTarget = targetsByOutput[output] else { continue }
deps.insert(depTarget)
}
}
target.dependencies = deps.sorted(by: \.name)
log.debug("| '\(target.name)' has deps: \(target.dependencies)")
}
log.debug("-----------------------------------")
if !debugLogUnknownFlags.isEmpty {
log.debug("---------- UNKNOWN FLAGS ----------")
for flag in debugLogUnknownFlags.sorted() {
log.debug("| \(flag)")
}
log.debug("-----------------------------------")
}
}
private func allOutputs(for output: String) -> Set<String> {
// TODO: Should we attempt to do canonicalization instead?
var stack: [String] = [output]
var results: Set<String> = []
while let last = stack.popLast() {
guard results.insert(last).inserted else { continue }
for alias in outputAliases[last] ?? [] {
stack.append(alias)
}
}
return results
}
private mutating func computeBuildArgs(
for edge: NinjaBuildFile.BuildEdge,
in ninja: NinjaBuildFile
) throws -> BuildArgs? {
let commandLine = try ninja.commandLine(for: edge)
let command = try CommandParser.parseKnownCommandOnly(commandLine)
guard let command, command.executable.knownCommand == .swiftc else {
return nil
}
var buildArgs = BuildArgs(for: .swiftc, args: command.args)
// Only include known flags for now.
buildArgs = buildArgs.filter { arg in
if arg.flag != nil {
return true
}
if log.logLevel <= .debug {
// Note the unknown flags.
guard let value = arg.value, value.hasPrefix("-") else { return false }
debugLogUnknownFlags.insert(value)
}
return false
}
return buildArgs
}
/// Check to see if this is a forced-XXX-dep.swift file, which is only used
/// to hack around CMake dependencies, and can be dropped.
private func isForcedDepSwiftFile(_ path: AbsolutePath) -> Bool {
path.fileName.scanningUTF8 { scanner in
guard scanner.tryEat(utf8: "forced-") else {
return false
}
while scanner.tryEat() {
if scanner.tryEat(utf8: "-dep.swift"), !scanner.hasInput {
return true
}
}
return false
}
}
func getSources(
from edge: NinjaBuildFile.BuildEdge, buildDir: RepoBuildDir
) throws -> SwiftTarget.Sources {
let files: [AnyPath] = edge.inputs.map(AnyPath.init)
// Split the files into repo sources and external sources. Repo sources
// are those under the repo path, external sources are outside that path,
// and are either for dependencies such as swift-syntax, or are generated
// from e.g de-gyb'ing.
var sources = SwiftTarget.Sources()
for input in files where input.language == .swift {
switch input {
case .relative(let r):
// A relative path is for a file in the build directory, it's external.
let abs = buildDir.path.appending(r)
guard abs.exists else { continue }
sources.externalSources.append(abs.realPath)
case .absolute(let a):
guard a.exists else { continue }
// Symlinks shouldn't really be a concern here, but we need to realpath
// in order to canonicalize the casing.
let a = a.realPath
guard let rel = a.removingPrefix(buildDir.repoPath) else {
sources.externalSources.append(a)
continue
}
sources.repoSources.append(rel)
}
}
// Avoid adding forced dependency files.
sources.externalSources = sources.externalSources
.filter { !isForcedDepSwiftFile($0) }
return sources
}
private mutating func tryAddTarget(
for edge: NinjaBuildFile.BuildEdge,
buildDir: RepoBuildDir
) throws {
// Phonies are only used to track aliases.
if edge.isPhony {
for output in edge.outputs {
outputAliases[output, default: []] += edge.inputs
}
return
}
// Ignore build rules that don't have object file or swiftmodule outputs.
let forBuild = edge.outputs.contains(
where: { $0.hasExtension(.o) }
)
let forModule = edge.outputs.contains(
where: { $0.hasExtension(.swiftmodule) }
)
guard forBuild || forModule else {
return
}
let primaryOutput = edge.outputs.first!
let sources = try getSources(from: edge, buildDir: buildDir)
let repoSources = sources.repoSources
let externalSources = sources.externalSources
// Is this for a build (producing a '.o'), we need to have at least one
// repo source. Module dependencies can use external sources.
guard !repoSources.isEmpty || (forModule && !externalSources.isEmpty) else {
return
}
guard let buildArgs = try computeBuildArgs(for: edge, in: buildDir.ninjaFile) else { return }
// Pick up the module name from the arguments, or use an explicitly
// specified module name if we have one. The latter might be invalid so
// may not be part of the build args (e.g 'swift-plugin-server'), but is
// fine for generation.
let moduleName = buildArgs.lastValue(for: .moduleName) ?? edge.bindings[.swiftModuleName]
guard let moduleName else {
log.debug("! Skipping Swift target with output \(primaryOutput); no module name")
return
}
let moduleLinkName = buildArgs.lastValue(for: .moduleLinkName) ?? edge.bindings[.swiftLibraryName]
let name = moduleLinkName ?? moduleName
// Add the dependencies. We track dependencies for any input files, along
// with any recorded swiftmodule dependencies.
dependenciesByTargetName.withValue(for: name, default: []) { deps in
deps.formUnion(
edge.inputs.filter {
$0.hasExtension(.swiftmodule) || $0.hasExtension(.o)
}
)
deps.formUnion(
edge.dependencies.filter { $0.hasExtension(.swiftmodule) }
)
}
var buildRule: SwiftTarget.BuildRule?
var emitModuleRule: SwiftTarget.EmitModuleRule?
if forBuild && !repoSources.isEmpty {
// We've already ensured that `repoSources` is non-empty.
buildRule = .init(
parentPath: repoSources.commonAncestor!, sources: sources,
buildArgs: buildArgs
)
}
if forModule {
emitModuleRule = .init(sources: sources, buildArgs: buildArgs)
}
let target = targetsByName[name] ?? {
log.debug("+ Discovered Swift target '\(name)' with output '\(primaryOutput)'")
let target = SwiftTarget(name: name, moduleName: moduleName)
targetsByName[name] = target
targets.append(target)
return target
}()
for output in edge.outputs {
targetsByOutput[output] = target
}
if buildRule == nil || target.buildRule == nil {
if let buildRule {
target.buildRule = buildRule
}
} else {
log.debug("""
! Skipping '\(name)' build rule for \
'\(primaryOutput)'; already added
""")
}
if emitModuleRule == nil || target.emitModuleRule == nil {
if let emitModuleRule {
target.emitModuleRule = emitModuleRule
}
} else {
log.debug("""
! Skipping '\(name)' emit module rule for \
'\(primaryOutput)'; already added
""")
}
}
func getTargets(below path: RelativePath) -> [SwiftTarget] {
targets.filter { target in
guard let parent = target.buildRule?.parentPath, parent.starts(with: path)
else {
return false
}
return true
}
}
}