Files
swift-mirror/utils/swift-xcodegen/Sources/SwiftXcodeGen/Generator/ProjectGenerator.swift
Hamish Knight 38c1d28197 [xcodegen] Fix hasPrefix for paths
The intention here was to do a component-wise
prefix check, not sure why I did a string prefix
check. Switch to component prefix and rename to
`starts(with:)` to match `FilePath`.
2025-01-02 15:06:24 +00:00

860 lines
29 KiB
Swift

//===--- ProjectGenerator.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
//
//===----------------------------------------------------------------------===//
import Xcodeproj
import Darwin
extension Xcode.Reference {
fileprivate var displayName: String {
name ?? RelativePath(path).fileName
}
}
fileprivate final class ProjectGenerator {
let spec: ProjectSpec
private var project = Xcode.Project()
private let allTarget: Xcode.Target
enum CachedGroup {
/// Covered by a parent folder reference.
case covered
/// Present in the project.
case present(Xcode.Group)
}
private var groups: [RelativePath: CachedGroup] = [:]
private var files: [RelativePath: Xcode.FileReference] = [:]
private var targets: [String: Xcode.Target] = [:]
private var unbuildableSources: [ClangTarget.Source] = []
private var runnableBuildTargets: [RunnableTarget: Xcode.Target] = [:]
/// The group in which external files are stored.
private var externalsGroup: Xcode.Group {
if let _externalsGroup {
return _externalsGroup
}
let group = project.mainGroup.addGroup(
path: "", pathBase: .groupDir, name: "external"
)
_externalsGroup = group
return group
}
private var _externalsGroup: Xcode.Group?
private lazy var includeSubstitutionTarget = {
project.addTarget(name: "swift-include-substitutions")
}()
private var includeSubstitutions: Set<BuildArgs.PathSubstitution> = []
/// The main repo dir relative to the project.
private lazy var mainRepoDirInProject: RelativePath? =
spec.mainRepoDir.map { repoRelativePath.appending($0) }
private var generated: Bool = false
var name: String {
spec.name
}
var buildDir: RepoBuildDir {
spec.buildDir
}
var addSwiftDependencies: Bool {
spec.addSwiftDependencies
}
var repoPath: AbsolutePath {
buildDir.repoPath
}
var repoRelativePath: RelativePath {
buildDir.repoRelativePath
}
var projectRootDir: AbsolutePath {
buildDir.projectRootDir
}
var pathName: RelativePath {
"\(name).xcodeproj"
}
var runnableTargets: RunnableTargets {
get throws {
try spec.runnableBuildDir.runnableTargets
}
}
init(for spec: ProjectSpec) {
self.spec = spec
// Create an 'ALL' meta-target that depends on everything.
self.allTarget = project.addTarget(name: "ALL")
// Setup the project root.
self.project.mainGroup.path = projectRootDir.rawPath
self.project.mainGroup.pathBase = .absolute
self.project.buildSettings.common.PROJECT_DIR = projectRootDir.rawPath
}
/// Computes both the parent group along with the relative child path
/// for a file path relative to the project root.
private func parentGroup(
for path: RelativePath
) -> (parentGroup: Xcode.Group, childPath: RelativePath)? {
guard let parent = path.parentDir else {
// We've already handled paths under the repo, so this must be for
// paths outside the repo.
return (externalsGroup, path)
}
// We avoid adding a parent for paths above the repo, e.g we want a
// top-level 'lib', not 'swift/lib'.
if parent == repoRelativePath || parent == mainRepoDirInProject {
return (project.mainGroup, path)
}
guard let parentGroup = group(for: parent) else { return nil }
return (parentGroup, RelativePath(path.fileName))
}
/// Returns the group for a given path, or `nil` if the path is covered
/// by a parent folder reference.
private func group(for path: RelativePath) -> Xcode.Group? {
if let result = groups[path] {
switch result {
case .covered:
return nil
case .present(let g):
return g
}
}
guard
files[path] == nil, let (parentGroup, childPath) = parentGroup(for: path)
else {
groups[path] = .covered
return nil
}
let group = parentGroup.addGroup(
path: childPath.rawPath, pathBase: .groupDir, name: path.fileName
)
groups[path] = .present(group)
return group
}
private func checkNotExcluded(
_ path: RelativePath?, for description: String? = nil
) -> Bool {
guard let path else { return true }
// Not very efficient, but excludedPaths should be small in practice.
guard let excluded = spec.excludedPaths.first(
where: { path.starts(with: $0.path) }
) else {
return true
}
if let description, let reason = excluded.reason {
log.note("""
Skipping \(description) at \
'\(repoRelativePath.appending(path))'; \(reason)
""")
}
return false
}
@discardableResult
private func getOrCreateProjectRef(
_ ref: ProjectSpec.PathReference
) -> Xcode.FileReference? {
let path = ref.path
if let file = files[path] {
return file
}
assert(
projectRootDir.appending(path).exists, "File '\(path)' does not exist"
)
// If this is a folder reference, make sure we haven't already created a
// group there.
if ref.kind == .folder {
guard groups[path] == nil else {
return nil
}
}
guard let (parentGroup, childPath) = parentGroup(for: path) else {
return nil
}
let file = parentGroup.addFileReference(
path: childPath.rawPath, isDirectory: ref.kind == .folder,
pathBase: .groupDir, name: path.fileName
)
files[path] = file
return file
}
@discardableResult
private func getOrCreateRepoRef(
_ ref: ProjectSpec.PathReference
) -> Xcode.FileReference? {
let path = ref.path
guard checkNotExcluded(path) else { return nil }
return getOrCreateProjectRef(ref.withPath(repoRelativePath.appending(path)))
}
func getAllRepoSubpaths(of parent: RelativePath) throws -> [RelativePath] {
try buildDir.getAllRepoSubpaths(of: parent)
}
func generateBaseTarget(
_ name: String, at parentPath: RelativePath?, canUseBuildableFolder: Bool,
productType: Xcode.Target.ProductType?, includeInAllTarget: Bool
) -> Xcode.Target? {
let name = {
// If we have a same-named target, disambiguate.
if targets[name] == nil {
return name
}
var i = 2
var newName: String { "\(name)\(i)" }
while targets[newName] != nil {
i += 1
}
return newName
}()
var buildableFolder: Xcode.FileReference?
if let parentPath, !parentPath.components.isEmpty {
// If we've been asked to use buildable folders, see if we can create
// a folder reference at the parent path. Otherwise, create a group at
// the parent path. If we can't create either a folder or group, this is
// nested in a folder reference and there's nothing we can do.
if spec.useBuildableFolders && canUseBuildableFolder {
buildableFolder = getOrCreateRepoRef(.folder(parentPath))
}
guard buildableFolder != nil ||
group(for: repoRelativePath.appending(parentPath)) != nil else {
// If this isn't a child of an explicitly added reference, something
// has probably gone wrong.
if !spec.referencesToAdd.contains(
where: { parentPath.starts(with: $0.path) }
) {
log.warning("""
Target '\(name)' at '\(repoRelativePath.appending(parentPath))' is \
nested in a folder reference; skipping. This is likely an xcodegen bug.
""")
}
return nil
}
}
let target = project.addTarget(productType: productType, name: name)
targets[name] = target
if includeInAllTarget {
allTarget.addDependency(on: target)
}
if let buildableFolder {
target.addBuildableFolder(buildableFolder)
}
target.buildSettings.common.ONLY_ACTIVE_ARCH = "YES"
target.buildSettings.common.USE_HEADERMAP = "NO"
// The product name needs to be unique across every project we generate
// (to allow the combined workspaces to work), so add in the project name.
target.buildSettings.common.PRODUCT_NAME = "\(self.name)_\(name)"
return target
}
func replacingToolchainPath(_ str: String) -> String? {
// Replace a toolchain path with the toolchain being used by Xcode.
// TODO: Can we do better than a scan here? Could we get the old
// toolchain path from the build args?
str.scanningUTF8 { scanner in
repeat {
if scanner.tryEat(utf8: ".xctoolchain") {
return "${TOOLCHAIN_DIR}\(String(utf8: scanner.remaining))"
}
} while scanner.tryEat()
return nil
}
}
func replacingProjectDir(_ str: String) -> String {
// Replace paths within the project directory with PROJECT_DIR.
str.replacing(projectRootDir.rawPath, with: "${PROJECT_DIR}")
}
func applyBaseSubstitutions(to buildArgs: inout BuildArgs) {
buildArgs.transformValues(includeSubOptions: true) { value in
if let replacement = replacingToolchainPath(value) {
return replacement
}
return replacingProjectDir(value)
}
}
/// Checks whether a target can be represented using a buildable folder.
func canUseBuildableFolder(
at parentPath: RelativePath, sources: [RelativePath]
) throws -> Bool {
// To use a buildable folder, all child sources need to be accounted for
// in the target. If we have any stray sources not part of the target,
// attempting to use a buildable folder would incorrectly include them.
// Additionally, special targets like "Unbuildables" have an empty parent
// path, avoid buildable folders for them.
guard spec.useBuildableFolders, !parentPath.isEmpty else { return false }
let sources = Set(sources)
return try getAllRepoSubpaths(of: parentPath)
.allSatisfy { !$0.isSourceLike || sources.contains($0) }
}
/// Checks whether a given Clang target can be represented using a buildable
/// folder.
func canUseBuildableFolder(for clangTarget: ClangTarget) throws -> Bool {
// In addition to the standard checking, we also must not have any
// unbuildable sources or sources with unique arguments.
// TODO: To improve the coverage of buildable folders, we ought to start
// automatically splitting umbrella Clang targets like 'stdlib', since
// they currently always have files with unique args.
guard spec.useBuildableFolders, clangTarget.unbuildableSources.isEmpty else {
return false
}
let parent = clangTarget.parentPath
let sources = clangTarget.sources.map(\.path)
let hasConsistentArgs = try sources.allSatisfy {
try !buildDir.clangArgs.hasUniqueArgs(for: $0, parent: parent)
}
guard hasConsistentArgs else { return false }
return try canUseBuildableFolder(at: parent, sources: sources)
}
func canUseBuildableFolder(
for buildRule: SwiftTarget.BuildRule
) throws -> Bool {
guard let parentPath = buildRule.parentPath else { return false }
return try canUseBuildableFolder(
at: parentPath, sources: buildRule.sources.repoSources
)
}
func generateClangTarget(
_ targetInfo: ClangTarget, includeInAllTarget: Bool = true
) throws {
let targetPath = targetInfo.parentPath
guard checkNotExcluded(targetPath, for: "Clang target") else {
return
}
unbuildableSources += targetInfo.unbuildableSources
// Need to defer the addition of headers since the target may want to use
// a buildable folder.
defer {
for header in targetInfo.headers {
getOrCreateRepoRef(.file(header))
}
}
// If we have no sources, we're done.
if targetInfo.sources.isEmpty {
// Inform the user if the target was completely empty.
if targetInfo.headers.isEmpty && targetInfo.unbuildableSources.isEmpty {
log.note("""
Skipping '\(repoRelativePath)/\(targetPath)'; has no sources with \
build args
""")
}
return
}
let target = generateBaseTarget(
targetInfo.name, at: targetPath,
canUseBuildableFolder: try canUseBuildableFolder(for: targetInfo),
productType: .staticArchive,
includeInAllTarget: includeInAllTarget
)
guard let target else { return }
// Don't optimize or generate debug info, that will only slow down
// compilation; we don't actually care about the binary.
target.buildSettings.common.GCC_OPTIMIZATION_LEVEL = "0"
target.buildSettings.common.GCC_GENERATE_DEBUGGING_SYMBOLS = "NO"
target.buildSettings.common.GCC_WARN_64_TO_32_BIT_CONVERSION = "NO"
var libBuildArgs = try buildDir.clangArgs.getArgs(for: targetPath)
applyBaseSubstitutions(to: &libBuildArgs)
target.buildSettings.common.HEADER_SEARCH_PATHS =
libBuildArgs.takePrintedValues(for: .I)
target.buildSettings.common.CLANG_CXX_LANGUAGE_STANDARD =
libBuildArgs.takeLastValue(for: .std)
target.buildSettings.common.OTHER_CPLUSPLUSFLAGS = libBuildArgs.printedArgs
let sourcesToBuild = target.addSourcesBuildPhase()
for source in targetInfo.sources {
let sourcePath = source.path
guard let sourceRef = getOrCreateRepoRef(.file(sourcePath)) else {
continue
}
let buildFile = sourcesToBuild.addBuildFile(fileRef: sourceRef)
// Add any per-file settings.
var fileArgs = try buildDir.clangArgs.getUniqueArgs(
for: sourcePath, parent: targetPath, infer: source.inferArgs
)
if !fileArgs.isEmpty {
applyBaseSubstitutions(to: &fileArgs)
buildFile.settings.COMPILER_FLAGS = fileArgs.printed
}
}
}
/// Record path substitutions for a given target.
func recordPathSubstitutions(
for target: Xcode.Target, _ substitutions: [BuildArgs.PathSubstitution]
) {
guard !substitutions.isEmpty else { return }
includeSubstitutions.formUnion(substitutions)
target.addDependency(on: includeSubstitutionTarget)
}
/// Add the script phase to populate the substituted includes if needed.
func addSubstitutionPhaseIfNeeded() {
guard !includeSubstitutions.isEmpty else { return }
let subs = includeSubstitutions.sorted(by: \.oldPath.rawPath).map { sub in
(oldPath: replacingProjectDir(sub.oldPath.rawPath),
newPath: sub.newPath.rawPath)
}
let rsyncs = subs.map { sub in
let oldPath = sub.oldPath.escaped
let newPath = sub.newPath.escaped
return """
mkdir -p \(newPath)
rsync -aqm --delete --exclude='*.swift*' --exclude '*.o' --exclude '*.d' \
--exclude '*.dylib' --exclude '*.a' --exclude '*.cmake' --exclude '*.json' \
\(oldPath)/ \(newPath)/
"""
}.joined(separator: "\n")
let command = """
set -e
if [ -z "${SYMROOT}" ]; then
echo 'SYMROOT not defined'
exit 1
fi
\(rsyncs)
"""
includeSubstitutionTarget.addShellScriptBuildPhase(
script: command,
inputs: subs.map(\.oldPath),
outputs: subs.map(\.newPath),
alwaysRun: false
)
}
func applySubstitutions(
to buildArgs: inout BuildArgs, target: Xcode.Target, targetInfo: SwiftTarget
) {
// First force -Onone. Running optimizations only slows down build times, we
// don't actually care about the compiled binary.
buildArgs.append(.flag(.Onone))
// Exclude the experimental skipping function bodies flags, we specify
// -experimental-skip-all-function bodies for modules, and if we promote
// an emit module rule to a build rule, these would cause issues.
buildArgs.exclude(
.experimentalSkipNonInlinableFunctionBodies,
.experimentalSkipNonInlinableFunctionBodiesWithoutTypes
)
if buildArgs.hasSubOptions(for: .swiftFrontend) {
buildArgs[subOptions: .swiftFrontend].exclude(
.experimentalSkipAllFunctionBodies,
.experimentalSkipNonInlinableFunctionBodies,
.experimentalSkipNonInlinableFunctionBodiesWithoutTypes
)
}
// Then inject includes for the dependencies.
for dep in targetInfo.dependencies {
// TODO: The escaping here is easy to miss, maybe we should invest in
// a custom interpolation type to make it clearer.
buildArgs.append("-I \(getModuleDir(for: dep).escaped)")
}
// Replace references to the sdk with $SDKROOT.
if let sdk = buildArgs.takeLastValue(for: .sdk) {
buildArgs.transformValues(includeSubOptions: true) { value in
value.replacing(sdk, with: "${SDKROOT}")
}
}
buildArgs = buildArgs.map { arg in
// -enable-experimental-cxx-interop was removed as a driver option in 5.9,
// to maintain the broadest compatibility with different toolchains, use
// the frontend option.
guard arg.flag == .enableExperimentalCxxInterop else { return arg }
return .option(
.Xfrontend, spacing: .spaced, value: "\(.enableExperimentalCxxInterop)"
)
}
// Replace includes that point into the build folder since they can
// reference swiftmodules that expect a mismatched compiler. We'll
// instead point them to a directory that has the swiftmodules removed,
// and the modules will be picked up from the DerivedData products.
let subs = buildArgs.substitutePaths(
for: .I, includeSubOptions: true) { include -> RelativePath? in
// NOTE: If llvm/clang ever start having swift targets, this will need
// changing to encompass the parent. For now, avoid copying the extra
// files.
guard let suffix = include.removingPrefix(buildDir.path) else {
return nil
}
return includeSubstDirectory.appending(suffix)
}
recordPathSubstitutions(for: target, subs)
applyBaseSubstitutions(to: &buildArgs)
}
func getModuleDir(for target: SwiftTarget) -> RelativePath {
"${SYMROOT}/Modules/\(target.name)"
}
var includeSubstDirectory: RelativePath {
"${SYMROOT}/swift-includes"
}
@discardableResult
func generateSwiftTarget(
_ targetInfo: SwiftTarget, emitModuleRule: SwiftTarget.EmitModuleRule,
includeInAllTarget: Bool = true
) throws -> Xcode.Target? {
if addSwiftDependencies {
// Produce a BuildRule and generate it.
let buildRule = SwiftTarget.BuildRule(
parentPath: nil, sources: emitModuleRule.sources,
buildArgs: emitModuleRule.buildArgs
)
return try generateSwiftTarget(
targetInfo, buildRule: buildRule, includeInAllTarget: includeInAllTarget
)
}
let target = generateBaseTarget(
targetInfo.name, at: nil, canUseBuildableFolder: false, productType: nil,
includeInAllTarget: includeInAllTarget
)
guard let target else { return nil }
var buildArgs = emitModuleRule.buildArgs
for secondary in emitModuleRule.sources.externalSources {
buildArgs.append(.value(secondary.rawPath))
}
applySubstitutions(to: &buildArgs, target: target, targetInfo: targetInfo)
let targetDir = getModuleDir(for: targetInfo)
let destModule = targetDir.appending("\(targetInfo.moduleName).swiftmodule")
target.addShellScriptBuildPhase(
script: """
mkdir -p \(targetDir.escaped)
run() {
echo "$ $@"
exec "$@"
}
run xcrun swiftc -sdk "${SDKROOT}" \
-emit-module -emit-module-path \(destModule.escaped) \
-Xfrontend -experimental-skip-all-function-bodies \
\(buildArgs.printed)
""",
inputs: [],
outputs: [destModule.rawPath],
alwaysRun: true
)
return target
}
@discardableResult
func generateSwiftTarget(
_ targetInfo: SwiftTarget, buildRule: SwiftTarget.BuildRule,
includeInAllTarget: Bool = true
) throws -> Xcode.Target? {
guard checkNotExcluded(buildRule.parentPath, for: "Swift target") else {
return nil
}
// Create the target.
let target = generateBaseTarget(
targetInfo.name, at: buildRule.parentPath,
canUseBuildableFolder: try canUseBuildableFolder(for: buildRule),
productType: .staticArchive,
includeInAllTarget: includeInAllTarget
)
guard let target else { return nil }
// Explicit modules currently fails to build with:
// Invalid argument '-std=c++17' not allowed with 'Objective-C'
target.buildSettings.common.SWIFT_ENABLE_EXPLICIT_MODULES = "NO"
let buildSettings = target.buildSettings
var buildArgs = buildRule.buildArgs
applySubstitutions(to: &buildArgs, target: target, targetInfo: targetInfo)
// Follow the same logic as swift-driver and set the module name to 'main'
// if we don't have one.
let moduleName = buildArgs.takePrintedLastValue(for: .moduleName)
buildSettings.common.PRODUCT_MODULE_NAME = moduleName ?? "main"
// Emit a module if we need to.
// TODO: This currently just uses the build rule command args, should we
// diff/merge the args? Or do it separately if they differ?
if targetInfo.emitModuleRule != nil {
buildSettings.common.DEFINES_MODULE = "YES"
}
// Disable the Obj-C bridging header; we don't currently use this, and
// even if we did, we'd probably want to use the one in the Ninja build
// folder.
// This also works around a compiler crash
// (https://github.com/swiftlang/swift/issues/78190).
buildSettings.common.SWIFT_OBJC_INTERFACE_HEADER_NAME = ""
if let last = buildArgs.takeFlagGroup(.O, .Onone) {
buildSettings.common.SWIFT_OPTIMIZATION_LEVEL = last.printed
}
// Respect '-wmo' if passed.
// TODO: Should we try force batch mode where we can? Unfortunately the
// stdlib currently doesn't build with batch mode, so we'd need to special
// case it.
if buildArgs.takeFlags(.wmo, .wholeModuleOptimization) {
buildSettings.common.SWIFT_COMPILATION_MODE = "wholemodule"
}
let swiftVersion = buildArgs.takeLastValue(for: .swiftVersion)
buildSettings.common.SWIFT_VERSION = swiftVersion ?? "5.0"
if let targetStr = buildArgs.takeLastValue(for: .target),
let ver = targetStr.firstMatch(of: #/macosx?(\d+(?:\.\d+)?)/#) {
buildSettings.common.MACOSX_DEPLOYMENT_TARGET = String(ver.1)
}
// Each target gets their own product dir. Add the search paths for
// dependencies individually, so that we don't accidentally pull in a
// module we don't need (e.g swiftCore for targets that don't want the
// just-built stdlib).
let productDir = getModuleDir(for: targetInfo).rawPath
buildSettings.common.TARGET_BUILD_DIR = productDir
buildSettings.common.BUILT_PRODUCTS_DIR = productDir
buildSettings.common.SWIFT_INCLUDE_PATHS =
buildArgs.takePrintedValues(for: .I)
buildSettings.common.OTHER_SWIFT_FLAGS = buildArgs.printedArgs
// Add compile sources phase.
let sourcesToBuild = target.addSourcesBuildPhase()
for source in buildRule.sources.repoSources {
guard let sourceRef = getOrCreateRepoRef(.file(source)) else {
continue
}
sourcesToBuild.addBuildFile(fileRef: sourceRef)
}
for absSource in buildRule.sources.externalSources {
guard let source = absSource.removingPrefix(projectRootDir) else {
log.warning("""
Source file '\(absSource)' is outside the project directory; ignoring
""")
continue
}
guard let sourceRef = getOrCreateProjectRef(.file(source)) else {
continue
}
sourcesToBuild.addBuildFile(fileRef: sourceRef)
}
// Finally add any .swift.gyb files.
if let parentPath = buildRule.parentPath {
for gyb in try getAllRepoSubpaths(of: parentPath) where gyb.isSwiftGyb {
getOrCreateRepoRef(.file(gyb))
}
}
return target
}
@discardableResult
func generateSwiftTarget(
_ target: SwiftTarget, includeInAllTarget: Bool = true
) throws -> Xcode.Target? {
if let buildRule = target.buildRule {
return try generateSwiftTarget(target, buildRule: buildRule)
}
if let emitModuleRule = target.emitModuleRule {
return try generateSwiftTarget(target, emitModuleRule: emitModuleRule)
}
return nil
}
func sortGroupChildren(_ group: Xcode.Group) {
group.subitems.sort { lhs, rhs in
// The 'externals' group is special, sort it first.
if (lhs === _externalsGroup) != (rhs === _externalsGroup) {
return lhs === _externalsGroup
}
// Sort directories first.
if lhs.isDirectoryLike != rhs.isDirectoryLike {
return lhs.isDirectoryLike
}
// Then alphabetically.
return lhs.displayName.lowercased() < rhs.displayName.lowercased()
}
for case let sub as Xcode.Group in group.subitems {
sortGroupChildren(sub)
}
}
func generateIfNeeded() throws {
guard !generated else { return }
generated = true
// First add file/folder references.
for ref in spec.referencesToAdd {
getOrCreateRepoRef(ref)
}
// Gather the Swift targets to generate, including any dependencies.
var swiftTargets: Set<SwiftTarget> = []
for targetSource in spec.swiftTargetSources {
for target in try buildDir.getSwiftTargets(for: targetSource) {
swiftTargets.insert(target)
swiftTargets.formUnion(target.dependencies)
}
}
let sortedTargets = swiftTargets.sorted(by: \.name)
if !sortedTargets.isEmpty {
log.debug("---- SWIFT TARGETS TO GENERATE ----")
log.debug("\(sortedTargets.map(\.name).joined(separator: ", "))")
log.debug("-----------------------------------")
}
// Generate the Swift targets.
var generatedSwiftTargets: [SwiftTarget: Xcode.Target] = [:]
for target in sortedTargets {
generatedSwiftTargets[target] = try generateSwiftTarget(target)
}
// Wire up the dependencies.
for (targetInfo, target) in generatedSwiftTargets {
for dep in targetInfo.dependencies {
guard let depTarget = generatedSwiftTargets[dep] else { continue }
target.addDependency(on: depTarget)
}
}
// Add substitutions phase if any Swift targets need it.
addSubstitutionPhaseIfNeeded()
// Generate the Clang targets.
for targetSource in spec.clangTargetSources.sorted(by: \.name) {
let target = try buildDir.getClangTarget(
for: targetSource, knownUnbuildables: spec.knownUnbuildables
)
guard let target else { continue }
try generateClangTarget(target)
}
if !unbuildableSources.isEmpty {
let target = ClangTarget(
name: "Unbuildables",
parentPath: ".",
sources: unbuildableSources,
headers: []
)
try generateClangTarget(target, includeInAllTarget: false)
}
// Add targets for runnable targets if needed.
if spec.addRunnableTargets && spec.addBuildForRunnableTargets {
// We need to preserve PATH to find Ninja, which could e.g be in a
// homebrew prefix, which isn't in the launchd environment (and therefore
// Xcode doesn't have it).
let path = getenv("PATH").map { String(cString: $0) }
for runnable in try runnableTargets {
// TODO: Can/should we use the external build tool target kind?
let target = project.addTarget(name: "ninja-build-\(runnable.name)")
var script = ""
if let path {
script += """
export PATH="\(path)"
"""
}
script += """
ninja -C \(spec.runnableBuildDir.path.escaped) -- \
\(runnable.ninjaTargetName.escaped)
"""
target.addShellScriptBuildPhase(
script: script, inputs: [], outputs: [], alwaysRun: true
)
runnableBuildTargets[runnable] = target
}
}
// Sort the groups.
sortGroupChildren(project.mainGroup)
}
func generateAndWrite(
into outputDir: AbsolutePath
) throws -> GeneratedProject {
try generateIfNeeded()
let projDir = outputDir.appending(pathName)
let projDataPath = projDir.appending("project.pbxproj")
try projDataPath.write(project.generatePlist().serialize())
log.info("Generated '\(projDataPath)'")
// Add the ALL meta-target as a scheme (we use a suffix to disambiguate it
// from the ALL workspace scheme we generate).
let allBuildTargets = [Scheme.BuildTarget(allTarget, in: pathName)]
var schemes = SchemeGenerator(in: projDir)
schemes.add(Scheme(
"ALL-\(name)", replaceExisting: true, buildTargets: allBuildTargets
))
// Add schemes for runnable targets.
if spec.addRunnableTargets {
for runnable in try runnableTargets {
// Avoid replacing an existing scheme if it exists.
// FIXME: Really we ought to be reading in the existing scheme, and
// updating any values that need changing.
var scheme = Scheme(runnable.name, replaceExisting: false)
if let target = runnableBuildTargets[runnable] {
scheme.buildAction.targets.append(.init(target, in: pathName))
}
// FIXME: Because we can't update an existing scheme, use a symlink to
// refer to the run destination, allowing us to change it if needed.
let link = projDir.appending(
".swift-xcodegen/runnable/\(runnable.name)"
)
try link.symlink(to: runnable.path)
scheme.runAction = .init(path: link)
schemes.add(scheme)
}
}
try schemes.write()
return GeneratedProject(at: projDir, allBuildTargets: allBuildTargets)
}
}
extension ProjectSpec {
public func generateAndWrite(
into outputDir: AbsolutePath
) throws -> GeneratedProject {
let generator = ProjectGenerator(for: self)
return try generator.generateAndWrite(into: outputDir)
}
}