Files
Hamish Knight b5d4c4c46d [utils] Move swift-xcodegen to swift-dev-utils
The goal here is for `swift-dev-utils` to be a monolithic package
for development utils written in Swift.
2026-01-18 19:25:51 +00:00

434 lines
14 KiB
Swift

//===--- SwiftXcodegen.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 ArgumentParser
import Foundation
import SwiftXcodeGen
@main
@dynamicMemberLookup
struct SwiftXcodegen: AsyncParsableCommand, Sendable {
// MARK: Options
@OptionGroup(title: "LLVM Projects")
var llvmProjectOpts: LLVMProjectOptions
subscript<T>(dynamicMember kp: KeyPath<LLVMProjectOptions, T>) -> T {
llvmProjectOpts[keyPath: kp]
}
@OptionGroup(title: "Swift targets")
var swiftTargetOpts: SwiftTargetOptions
subscript<T>(dynamicMember kp: KeyPath<SwiftTargetOptions, T>) -> T {
swiftTargetOpts[keyPath: kp]
}
@OptionGroup(title: "Runnable targets")
var runnableTargetOptions: RunnableTargetOptions
subscript<T>(dynamicMember kp: KeyPath<RunnableTargetOptions, T>) -> T {
runnableTargetOptions[keyPath: kp]
}
@OptionGroup(title: "Project configuration")
var projectOpts: ProjectOptions
subscript<T>(dynamicMember kp: KeyPath<ProjectOptions, T>) -> T {
projectOpts[keyPath: kp]
}
@OptionGroup(title: "Misc")
var miscOptions: MiscOptions
subscript<T>(dynamicMember kp: KeyPath<MiscOptions, T>) -> T {
miscOptions[keyPath: kp]
}
@Argument(help: "The path to the Ninja build directory to generate for")
var buildDir: AnyPath
// MARK: Command
private func newProjectSpec(
_ name: String, for buildDir: RepoBuildDir,
runnableBuildDir: RepoBuildDir? = nil,
mainRepoDir: RelativePath? = nil
) -> ProjectSpec {
ProjectSpec(
name, for: buildDir, runnableBuildDir: runnableBuildDir ?? buildDir,
addClangTargets: self.addClangTargets,
addSwiftTargets: self.addSwiftTargets,
addSwiftDependencies: self.addSwiftDependencies,
addRunnableTargets: false,
addBuildForRunnableTargets: self.addBuildForRunnableTargets,
inferArgs: self.inferArgs, preferFolderRefs: self.preferFolderRefs,
useBuildableFolders: self.useBuildableFolders, mainRepoDir: mainRepoDir
)
}
@discardableResult
func writeSwiftXcodeProject(
for ninja: NinjaBuildDir, into outputDir: AbsolutePath
) throws -> GeneratedProject {
let buildDir = try ninja.buildDir(for: .swift)
// Check to see if we have a separate runnable build dir.
let runnableBuildDirPath =
self.runnableBuildDir?.absoluteInWorkingDir.realPath
let runnableBuildDir = try runnableBuildDirPath.map {
try NinjaBuildDir(at: $0, projectRootDir: ninja.projectRootDir)
.buildDir(for: .swift)
}
var spec = newProjectSpec(
"Swift", for: buildDir, runnableBuildDir: runnableBuildDir
)
if self.addDocs {
spec.addTopLevelDocs()
spec.addDocsGroup(at: "docs")
spec.addDocsGroup(at: "userdocs")
}
spec.addHeaders(in: "include")
if self.addCompilerLibs {
spec.addClangTargets(below: "lib", addingPrefix: "swift")
spec.addClangTarget(at: "SwiftCompilerSources")
spec.addSwiftTargets(below: "lib")
spec.addSwiftTargets(below: "SwiftCompilerSources")
}
if self.addCompilerTools {
spec.addClangTargets(below: "tools")
spec.addSwiftTargets(below: "tools")
}
if self.addStdlibCxx || self.addStdlibSwift {
// These are headers copied from LLVM, avoid including them in the project
// to avoid confusion.
spec.addExcludedPath("stdlib/include/llvm")
}
if self.addStdlibCxx {
// This doesn't build with Clang 15, it does build with ToT Clang though.
spec.addUnbuildableFile(
"stdlib/tools/swift-reflection-test/swift-reflection-test.c"
)
// Add a single target for all the C/C++ files in the stdlib. We may have
// unbuildable files, which will be added to the Unbuildables target.
spec.addClangTarget(at: "stdlib", mayHaveUnbuildableFiles: true)
}
if self.addStdlibSwift {
// Add any Swift targets in the stdlib.
spec.addSwiftTargets(below: "stdlib")
}
if self.addUnitTests {
// Create a single 'unittests' target.
spec.addClangTarget(at: "unittests")
}
if self.addTestFolders {
spec.addReference(to: "test")
spec.addReference(to: "validation-test")
}
for blueFolder in self.blueFolders.components(separatedBy: ",")
where !blueFolder.isEmpty {
spec.addReference(to: RelativePath(blueFolder))
}
// Only enable runnable targets for Swift for now.
if self.addRunnableTargets {
spec.addRunnableTargets = true
// If we don't have debug info, warn.
if let config = try spec.runnableBuildDir.buildConfiguration,
!config.hasDebugInfo {
log.warning("""
Specified build directory '\(spec.runnableBuildDir.path)' does not \
have debug info; runnable targets will not be debuggable with LLDB. \
Either build with debug info enabled, or specify a separate debug \
build directory with '--runnable-build-dir'. Runnable targets may be \
disabled by passing '--no-runnable-targets'.
""")
}
}
return try spec.generateAndWrite(into: outputDir)
}
func writeSwiftRuntimesXcodeProject(
for ninja: NinjaBuildDir, into outputDir: AbsolutePath
) throws -> GeneratedProject {
let buildDir = try ninja.buildDir(for: .swiftRuntimes)
var spec = newProjectSpec("SwiftRuntimes", for: buildDir)
spec.addClangTarget(at: "core", mayHaveUnbuildableFiles: true)
spec.addSwiftTargets(below: "core")
if self.addDocs {
spec.addTopLevelDocs()
}
return try spec.generateAndWrite(into: outputDir)
}
@discardableResult
func writeClangXcodeProject(
for ninja: NinjaBuildDir, into outputDir: AbsolutePath
) throws -> GeneratedProject {
var spec = newProjectSpec(
"Clang", for: try ninja.buildDir(for: .llvm), mainRepoDir: "clang"
)
if self.addDocs {
spec.addTopLevelDocs()
spec.addDocsGroup(at: "docs")
}
spec.addHeaders(in: "include")
if self.addCompilerLibs {
spec.addClangTargets(below: "lib", addingPrefix: "clang")
}
if self.addCompilerTools {
spec.addClangTargets(below: "tools")
if self.addClangToolsExtra {
spec.addClangTargets(
below: "../clang-tools-extra", addingPrefix: "extra-",
mayHaveUnbuildableFiles: true, excluding: ["test"]
)
if self.addTestFolders {
spec.addReference(to: "../clang-tools-extra/test")
} else {
// Avoid adding any headers present in the test folder.
spec.addExcludedPath("../clang-tools-extra/test")
}
}
}
if self.addUnitTests {
spec.addClangTarget(at: "unittests")
}
if self.addTestFolders {
spec.addReference(to: "test")
}
return try spec.generateAndWrite(into: outputDir)
}
@discardableResult
func writeLLDBXcodeProject(
for ninja: NinjaBuildDir, into outputDir: AbsolutePath
) throws -> GeneratedProject {
var spec = newProjectSpec("LLDB", for: try ninja.buildDir(for: .lldb))
if self.addDocs {
spec.addTopLevelDocs()
spec.addDocsGroup(at: "docs")
}
spec.addHeaders(in: "include")
if self.addCompilerLibs {
spec.addClangTargets(below: "source", addingPrefix: "lldb")
}
if self.addCompilerTools {
spec.addClangTargets(below: "tools")
}
if self.addUnitTests {
spec.addClangTarget(at: "unittests")
}
if self.addTestFolders {
spec.addReference(to: "test")
}
return try spec.generateAndWrite(into: outputDir)
}
@discardableResult
func writeLLVMXcodeProject(
for ninja: NinjaBuildDir, into outputDir: AbsolutePath
) throws -> GeneratedProject {
var spec = newProjectSpec(
"LLVM", for: try ninja.buildDir(for: .llvm), mainRepoDir: "llvm"
)
if self.addDocs {
spec.addTopLevelDocs()
spec.addDocsGroup(at: "docs")
}
spec.addHeaders(in: "include")
if self.addCompilerLibs {
spec.addClangTargets(below: "lib", addingPrefix: "llvm")
}
if self.addCompilerTools {
spec.addClangTargets(below: "tools")
}
if self.addTestFolders {
spec.addReference(to: "test")
}
// FIXME: Looks like compiler-rt has its own build directory
// llvm-macosx-arm64/tools/clang/runtime/compiler-rt-bins/build.ninja
if self.addCompilerRT {
spec.addClangTargets(
below: "../compiler-rt", addingPrefix: "extra-"
)
if self.addTestFolders {
spec.addReference(to: "../compiler-rt/test")
} else {
// Avoid adding any headers present in the test folder.
spec.addExcludedPath("../compiler-rt/test")
}
}
return try spec.generateAndWrite(into: outputDir)
}
func getWorkspace(for proj: GeneratedProject) throws -> WorkspaceGenerator {
var generator = WorkspaceGenerator()
generator.addProject(proj)
return generator
}
func runTask<R>(
_ body: @escaping @Sendable () throws -> R
) async throws -> Task<R, any Error> {
let task = Task(operation: body)
if !self.parallel {
_ = try await task.value
}
return task
}
func showCaveatsIfNeeded() {
guard log.logLevel <= .note else { return }
var notes: [String] = []
if !projectOpts.addStdlibSwift {
notes.append("""
- Swift standard library targets are disabled by default since they require
using a development snapshot of Swift with Xcode. You can pass '--stdlib-swift'
to enable. See the '--help' entry for more info.
""")
}
guard !notes.isEmpty else { return }
log.note("Caveats:")
for note in notes {
for line in note.components(separatedBy: .newlines) {
log.note(line)
}
}
}
func generate() async throws {
let buildDirPath = buildDir.absoluteInWorkingDir.realPath
log.info("Generating project for '\(buildDirPath)'...")
let projectRootDir = self.projectRootDir?.absoluteInWorkingDir
let buildDir = try NinjaBuildDir(at: buildDirPath, projectRootDir: projectRootDir)
let outputDir = miscOptions.outputDir?.absoluteInWorkingDir ?? buildDir.projectRootDir
let swiftProj = try await runTask {
try writeSwiftXcodeProject(for: buildDir, into: outputDir)
}
let runtimesProj = try await runTask { () -> GeneratedProject? in
guard let runtimesBuildDir = self.runtimesBuildDir?.absoluteInWorkingDir else {
return nil
}
let buildDir = try NinjaBuildDir(
at: runtimesBuildDir, projectRootDir: projectRootDir
)
return try writeSwiftRuntimesXcodeProject(for: buildDir, into: outputDir)
}
let llvmProj = try await runTask {
self.addLLVM ? try writeLLVMXcodeProject(for: buildDir, into: outputDir) : nil
}
let clangProj = try await runTask {
self.addClang ? try writeClangXcodeProject(for: buildDir, into: outputDir) : nil
}
let lldbProj = try await runTask {
self.addLLDB ? try writeLLDBXcodeProject(for: buildDir, into: outputDir) : nil
}
var swiftWorkspace = try await getWorkspace(for: swiftProj.value)
if let runtimesProj = try await runtimesProj.value {
swiftWorkspace.addProject(runtimesProj)
try swiftWorkspace.write("Swift+Runtimes", into: outputDir)
}
if let llvmProj = try await llvmProj.value {
var swiftLLVMWorkspace = swiftWorkspace
swiftLLVMWorkspace.addProject(llvmProj)
try swiftLLVMWorkspace.write("Swift+LLVM", into: outputDir)
}
if let clangProj = try await clangProj.value,
let llvmProj = try await llvmProj.value {
var clangLLVMWorkspace = WorkspaceGenerator()
clangLLVMWorkspace.addProject(clangProj)
clangLLVMWorkspace.addProject(llvmProj)
try clangLLVMWorkspace.write("Clang+LLVM", into: outputDir)
var allWorkspace = swiftWorkspace
allWorkspace.addProject(clangProj)
allWorkspace.addProject(llvmProj)
try allWorkspace.write("Swift+Clang+LLVM", into: outputDir)
}
if let lldbProj = try await lldbProj.value {
var swiftLLDBWorkspace = swiftWorkspace
swiftLLDBWorkspace.addProject(lldbProj)
try swiftLLDBWorkspace.write("Swift+LLDB", into: outputDir)
if let llvmProj = try await llvmProj.value {
var lldbLLVMWorkspace = WorkspaceGenerator()
lldbLLVMWorkspace.addProject(lldbProj)
lldbLLVMWorkspace.addProject(llvmProj)
try lldbLLVMWorkspace.write("LLDB+LLVM", into: outputDir)
}
}
}
func printingTimeTaken<T>(_ fn: () async throws -> T) async rethrows -> T {
let start = ContinuousClock.now
let result = try await fn()
let end = ContinuousClock.now
let duration = start.duration(to: end)
// Note we don't print the time taken when we fail.
var message = "Successfully generated in "
message += duration.formatted(
.units(
allowed: [.seconds],
width: .narrow,
fractionalPart: .init(lengthLimits: 0...3, roundingRule: .up)
)
)
log.info(message)
return result
}
func run() async {
// Set the log level
log.logLevel = .init(self.logLevel ?? (self.quiet ? .warning : .info))
do {
try await printingTimeTaken {
try await generate()
}
showCaveatsIfNeeded()
} catch {
log.error("\(error)")
}
if log.hadError {
Darwin.exit(1)
}
}
}