Introduce swift-xcodegen

This is a tool specifically designed to generate
Xcode projects for the Swift repo (as well as a
couple of adjacent repos such as LLVM and Clang).
It aims to provide a much more user-friendly experience
than the CMake Xcode generation (`build-script --xcode`).
This commit is contained in:
Hamish Knight
2024-11-05 21:11:42 +00:00
parent 3efa770c86
commit 03d8ea5248
63 changed files with 8307 additions and 60 deletions

2
.github/CODEOWNERS vendored
View File

@@ -261,6 +261,7 @@
# utils
/utils/*windows* @compnerd
/utils/generate-xcode @hamishknight
/utils/gyb_sourcekit_support/ @ahoppen @bnbarham @hamishknight @rintaro
/utils/sourcekit_fuzzer/ @ahoppen @bnbarham @hamishknight @rintaro
/utils/swift_build_support/products/earlyswiftsyntax.py @ahoppen @bnbarham @hamishknight @rintaro
@@ -268,6 +269,7 @@
/utils/swift_build_support/products/sourcekitlsp.py @ahoppen @bnbarham @hamishknight @rintaro
/utils/swift_build_support/products/swiftformat.py @ahoppen @allevato @bnbarham @hamishknight @rintaro
/utils/swift_build_support/products/swiftsyntax.py @ahoppen @bnbarham @hamishknight @rintaro
/utils/swift-xcodegen/ @hamishknight
/utils/update-checkout* @shahmishal
/utils/update_checkout/ @shahmishal
/utils/vim/ @compnerd

View File

@@ -26,7 +26,6 @@ toolchain as a one-off, there are a couple of differences:
- [Setting up your fork](#setting-up-your-fork)
- [Using Ninja with Xcode](#using-ninja-with-xcode)
- [Regenerating the Xcode project](#regenerating-the-xcode-project)
- [Troubleshooting editing issues in Xcode](#troubleshooting-editing-issues-in-xcode)
- [Other IDEs setup](#other-ides-setup)
- [Editing](#editing)
- [Incremental builds with Ninja](#incremental-builds-with-ninja)
@@ -367,55 +366,21 @@ following steps assume that you have already [built the toolchain with Ninja](#t
[debug variant](#debugging-issues) of the component you intend to debug.
* <p id="generate-xcode">
Generate the Xcode project with
Generate the Xcode project with:
```sh
utils/build-script --swift-darwin-supported-archs "$(uname -m)" --xcode --clean
utils/generate-xcode <build dir>
```
This can take a few minutes due to metaprogrammed sources that depend on LLVM
tools that are built from source.
</p>
* Create an empty Xcode workspace and open it.
* Add `build/Xcode-*/swift-macosx-*/Swift.xcodeproj` to the workspace by
selecting the Project navigator and choosing
*File > Add Files to "\<workspace name>"*.
where `<build dir>` is the path to the build directory e.g
`../build/Ninja-RelWithDebInfoAssert`. This will create a `Swift.xcodeproj`
in the parent directory (next to the `build` directory).
> **Important**\
> If upon addition Xcode prompts to autocreate schemes, select *Manually
Manage Schemes*.
This Xcode project includes the sources for almost everything in the
repository, including the compiler, standard library and runtime.
If you intend to work on a compiler subcomponent that is written in Swift and
has a `Package.swift` file, e.g. `lib/ASTGen`, first choose
*Product > Scheme > Manage Schemes* and select the *Autocreate schemes*
checkbox, then add the package directory to the workspace the same way you
added the Xcode project.
Xcode will automatically create schemes for the package manifest.
* Create an Xcode project using the _External Build System_ template, and add
it to the workspace.
* Create a target in the new Xcode project, using the _External Build System_
template.
* In the _Info_ pane of the target settings, set
* _Build Tool_ to the absolute path of the `ninja` executable (the output of
`which ninja` on the command line)
* _Arguments_ to a Ninja target (e.g. `bin/swift-frontend` is the compiler)
* _Directory_ to the absolute path of the `build/Ninja-*/swift-macosx-*`
directory
* Create a scheme in the workspace, making sure to select the target you just
created. Be *extra* careful not to choose a target from the generated Xcode
project you added to the workspace.
* Spot-check your target in the settings for the _Build_ scheme action.
* If the target is executable, adjust the settings for the _Run_ scheme action:
* In the _Info_ pane, select the _Executable_ produced by the Ninja target
from `build/Ninja-*/swift-macosx-*/bin` (e.g. `swift-frontend`).
* In the _Arguments_ pane, add command line arguments that you want to pass to
the executable on launch (e.g. `path/to/file.swift -typecheck` for
`bin/swift-frontend`).
* Optionally set a custom working directory in the _Options_ pane.
* Follow the previous steps to create more targets and schemes per your line
of work.
`generate-xcode` directly invokes `swift-xcodegen`, which is a tool designed
specifically to generate Xcode projects for the Swift repo (as well as a
couple of adjacent repos such as LLVM and Clang). It supports a number of
different options, you can run `utils/generate-xcode --help` to see them. For
more information, see [the documentation for `swift-xcodegen`](/utils/swift-xcodegen/README.md).
#### Regenerating the Xcode project
@@ -426,15 +391,6 @@ multiple `update-checkout` rounds, the resulting divergence is likely to begin
affecting your editing experience. To fix this, regenerate the project by
running the invocation from the <a href="#generate-xcode">first step</a>.
#### Troubleshooting editing issues in Xcode
* If a syntax highlighting or code action issue does not resolve itself after
regenerating the Xcode project, select a scheme that covers the affected area
and try *Product > Analyze*.
* Xcode has been seen to sometimes get stuck on indexing after switching back
and forth between distant branches. To sort things out, close the workspace
and delete the _Index_ directory from its derived data.
### Other IDEs setup
You can also use other editors and IDEs to work on Swift.
@@ -658,12 +614,11 @@ printed to stderr. It will likely look something like:
on. If you are new to LLDB, check out the [official LLDB documentation][] and
[nesono's LLDB cheat sheet][].
- Using LLDB within Xcode:
Select the current scheme 'swift-frontend' → Edit Scheme → Run phase →
Arguments tab. Under "Arguments Passed on Launch", copy-paste the `<args>`
and make sure that "Expand Variables Based On" is set to swift-frontend.
Close the scheme editor. If you now run the compiler
(<kbd>⌘</kbd>+<kbd>R</kbd> or Product → Run), you will be able to use the
Xcode debugger.
Select the current scheme 'swift-frontend' → Edit Scheme → Run → Arguments
tab. Under "Arguments Passed on Launch", copy-paste the `<args>` and make sure
that "Expand Variables Based On" is set to swift-frontend. Close the scheme
editor. If you now run the compiler (<kbd>⌘</kbd>+<kbd>R</kbd> or
Product → Run), you will be able to use the Xcode debugger.
Xcode also has the ability to attach to and debug Swift processes launched
elsewhere. Under Debug → Attach to Process by PID or name..., you can enter

View File

@@ -240,6 +240,9 @@ config.substitutions.append( ('%use_just_built_liblto', use_just_built_liblto) )
config.substitutions.append( ('%llvm_libs_dir', llvm_libs_dir) )
config.substitutions.append( ('%llvm_plugin_ext', llvm_plugin_ext) )
# Allow tests to restore the original environment if they need to.
config.substitutions.append( ('%original_path_env', config.environment['PATH']) )
def append_to_env_path(directory):
config.environment['PATH'] = \
os.path.pathsep.join((directory, config.environment['PATH']))

2
utils/generate-xcode Executable file
View File

@@ -0,0 +1,2 @@
#!/bin/zsh
exec "$0:A:h/swift-xcodegen/swift-xcodegen" "$@"

6
utils/swift-xcodegen/.gitignore vendored Normal file
View File

@@ -0,0 +1,6 @@
.DS_Store
/.build
/Packages
/*.xcodeproj
xcuserdata/
.swiftpm

View File

@@ -0,0 +1,59 @@
{
"pins" : [
{
"identity" : "swift-argument-parser",
"kind" : "remoteSourceControl",
"location" : "https://github.com/apple/swift-argument-parser.git",
"state" : {
"revision" : "0fbc8848e389af3bb55c182bc19ca9d5dc2f255b",
"version" : "1.4.0"
}
},
{
"identity" : "swift-driver",
"kind" : "remoteSourceControl",
"location" : "https://github.com/swiftlang/swift-driver",
"state" : {
"branch" : "main",
"revision" : "c647e91574122f2b104d294ab1ec5baadaa1aa95"
}
},
{
"identity" : "swift-llbuild",
"kind" : "remoteSourceControl",
"location" : "https://github.com/apple/swift-llbuild.git",
"state" : {
"branch" : "main",
"revision" : "e4ea3d267974bf75e637ec65c0b753558b22a451"
}
},
{
"identity" : "swift-toolchain-sqlite",
"kind" : "remoteSourceControl",
"location" : "https://github.com/swiftlang/swift-toolchain-sqlite",
"state" : {
"revision" : "9cff1f87bf66f6642ba510d42da7a3bb10006bba",
"version" : "0.1.1"
}
},
{
"identity" : "swift-tools-support-core",
"kind" : "remoteSourceControl",
"location" : "https://github.com/apple/swift-tools-support-core.git",
"state" : {
"branch" : "main",
"revision" : "a76104dbd3c3fff41adb70bc7e917a4b2d076cef"
}
},
{
"identity" : "yams",
"kind" : "remoteSourceControl",
"location" : "https://github.com/jpsim/Yams.git",
"state" : {
"revision" : "0d9ee7ea8c4ebd4a489ad7a73d5c6cad55d6fed3",
"version" : "5.0.6"
}
}
],
"version" : 2
}

View File

@@ -0,0 +1,50 @@
// swift-tools-version: 5.8
// The swift-tools-version declares the minimum version of Swift required to build this package.
import PackageDescription
import class Foundation.ProcessInfo
let package = Package(
name: "swift-xcodegen",
platforms: [.macOS(.v13)],
targets: [
.target(name: "Xcodeproj", exclude: ["README.md"]),
.target(
name: "SwiftXcodeGen",
dependencies: [
.product(name: "ArgumentParser", package: "swift-argument-parser"),
.product(name: "SwiftOptions", package: "swift-driver"),
"Xcodeproj"
],
swiftSettings: [
.enableExperimentalFeature("StrictConcurrency")
]
),
.executableTarget(
name: "swift-xcodegen",
dependencies: [
.product(name: "ArgumentParser", package: "swift-argument-parser"),
"SwiftXcodeGen"
],
swiftSettings: [
.enableExperimentalFeature("StrictConcurrency")
]
),
.testTarget(
name: "SwiftXcodeGenTest",
dependencies: ["SwiftXcodeGen"]
)
]
)
if ProcessInfo.processInfo.environment["SWIFTCI_USE_LOCAL_DEPS"] == nil {
package.dependencies += [
.package(url: "https://github.com/apple/swift-argument-parser", from: "1.4.0"),
.package(url: "https://github.com/swiftlang/swift-driver", branch: "main"),
]
} else {
package.dependencies += [
.package(path: "../../../swift-argument-parser"),
.package(path: "../../../swift-driver"),
]
}

View File

@@ -0,0 +1,108 @@
# swift-xcodegen
A script for generating an Xcode project for the Swift repo, that sits on top of an existing Ninja build. This has a few advantages over CMake's Xcode generator (using `build-script --xcode`):
- Fast to regenerate (less than a second)
- Native Swift targets for ASTGen/SwiftCompilerSources + Standard Library
- Better file organization (by path rather than by target)
- Much fewer targets, easier to manage
This script is primarily focussed on providing a good editor experience for working on the Swift project; it is not designed to produce compiled products or run tests, that should be done with `ninja` and `build-script`. It can however be used to [debug executables produced by the Ninja build](#debugging).
## Running
Run as:
```
./swift-xcodegen <path to Ninja build directory>
```
An Xcode project will be created in the grandparent directory (i.e `build/../Swift.xcodeproj`). Projects for LLVM, LLDB, and Clang may also be created by passing `--llvm`, `--lldb`, and `--clang` respectively. Workspaces of useful combinations will also be created (e.g Swift+LLVM, Clang+LLVM).
An `ALL` meta-target is created that depends on all the targets in the given project or workspace. A scheme for this target is automatically generated too (and automatic scheme generation is disabled). You can manually add individual schemes for targets you're interested in. Note however that Clang targets do not currently have dependency information.
## Debugging
By default, schemes are added for executable products, which can be used for debugging in Xcode. These use `ninja` to build the product before running. If using a separate build for debugging, you can specify it with `--runnable-build-dir`.
## Standard library targets
By default, C/C++ standard library + runtime files are added to the project. Swift targets may be added by passing `--stdlib-swift`, which adds a target for the core standard library as well as auxiliary libraries (e.g CxxStdlib, Backtracing, Concurrency). This requires using Xcode with an up-to-date development snapshot, since the standard library expects to be built using the just-built compiler.
## Command usage
```
USAGE: swift-xcodegen [<options>] <build-dir>
ARGUMENTS:
<build-dir> The path to the Ninja build directory to generate for
LLVM PROJECTS:
--clang/--no-clang Generate an xcodeproj for Clang (default: --no-clang)
--clang-tools-extra/--no-clang-tools-extra
When generating a project for Clang, whether to include clang-tools-extra (default: --clang-tools-extra)
--lldb/--no-lldb Generate an xcodeproj for LLDB (default: --no-lldb)
--llvm/--no-llvm Generate an xcodeproj for LLVM (default: --no-llvm)
SWIFT TARGETS:
--swift-targets/--no-swift-targets
Generate targets for Swift files, e.g ASTGen, SwiftCompilerSources. Note
this by default excludes the standard library, see '--stdlib-swift'. (default: --swift-targets)
--swift-dependencies/--no-swift-dependencies
When generating Swift targets, add dependencies (e.g swift-syntax) to the
generated project. This makes build times slower, but improves syntax
highlighting for targets that depend on them. (default: --swift-dependencies)
RUNNABLE TARGETS:
--runnable-build-dir <runnable-build-dir>
If specified, runnable targets will use this build directory. Useful for
configurations where a separate debug build directory is used.
--runnable-targets/--no-runnable-targets
Whether to add runnable targets for e.g swift-frontend. This is useful
for debugging in Xcode. (default: --runnable-targets)
--build-runnable-targets/--no-build-runnable-targets
If runnable targets are enabled, whether to add a build action for them.
If false, they will be added as freestanding schemes. (default: --build-runnable-targets)
PROJECT CONFIGURATION:
--compiler-libs/--no-compiler-libs
Generate targets for compiler libraries (default: --compiler-libs)
--compiler-tools/--no-compiler-tools
Generate targets for compiler tools (default: --compiler-tools)
--docs/--no-docs Add doc groups to the generated projects (default: --docs)
--stdlib, --stdlib-cxx/--no-stdlib, --no-stdlib-cxx
Generate a target for C/C++ files in the standard library (default: --stdlib)
--stdlib-swift/--no-stdlib-swift
Generate targets for Swift files in the standard library. This requires
using Xcode with with a main development snapshot (and as such is disabled
by default). (default: --no-stdlib-swift)
--test-folders/--no-test-folders
Add folder references for test files (default: --test-folders)
--unittests/--no-unittests
Generate a target for the unittests (default: --unittests)
--infer-args/--no-infer-args
Whether to infer build arguments for files that don't have any, based
on the build arguments of surrounding files. This is mainly useful for
files that aren't built in the default config, but are still useful to
edit (e.g sourcekitdAPI-InProc.cpp). (default: --infer-args)
MISC:
--project-root-dir <project-root-dir>
The project root directory, which is the parent directory of the Swift repo.
By default this is inferred from the build directory path.
--output-dir <output-dir>
The output directory to write the Xcode project to. Defaults to the project
root directory.
--log-level <log-level> The log level verbosity (default: info) (values: debug, info, note, warning, error)
--parallel/--no-parallel
Parallelize generation of projects (default: --parallel)
-q, --quiet Quiet output; equivalent to --log-level warning
OPTIONS:
-h, --help Show help information.
```
## TODO
- [ ] Add support for mixed Swift + Clang targets
- [ ] More tests

View File

@@ -0,0 +1,284 @@
//===--- BuildArgs.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 BuildArgs {
let command: KnownCommand
private var topLevelArgs: [Command.Argument] = []
private var subOptArgs: [SubOptionArgs] = []
init(for command: KnownCommand, args: [Command.Argument] = []) {
self.command = command
self += args
}
}
extension BuildArgs {
struct SubOptionArgs {
var flag: Command.Flag
var args: BuildArgs
var command: KnownCommand {
args.command
}
}
}
extension BuildArgs {
typealias Element = Command.Argument
/// Whether both the arguments and sub-option arguments are empty.
var isEmpty: Bool {
topLevelArgs.isEmpty && subOptArgs.isEmpty
}
/// Whether the argument have a given flag.
func hasFlag(_ flag: Command.Flag) -> Bool {
topLevelArgs.contains(where: { $0.flag == flag })
}
/// Retrieve the flag in a given list of flags.
func lastFlag(in flags: [Command.Flag]) -> Command.Flag? {
for arg in topLevelArgs.reversed() {
guard let flag = arg.flag, flags.contains(flag) else { continue }
return flag
}
return nil
}
/// Retrieve the flag in a given list of flags.
func lastFlag(in flags: Command.Flag...) -> Command.Flag? {
lastFlag(in: flags)
}
/// Retrieve the last value for a given flag, unescaped.
func lastValue(for flag: Command.Flag) -> String? {
topLevelArgs.last(where: { $0.flag == flag })?.value
}
/// Retrieve the last printed value for a given flag, escaping as needed.
func lastPrintedValue(for flag: Command.Flag) -> String? {
lastValue(for: flag)?.escaped
}
/// Retrieve the printed values for a given flag, escaping as needed.
func printedValues(for flag: Command.Flag) -> [String] {
topLevelArgs.compactMap { $0.option(for: flag)?.value.escaped }
}
var printedArgs: [String] {
topLevelArgs.flatMap(\.printedArgs) + subOptArgs.flatMap { subArgs in
let printedFlag = subArgs.flag.printed
return subArgs.args.printedArgs.flatMap { [printedFlag, $0] }
}
}
var printed: String {
printedArgs.joined(separator: " ")
}
func hasSubOptions(for command: KnownCommand) -> Bool {
subOptArgs.contains(where: { $0.command == command })
}
/// Retrieve a set of sub-options for a given command.
func subOptions(for command: KnownCommand) -> BuildArgs {
hasSubOptions(for: command) ? self[subOptions: command] : .init(for: command)
}
subscript(subOptions command: KnownCommand) -> BuildArgs {
_read {
let index = subOptArgs.firstIndex(where: { $0.command == command })!
yield subOptArgs[index].args
}
_modify {
let index = subOptArgs.firstIndex(where: { $0.command == command })!
yield &subOptArgs[index].args
}
}
/// Apply a transform to the set of arguments. Note this doesn't include any
/// sub-options.
func map(_ transform: (Element) throws -> Element) rethrows -> Self {
var result = self
result.topLevelArgs = try topLevelArgs.map(transform)
return result
}
/// Apply a filter to the set of arguments. Note this doesn't include any
/// sub-options.
func filter(_ predicate: (Element) throws -> Bool) rethrows -> Self {
var result = self
result.topLevelArgs = try topLevelArgs.filter(predicate)
return result
}
/// Remove a set of flags from the arguments.
mutating func exclude(_ flags: [Command.Flag]) {
topLevelArgs.removeAll { arg in
guard let f = arg.flag else { return false }
return flags.contains(f)
}
}
/// Remove a set of flags from the arguments.
mutating func exclude(_ flags: Command.Flag...) {
exclude(flags)
}
/// Remove a set of flags from the arguments.
func excluding(_ flags: [Command.Flag]) -> Self {
var result = self
result.exclude(flags)
return result
}
/// Remove a set of flags from the arguments.
func excluding(_ flags: Command.Flag...) -> Self {
excluding(flags)
}
/// Take the last unescaped value for a given flag, removing all occurances
/// of the flag from the arguments.
mutating func takeLastValue(for flag: Command.Flag) -> String? {
guard let value = lastValue(for: flag) else { return nil }
exclude(flag)
return value
}
/// Take the last printed value for a given flag, escaping as needed, and
/// removing all occurances of the flag from the arguments
mutating func takePrintedLastValue(for flag: Command.Flag) -> String? {
guard let value = lastPrintedValue(for: flag) else { return nil }
exclude(flag)
return value
}
/// Take a set of printed values for a given flag, escaping as needed.
mutating func takePrintedValues(for flag: Command.Flag) -> [String] {
let result = topLevelArgs.compactMap { $0.option(for: flag)?.value.escaped }
exclude(flag)
return result
}
/// Take a flag, returning `true` if it was removed, `false` if it isn't
/// present.
mutating func takeFlag(_ flag: Command.Flag) -> Bool {
guard hasFlag(flag) else { return false }
exclude(flag)
return true
}
/// Takes a set of flags, returning `true` if the flags were removed, `false`
/// if they aren't present.
mutating func takeFlags(_ flags: Command.Flag...) -> Bool {
guard flags.contains(where: self.hasFlag) else { return false }
exclude(flags)
return true
}
/// Takes a set of related flags, returning the last one encountered, or `nil`
/// if no flags in the group are present.
mutating func takeFlagGroup(_ flags: Command.Flag...) -> Command.Flag? {
guard let value = lastFlag(in: flags) else { return nil }
exclude(flags)
return value
}
private mutating func appendSubOptArg(
_ value: String, for command: KnownCommand, flag: Command.Flag
) {
let idx = subOptArgs.firstIndex(where: { $0.command == command }) ?? {
subOptArgs.append(.init(flag: flag, args: .init(for: command)))
return subOptArgs.endIndex - 1
}()
subOptArgs[idx].args.append(value.escaped)
}
mutating func append(_ element: Element) {
if let flag = element.flag, let command = flag.subOptionCommand,
let value = element.value {
appendSubOptArg(value, for: command, flag: flag)
} else if let last = topLevelArgs.last, case .flag(let flag) = last,
case .value(let value) = element {
// If the last element is a flag, and this is a value, we may need to
// merge.
topLevelArgs.removeLast()
topLevelArgs += try! CommandParser.parseArguments(
"\(flag) \(value.escaped)", for: command
)
} else {
topLevelArgs.append(element)
}
}
mutating func append<S: Sequence>(contentsOf seq: S) where S.Element == Element {
for element in seq {
append(element)
}
}
static func += <S: Sequence> (lhs: inout Self, rhs: S) where S.Element == Element {
lhs.append(contentsOf: rhs)
}
mutating func append(_ input: String) {
self += try! CommandParser.parseArguments(input, for: command)
}
/// Apply a transform to the values of any options present. If
/// `includeSubOptions` is `true`, the transform will also be applied to any
/// sub-options present.
mutating func transformValues(
for flag: Command.Flag? = nil, includeSubOptions: Bool,
_ fn: (String) throws -> String
) rethrows {
topLevelArgs = try topLevelArgs.map { arg in
guard flag == nil || arg.flag == flag else { return arg }
return try arg.mapValue(fn)
}
if includeSubOptions {
for idx in subOptArgs.indices {
try subOptArgs[idx].args.transformValues(
for: flag, includeSubOptions: true, fn
)
}
}
}
struct PathSubstitution: Hashable {
var oldPath: AbsolutePath
var newPath: AnyPath
}
/// Apply a substitution to any paths present in the option values, returning
/// the substitutions made. If `includeSubOptions` is `true`, the substitution
/// will also be applied to any sub-options present.
mutating func substitutePaths<Path: PathProtocol>(
for flag: Command.Flag? = nil, includeSubOptions: Bool,
_ fn: (AbsolutePath) throws -> Path?
) rethrows -> [BuildArgs.PathSubstitution] {
var subs: [BuildArgs.PathSubstitution] = []
try transformValues(for: flag,
includeSubOptions: includeSubOptions) { value in
guard case .absolute(let path) = AnyPath(value),
let newPath = try fn(path) else { return value }
let subst = PathSubstitution(oldPath: path, newPath: AnyPath(newPath))
subs.append(subst)
return subst.newPath.rawPath
}
return subs
}
}
extension BuildArgs: CustomStringConvertible {
var description: String { printed }
}

View File

@@ -0,0 +1,93 @@
//===--- ClangBuildArgsProvider.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 Foundation
struct ClangBuildArgsProvider {
private var args = CommandArgTree()
private var outputs: [RelativePath: AbsolutePath] = [:]
init(for buildDir: RepoBuildDir) throws {
let buildDirPath = buildDir.path
let repoPath = buildDir.repoPath
// TODO: Should we get Clang build args from the build.ninja? We're already
// parsing that to get the Swift targets, seems unfortunate to have 2
// sources of truth.
let fileName = buildDirPath.appending("compile_commands.json")
guard fileName.exists else {
throw XcodeGenError.pathNotFound(fileName)
}
log.debug("[*] Reading Clang build args from '\(fileName)'")
let parsed = try JSONDecoder().decode(
CompileCommands.self, from: try fileName.read()
)
// Gather the candidates for each file to get build arguments for. We may
// have multiple outputs, in which case, pick the first one that exists.
var commandsToAdd: [RelativePath:
(output: AbsolutePath, args: [Command.Argument])] = [:]
for command in parsed {
guard command.command.executable.knownCommand == .clang,
let relFilePath = command.file.removingPrefix(repoPath)
else {
continue
}
let output = command.directory.appending(command.output)
if let existing = commandsToAdd[relFilePath],
existing.output.exists || !output.exists {
continue
}
commandsToAdd[relFilePath] = (output, command.command.args)
}
for (path, (output, commandArgs)) in commandsToAdd {
// Only include arguments that have known flags.
args.insert(commandArgs.filter({ $0.flag != nil }), for: path)
outputs[path] = output
}
}
/// Retrieve the arguments at a given path, including those in the parent.
func getArgs(for path: RelativePath) -> BuildArgs {
// Sort the arguments to get a deterministic ordering.
// FIXME: We ought to get the command from the arg tree.
.init(for: .clang, args: args.getArgs(for: path).sorted())
}
/// Retrieve the arguments at a given path, excluding those already covered
/// by a parent.
func getUniqueArgs(
for path: RelativePath, parent: RelativePath, infer: Bool = false
) -> BuildArgs {
var fileArgs: Set<Command.Argument> = []
if hasBuildArgs(for: path) {
fileArgs = args.getUniqueArgs(for: path, parent: parent)
} else if infer {
// If we can infer arguments, walk up to the nearest parent with args.
if let component = path.stackedComponents
.reversed().dropFirst().first(where: hasBuildArgs) {
fileArgs = args.getUniqueArgs(for: component, parent: parent)
}
}
// Sort the arguments to get a deterministic ordering.
// FIXME: We ought to get the command from the arg tree.
return .init(for: .clang, args: fileArgs.sorted())
}
/// Whether the given file has build arguments.
func hasBuildArgs(for path: RelativePath) -> Bool {
!args.getArgs(for: path).isEmpty
}
func isObjectFilePresent(for path: RelativePath) -> Bool {
outputs[path]?.exists == true
}
}

View File

@@ -0,0 +1,49 @@
//===--- CommandArgTree.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
//
//===----------------------------------------------------------------------===//
/// A tree of compile command arguments, indexed by path such that those unique
/// to a particular file can be queried, with common arguments associated
/// with a common parent.
struct CommandArgTree {
private var storage: [RelativePath: Set<Command.Argument>]
init() {
self.storage = [:]
}
mutating func insert(_ args: [Command.Argument], for path: RelativePath) {
let args = Set(args)
for component in path.stackedComponents {
// If we haven't added any arguments, add them. If we're adding arguments
// for the file itself, this is the only way we'll add arguments,
// otherwise we can form an intersection with the other arguments.
let inserted = storage.insertValue(args, for: component)
guard !inserted && component != path else { continue }
// We use subscript(_:default:) to mutate in-place without CoW.
storage[component, default: []].formIntersection(args)
}
}
/// Retrieve the arguments at a given path, including those in the parent.
func getArgs(for path: RelativePath) -> Set<Command.Argument> {
storage[path] ?? []
}
/// Retrieve the arguments at a given path, excluding those already covered
/// by a given parent.
func getUniqueArgs(
for path: RelativePath, parent: RelativePath
) -> Set<Command.Argument> {
getArgs(for: path).subtracting(getArgs(for: parent))
}
}

View File

@@ -0,0 +1,75 @@
//===--- RunnableTargets.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
//
//===----------------------------------------------------------------------===//
/// A target that defines a runnable executable.
struct RunnableTarget: Hashable {
var name: String
var ninjaTargetName: String
var path: AbsolutePath
}
struct RunnableTargets {
private var addedPaths: Set<RelativePath> = []
private var targets: [RunnableTarget] = []
init(from buildDir: RepoBuildDir) throws {
for rule in try buildDir.ninjaFile.buildRules {
tryAddTarget(rule, buildDir: buildDir)
}
}
}
extension RunnableTargets: RandomAccessCollection {
typealias Element = RunnableTarget
typealias Index = Int
var startIndex: Int { targets.startIndex }
var endIndex: Int { targets.endIndex }
func index(_ i: Int, offsetBy distance: Int) -> Int {
targets.index(i, offsetBy: distance)
}
subscript(position: Int) -> RunnableTarget {
targets[position]
}
}
extension RunnableTargets {
private func getRunnablePath(
for outputs: [String]
) -> (String, RelativePath)? {
// We're only interested in rules with the path 'bin/<executable>'.
for output in outputs {
guard case let .relative(r) = AnyPath(output),
r.components.count == 2, r.components.first == "bin"
else { return nil }
return (output, r)
}
return nil
}
private mutating func tryAddTarget(
_ rule: NinjaBuildFile.BuildRule, buildDir: RepoBuildDir
) {
guard let (name, path) = getRunnablePath(for: rule.outputs),
addedPaths.insert(path).inserted else { return }
let absPath = buildDir.path.appending(path)
guard absPath.exists, absPath.isExecutable else { return }
let target = RunnableTarget(
name: path.fileName, ninjaTargetName: name, path: absPath
)
targets.append(target)
}
}

View File

@@ -0,0 +1,126 @@
//===--- SwiftDriverUtils.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
//
//===----------------------------------------------------------------------===//
// https://github.com/swiftlang/swift-driver/blob/661e0bc74bdae4d9f6ea8a7a54015292febb0059/Sources/SwiftDriver/Utilities/StringAdditions.swift
extension String {
/// Whether this string is a Swift identifier.
var isValidSwiftIdentifier: Bool {
guard let start = unicodeScalars.first else {
return false
}
let continuation = unicodeScalars.dropFirst()
return start.isValidSwiftIdentifierStart &&
continuation.allSatisfy { $0.isValidSwiftIdentifierContinuation }
}
}
extension Unicode.Scalar {
var isValidSwiftIdentifierStart: Bool {
guard isValidSwiftIdentifierContinuation else { return false }
if isASCIIDigit || self == "$" {
return false
}
// N1518: Recommendations for extended identifier characters for C and C++
// Proposed Annex X.2: Ranges of characters disallowed initially
if (0x0300...0x036F).contains(value) ||
(0x1DC0...0x1DFF).contains(value) ||
(0x20D0...0x20FF).contains(value) ||
(0xFE20...0xFE2F).contains(value) {
return false
}
return true
}
var isValidSwiftIdentifierContinuation: Bool {
if isASCII {
return isCIdentifierBody(allowDollar: true)
}
// N1518: Recommendations for extended identifier characters for C and C++
// Proposed Annex X.1: Ranges of characters allowed
return value == 0x00A8 ||
value == 0x00AA ||
value == 0x00AD ||
value == 0x00AF ||
(0x00B2...0x00B5).contains(value) ||
(0x00B7...0x00BA).contains(value) ||
(0x00BC...0x00BE).contains(value) ||
(0x00C0...0x00D6).contains(value) ||
(0x00D8...0x00F6).contains(value) ||
(0x00F8...0x00FF).contains(value) ||
(0x0100...0x167F).contains(value) ||
(0x1681...0x180D).contains(value) ||
(0x180F...0x1FFF).contains(value) ||
(0x200B...0x200D).contains(value) ||
(0x202A...0x202E).contains(value) ||
(0x203F...0x2040).contains(value) ||
value == 0x2054 ||
(0x2060...0x206F).contains(value) ||
(0x2070...0x218F).contains(value) ||
(0x2460...0x24FF).contains(value) ||
(0x2776...0x2793).contains(value) ||
(0x2C00...0x2DFF).contains(value) ||
(0x2E80...0x2FFF).contains(value) ||
(0x3004...0x3007).contains(value) ||
(0x3021...0x302F).contains(value) ||
(0x3031...0x303F).contains(value) ||
(0x3040...0xD7FF).contains(value) ||
(0xF900...0xFD3D).contains(value) ||
(0xFD40...0xFDCF).contains(value) ||
(0xFDF0...0xFE44).contains(value) ||
(0xFE47...0xFFF8).contains(value) ||
(0x10000...0x1FFFD).contains(value) ||
(0x20000...0x2FFFD).contains(value) ||
(0x30000...0x3FFFD).contains(value) ||
(0x40000...0x4FFFD).contains(value) ||
(0x50000...0x5FFFD).contains(value) ||
(0x60000...0x6FFFD).contains(value) ||
(0x70000...0x7FFFD).contains(value) ||
(0x80000...0x8FFFD).contains(value) ||
(0x90000...0x9FFFD).contains(value) ||
(0xA0000...0xAFFFD).contains(value) ||
(0xB0000...0xBFFFD).contains(value) ||
(0xC0000...0xCFFFD).contains(value) ||
(0xD0000...0xDFFFD).contains(value) ||
(0xE0000...0xEFFFD).contains(value)
}
/// `true` if this character is an ASCII digit: [0-9]
var isASCIIDigit: Bool { (0x30...0x39).contains(value) }
/// `true` if this is a body character of a C identifier,
/// which is [a-zA-Z0-9_].
func isCIdentifierBody(allowDollar: Bool = false) -> Bool {
if (0x41...0x5A).contains(value) ||
(0x61...0x7A).contains(value) ||
isASCIIDigit ||
self == "_" {
return true
} else {
return allowDollar && self == "$"
}
}
}

View File

@@ -0,0 +1,286 @@
//===--- 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] = [:]
private var addedFiles: Set<RelativePath> = []
// 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.buildRules {
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 rule: NinjaBuildFile.BuildRule
) throws -> BuildArgs? {
var buildArgs = BuildArgs(for: .swiftc)
if let commandAttr = rule.attributes[.command] {
// We have a custom command, parse it looking for a swiftc invocation.
let command = try CommandParser.parseKnownCommandOnly(commandAttr.value)
guard let command, command.executable.knownCommand == .swiftc else {
return nil
}
buildArgs += command.args
} else if rule.attributes[.flags] != nil {
// Ninja separates out other arguments we need, splice them back in.
for key: NinjaBuildFile.Attribute.Key in [.flags, .includes, .defines] {
guard let attr = rule.attributes[key] else { continue }
buildArgs.append(attr.value)
}
// Add a module name argument if one is specified, validating to
// ensure it's correct since we currently have some targets with
// invalid module names, e.g swift-plugin-server.
if let moduleName = rule.attributes[.swiftModuleName]?.value,
moduleName.isValidSwiftIdentifier {
buildArgs.append("-module-name \(moduleName)")
}
} else {
return nil
}
// 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 rule: NinjaBuildFile.BuildRule, buildDir: RepoBuildDir
) throws -> SwiftTarget.Sources {
// If we have SWIFT_SOURCES defined, use it, otherwise check the rule
// inputs.
let files: [AnyPath]
if let sourcesStr = rule.attributes[.swiftSources]?.value {
files = try CommandParser.parseArguments(sourcesStr, for: .swiftc)
.compactMap(\.value).map(AnyPath.init)
} else {
files = rule.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)
case .absolute(let a):
guard a.exists, 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 rule: NinjaBuildFile.BuildRule,
buildDir: RepoBuildDir
) throws {
// Phonies are only used to track aliases.
if rule.isPhony {
for output in rule.outputs {
outputAliases[output, default: []] += rule.inputs
}
return
}
// Ignore build rules that don't have object file or swiftmodule outputs.
let forBuild = rule.outputs.contains(
where: { $0.hasExtension(.o) }
)
let forModule = rule.outputs.contains(
where: { $0.hasExtension(.swiftmodule) }
)
guard forBuild || forModule else {
return
}
let primaryOutput = rule.outputs.first!
let sources = try getSources(from: rule, 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: rule) else { return }
let moduleName = buildArgs.lastValue(for: .moduleName)
guard let moduleName else {
log.debug("! Skipping Swift target with output \(primaryOutput); no module name")
return
}
let moduleLinkName = rule.attributes[.swiftLibraryName]?.value ??
buildArgs.lastValue(for: .moduleLinkName)
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(rule.inputs)
deps.formUnion(
rule.dependencies.filter { $0.hasExtension(.swiftmodule) }
)
}
var buildRule: SwiftTarget.BuildRule?
var emitModuleRule: SwiftTarget.EmitModuleRule?
if forBuild && !repoSources.isEmpty {
// Bail if we've already recorded a target with one of these inputs.
// TODO: Attempt to merge?
// TODO: Should we be doing this later?
for input in repoSources {
guard addedFiles.insert(input).inserted else {
log.debug("""
! Skipping '\(name)' with output '\(primaryOutput)'; \
contains input '\(input)' already added
""")
return
}
}
// We've already ensured that `repoSources` is non-empty.
let parent = repoSources.commonAncestor!
buildRule = .init(
parentPath: parent, 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 rule.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.hasPrefix(path)
else {
return false
}
return true
}
}
}

View File

@@ -0,0 +1,286 @@
//===--- Command.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 Command: Hashable {
var executable: AnyPath
var args: [Argument]
init(executable: AnyPath, args: [Argument]) {
self.executable = executable
self.args = args
}
}
extension Command: Decodable {
init(from decoder: Decoder) throws {
let command = try decoder.singleValueContainer().decode(String.self)
self = try CommandParser.parseCommand(command)
}
}
extension Command {
var printedArgs: [String] {
[executable.rawPath.escaped] + args.flatMap(\.printedArgs)
}
var printed: String {
printedArgs.joined(separator: " ")
}
}
// MARK: Argument
extension Command {
enum Argument: Hashable {
case option(Option)
case flag(Flag)
case value(String)
}
}
extension Command.Argument {
static func option(
_ flag: Command.Flag, spacing: Command.OptionSpacing, value: String
) -> Self {
.option(.init(flag, spacing: spacing, value: value))
}
var flag: Command.Flag? {
switch self {
case .option(let opt):
opt.flag
case .flag(let flag):
flag
case .value:
nil
}
}
var value: String? {
switch self {
case .option(let opt):
opt.value
case .value(let value):
value
case .flag:
nil
}
}
var printedArgs: [String] {
switch self {
case .option(let opt):
opt.printedArgs
case .value(let value):
[value.escaped]
case .flag(let f):
[f.printed]
}
}
var printed: String {
printedArgs.joined(separator: " ")
}
func option(for flag: Command.Flag) -> Command.Option? {
switch self {
case .option(let opt) where opt.flag == flag:
opt
default:
nil
}
}
/// If there is a value, apply a transform to it.
func mapValue(_ fn: (String) throws -> String) rethrows -> Self {
switch self {
case .option(let opt):
.option(try opt.mapValue(fn))
case .value(let value):
.value(try fn(value))
case .flag:
// Nothing to map.
self
}
}
}
// MARK: Flag
extension Command {
struct Flag: Hashable {
var dash: Dash
var name: Name
}
}
extension Command.Flag {
static func dash(_ name: Name) -> Self {
.init(dash: .single, name: name)
}
static func doubleDash(_ name: Name) -> Self {
.init(dash: .double, name: name)
}
var printed: String {
"\(dash.printed)\(name.rawValue)"
}
}
extension Command.Flag {
enum Dash: Int, CaseIterable, Comparable {
case single = 1, double
static func < (lhs: Self, rhs: Self) -> Bool { lhs.rawValue < rhs.rawValue }
}
}
extension Command.Flag.Dash {
init?(numDashes: Int) {
self.init(rawValue: numDashes)
}
var printed: String {
switch self {
case .single:
return "-"
case .double:
return "--"
}
}
}
extension DefaultStringInterpolation {
mutating func appendInterpolation(_ flag: Command.Flag) {
appendInterpolation(flag.printed)
}
}
// MARK: Option
extension Command {
struct Option: Hashable {
var flag: Flag
var spacing: OptionSpacing
var value: String
init(_ flag: Flag, spacing: OptionSpacing, value: String) {
self.flag = flag
self.spacing = spacing
self.value = value
}
}
}
extension Command.Option {
func withValue(_ newValue: String) -> Self {
var result = self
result.value = newValue
return result
}
func mapValue(_ fn: (String) throws -> String) rethrows -> Self {
withValue(try fn(value))
}
var printedArgs: [String] {
switch spacing {
case .equals, .unspaced:
["\(flag)\(spacing)\(value.escaped)"]
case .spaced:
["\(flag)", value.escaped]
}
}
var printed: String {
printedArgs.joined(separator: " ")
}
}
// MARK: OptionSpacing
extension Command {
enum OptionSpacing: Comparable {
case equals, unspaced, spaced
}
}
extension Command.OptionSpacing {
var printed: String {
switch self {
case .equals: "="
case .unspaced: ""
case .spaced: " "
}
}
}
// MARK: CustomStringConvertible
extension Command.Argument: CustomStringConvertible {
var description: String { printed }
}
extension Command.OptionSpacing: CustomStringConvertible {
var description: String { printed }
}
extension Command.Flag: CustomStringConvertible {
var description: String { printed }
}
extension Command.Option: CustomStringConvertible {
var description: String { printed }
}
// MARK: Comparable
// We sort the resulting command-line arguments to ensure deterministic
// ordering.
extension Command.Flag: Comparable {
static func < (lhs: Self, rhs: Self) -> Bool {
guard lhs.dash == rhs.dash else {
return lhs.dash < rhs.dash
}
return lhs.name.rawValue < rhs.name.rawValue
}
}
extension Command.Option: Comparable {
static func < (lhs: Self, rhs: Self) -> Bool {
guard lhs.flag == rhs.flag else {
return lhs.flag < rhs.flag
}
guard lhs.spacing == rhs.spacing else {
return lhs.spacing < rhs.spacing
}
return lhs.value < rhs.value
}
}
extension Command.Argument: Comparable {
static func < (lhs: Self, rhs: Self) -> Bool {
switch (lhs, rhs) {
// Sort flags < options < values
case (.flag, .option): true
case (.flag, .value): true
case (.option, .value): true
case (.option, .flag): false
case (.value, .flag): false
case (.value, .option): false
case (.flag(let lhs), .flag(let rhs)): lhs < rhs
case (.option(let lhs), .option(let rhs)): lhs < rhs
case (.value(let lhs), .value(let rhs)): lhs < rhs
}
}
}

View File

@@ -0,0 +1,204 @@
//===--- CommandParser.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 CommandParser {
private var input: ByteScanner
private var knownCommand: KnownCommand?
private init(_ input: UnsafeBufferPointer<UInt8>) {
self.input = ByteScanner(input)
}
static func parseCommand(_ input: String) throws -> Command {
var input = input
return try input.withUTF8 { bytes in
var parser = Self(bytes)
return try parser.parseCommand()
}
}
static func parseKnownCommandOnly(_ input: String) throws -> Command? {
var input = input
return try input.withUTF8 { bytes in
var parser = Self(bytes)
guard let executable = try parser.consumeExecutable(
dropPrefixBeforeKnownCommand: true
) else {
return nil
}
return Command(executable: executable, args: try parser.consumeArguments())
}
}
static func parseArguments(
_ input: String, for command: KnownCommand
) throws -> [Command.Argument] {
var input = input
return try input.withUTF8 { bytes in
var parser = Self(bytes)
parser.knownCommand = command
return try parser.consumeArguments()
}
}
private mutating func parseCommand() throws -> Command {
guard let executable = try consumeExecutable() else {
throw CommandParserError.expectedCommand
}
return Command(executable: executable, args: try consumeArguments())
}
private mutating func consumeExecutable(
dropPrefixBeforeKnownCommand: Bool = false
) throws -> AnyPath? {
var executable: AnyPath
repeat {
guard let executableUTF8 = try input.consumeElement() else {
return nil
}
executable = AnyPath(String(utf8: executableUTF8))
self.knownCommand = executable.knownCommand
// If we want to drop the prefix before a known command, keep dropping
// elements until we find the known command.
} while dropPrefixBeforeKnownCommand && knownCommand == nil
return executable
}
private mutating func consumeArguments() throws -> [Command.Argument] {
var args = [Command.Argument]()
while let arg = try consumeArgument() {
args.append(arg)
}
return args
}
}
enum CommandParserError: Error, CustomStringConvertible {
case expectedCommand
case unterminatedStringLiteral
var description: String {
switch self {
case .expectedCommand:
return "expected command in command line"
case .unterminatedStringLiteral:
return "unterminated string literal in command line"
}
}
}
fileprivate extension ByteScanner.Consumer {
/// Consumes a character, unescaping if needed.
mutating func consumeUnescaped() -> Bool {
if peek == "\\" {
skip()
}
return eat()
}
mutating func consumeStringLiteral() throws {
assert(peek == "\"")
skip()
repeat {
if peek == "\"" {
skip()
return
}
} while consumeUnescaped()
throw CommandParserError.unterminatedStringLiteral
}
}
fileprivate extension ByteScanner {
mutating func consumeElement() throws -> Bytes? {
// Eat any leading whitespace.
skip(while: \.isSpaceOrTab)
// If we're now at the end of the input, nothing can be parsed.
guard hasInput else { return nil }
// Consume the element, stopping at the first space.
return try consume(using: { consumer in
switch consumer.peek {
case let c where c.isSpaceOrTab:
return false
case "\"":
try consumer.consumeStringLiteral()
return true
default:
return consumer.consumeUnescaped()
}
})
}
}
extension CommandParser {
mutating func tryConsumeOption(
_ option: ByteScanner, for flagSpec: Command.FlagSpec.Element
) throws -> Command.Argument? {
var option = option
let flag = flagSpec.flag
guard option.tryEat(utf8: flag.name.rawValue) else {
return nil
}
func makeOption(
spacing: Command.OptionSpacing, _ value: String
) -> Command.Argument {
.option(flag, spacing: spacing, value: value)
}
let spacing = flagSpec.spacing
do {
var option = option
if spacing.contains(.equals), option.tryEat("="), option.hasInput {
return makeOption(spacing: .equals, String(utf8: option.remaining))
}
}
if spacing.contains(.unspaced), option.hasInput {
return makeOption(spacing: .unspaced, String(utf8: option.remaining))
}
if spacing.contains(.spaced), !option.hasInput,
let value = try input.consumeElement() {
return makeOption(spacing: .spaced, String(utf8: value))
}
return option.empty ? .flag(flag) : nil
}
mutating func consumeOption(
_ option: ByteScanner, dash: Command.Flag.Dash
) throws -> Command.Argument? {
// NOTE: If we ever expand the list of flags, we'll likely want to use a
// trie or something here.
guard let knownCommand else { return nil }
for spec in knownCommand.flagSpec.flags where spec.flag.dash == dash {
if let option = try tryConsumeOption(option, for: spec) {
return option
}
}
return nil
}
mutating func consumeArgument() throws -> Command.Argument? {
guard let element = try input.consumeElement() else { return nil }
return try element.withUnsafeBytes { bytes in
var option = ByteScanner(bytes)
var numDashes = 0
if option.tryEat("-") { numDashes += 1 }
if option.tryEat("-") { numDashes += 1 }
guard let dash = Command.Flag.Dash(numDashes: numDashes),
let result = try consumeOption(option, dash: dash) else {
return .value(String(utf8: option.whole))
}
return result
}
}
}

View File

@@ -0,0 +1,47 @@
//===--- CompileCommands.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
//
//===----------------------------------------------------------------------===//
/// A Decodable representation of compile_commands.json.
struct CompileCommands: Decodable {
public var commands: [Element]
init(_ commands: [Element]) {
self.commands = commands
}
public init(from decoder: Decoder) throws {
self.init(try decoder.singleValueContainer().decode([Element].self))
}
}
extension CompileCommands {
struct Element: Decodable {
var directory: AbsolutePath
var file: AbsolutePath
var output: RelativePath
var command: Command
}
}
extension CompileCommands: RandomAccessCollection {
typealias Index = Int
var startIndex: Index { commands.startIndex }
var endIndex: Index { commands.endIndex }
func index(_ i: Int, offsetBy distance: Int) -> Int {
commands.index(i, offsetBy: distance)
}
subscript(position: Index) -> Element {
commands[position]
}
}

View File

@@ -0,0 +1,30 @@
//===--- CompileLanguage.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
//
//===----------------------------------------------------------------------===//
enum CompileLanguage: Hashable {
case swift, cxx, c
}
extension PathProtocol {
var language: CompileLanguage? {
if hasExtension(.swift) {
return .swift
}
if hasExtension(.cpp) {
return .cxx
}
if hasExtension(.c) {
return .c
}
return nil
}
}

View File

@@ -0,0 +1,81 @@
//===--- FlagSpec.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
//
//===----------------------------------------------------------------------===//
extension Command {
struct FlagSpec {
let flags: [Element]
init(_ flags: [Element]) {
// Sort by shortest first, except in cases where one is a prefix of
// another, in which case we need the longer one first to ensure we prefer
// it when parsing.
self.flags = flags.sorted(by: { lhs, rhs in
let lhs = lhs.flag.name.rawValue
let rhs = rhs.flag.name.rawValue
guard lhs.count != rhs.count else {
return false
}
if lhs.count < rhs.count {
// RHS should be ordered first if it has LHS as a prefix.
return !rhs.hasPrefix(lhs)
} else {
// LHS should be ordered first if it has RHS as a prefix.
return lhs.hasPrefix(rhs)
}
})
}
}
}
extension Command {
struct OptionSpacingSpec: OptionSet {
var rawValue: Int
init(rawValue: Int) {
self.rawValue = rawValue
}
init(_ rawValue: Int) {
self.rawValue = rawValue
}
init(_ optionSpacing: OptionSpacing) {
switch optionSpacing {
case .equals:
self = .equals
case .unspaced:
self = .unspaced
case .spaced:
self = .spaced
}
}
static let equals = Self(1 << 0)
static let unspaced = Self(1 << 1)
static let spaced = Self(1 << 2)
}
}
extension Command.FlagSpec {
typealias Flag = Command.Flag
typealias OptionSpacingSpec = Command.OptionSpacingSpec
struct Element {
let flag: Flag
let spacing: OptionSpacingSpec
init(_ flag: Flag, option: [OptionSpacingSpec]) {
self.flag = flag
self.spacing = option.reduce([], { $0.union($1) })
}
init(_ flag: Flag, option: OptionSpacingSpec...) {
self.init(flag, option: option)
}
}
}

View File

@@ -0,0 +1,283 @@
//===--- KnownCommand.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
//
//===----------------------------------------------------------------------===//
@preconcurrency import SwiftOptions
enum KnownCommand {
case clang, swiftc, swiftFrontend
}
extension PathProtocol {
var knownCommand: KnownCommand? {
switch fileName {
case "clang", "clang++", "c++", "cc", "clang-cache":
.clang
case "swiftc":
.swiftc
case "swift-frontend":
.swiftFrontend
default:
nil
}
}
}
extension Command.Flag {
/// If this spec is for a suboption, returns the command that it is for, or
/// `nil` if it is not for a suboption
var subOptionCommand: KnownCommand? {
switch self {
case .Xcc:
.clang
case .Xfrontend:
.swiftFrontend
default:
nil
}
}
}
extension Command.Flag {
struct Name: Hashable {
let rawValue: String
// Fileprivate because definitions should be added below.
fileprivate init(_ rawValue: String) {
self.rawValue = rawValue
}
}
fileprivate static func dash(_ name: String) -> Self {
dash(.init(name))
}
fileprivate static func doubleDash(_ name: String) -> Self {
doubleDash(.init(name))
}
fileprivate static func swiftc(_ opt: SwiftOptions.Option) -> Self {
let dashes = opt.spelling.prefix(while: { $0 == "-" }).count
guard let dash = Command.Flag.Dash(numDashes: dashes) else {
fatalError("Dash count not handled")
}
let name = String(opt.spelling.dropFirst(dashes))
return .init(dash: dash, name: .init(name))
}
}
extension SwiftOptions.Option {
fileprivate static let optionsWithJoinedEquals: Set<String> = {
// Make a note of all flags that are equal joined.
var result = Set<String>()
for opt in SwiftOptions.Option.allOptions {
switch opt.kind {
case .separate, .input, .flag, .remaining, .multiArg:
continue
case .joined, .joinedOrSeparate, .commaJoined:
if opt.spelling.hasSuffix("=") {
result.insert(String(opt.spelling.dropLast()))
}
}
}
return result
}()
fileprivate var spacingSpec: Command.FlagSpec.OptionSpacingSpec {
var spacing = Command.OptionSpacingSpec()
switch kind {
case .input, .remaining:
fatalError("Not handled")
case .flag:
break
case .joined, .commaJoined:
spacing.insert(.unspaced)
case .separate, .multiArg:
spacing.insert(.spaced)
case .joinedOrSeparate:
spacing.insert([.unspaced, .spaced])
}
if Self.optionsWithJoinedEquals.contains(spelling) {
spacing.insert(.equals)
}
return spacing
}
}
extension Command.FlagSpec {
fileprivate init(_ options: [SwiftOptions.Option]) {
self.init(options.map { .init(.swiftc($0), option: $0.spacingSpec) })
}
}
extension Command.Flag {
// Swift + Clang
static let D = dash("D")
static let I = dash("I")
static let target = dash("target")
// Clang
static let isystem = dash("isystem")
static let isysroot = dash("isysroot")
static let f = dash("f")
static let U = dash("U")
static let W = dash("W")
static let std = dash("std")
// Swift
static let cxxInteroperabilityMode =
swiftc(.cxxInteroperabilityMode)
static let enableExperimentalCxxInterop =
swiftc(.enableExperimentalCxxInterop)
static let enableExperimentalFeature =
swiftc(.enableExperimentalFeature)
static let enableLibraryEvolution =
swiftc(.enableLibraryEvolution)
static let experimentalSkipAllFunctionBodies =
swiftc(.experimentalSkipAllFunctionBodies)
static let experimentalSkipNonInlinableFunctionBodies =
swiftc(.experimentalSkipNonInlinableFunctionBodies)
static let experimentalSkipNonInlinableFunctionBodiesWithoutTypes =
swiftc(.experimentalSkipNonInlinableFunctionBodiesWithoutTypes)
static let F =
swiftc(.F)
static let Fsystem =
swiftc(.Fsystem)
static let moduleName =
swiftc(.moduleName)
static let moduleLinkName =
swiftc(.moduleLinkName)
static let nostdimport =
swiftc(.nostdimport)
static let O =
swiftc(.O)
static let Onone =
swiftc(.Onone)
static let packageName =
swiftc(.packageName)
static let parseAsLibrary =
swiftc(.parseAsLibrary)
static let parseStdlib =
swiftc(.parseStdlib)
static let runtimeCompatibilityVersion =
swiftc(.runtimeCompatibilityVersion)
static let sdk =
swiftc(.sdk)
static let swiftVersion =
swiftc(.swiftVersion)
static let wholeModuleOptimization =
swiftc(.wholeModuleOptimization)
static let wmo =
swiftc(.wmo)
static let Xcc =
swiftc(.Xcc)
static let Xfrontend =
swiftc(.Xfrontend)
static let Xllvm =
swiftc(.Xllvm)
}
extension KnownCommand {
private static let clangSpec = Command.FlagSpec([
.init(.I, option: .equals, .unspaced, .spaced),
.init(.D, option: .unspaced, .spaced),
.init(.U, option: .unspaced, .spaced),
.init(.W, option: .unspaced),
// This isn't an actual Clang flag, but it allows us to scoop up all the
// -f[...] flags.
// FIXME: We ought to see if we can get away with preserving unknown flags.
.init(.f, option: .unspaced),
// FIXME: Really we ought to map to Xcode's SDK
.init(.isystem, option: .unspaced, .spaced),
.init(.isysroot, option: .unspaced, .spaced),
.init(.std, option: .equals),
.init(.target, option: .spaced),
])
// FIXME: We currently only parse a small subset of the supported driver
// options. This is because:
//
// - It avoids including incompatible options (e.g options only suitable when
// emitting modules when we want to do a regular build).
// - It avoids including options that produce unnecessary outputs (e.g
// dependencies, object files), especially as they would be outputting into
// the Ninja build, which needs to be left untouched (maybe we could filter
// out options that have paths that point into the build dir?).
// - It avoids including options that do unnecessary work (e.g emitting debug
// info, code coverage).
// - It's quicker.
//
// This isn't great though, and we probably ought to revisit this, especially
// if the driver starts categorizing its options such that we can better
// reason about which we want to use. It should also be noted that we
// currently allow arbitrary options to be passed through -Xfrontend, we may
// want to reconsider that.
// NOTE: You can pass '--log-level debug' to see the options that are
// currently being missed.
private static let swiftOptions: [SwiftOptions.Option] = [
.cxxInteroperabilityMode,
.D,
.disableAutolinkingRuntimeCompatibilityDynamicReplacements,
.enableBuiltinModule,
.enableExperimentalCxxInterop,
.enableExperimentalFeature,
.enableLibraryEvolution,
.experimentalSkipAllFunctionBodies,
.experimentalSkipNonInlinableFunctionBodies,
.experimentalSkipNonInlinableFunctionBodiesWithoutTypes,
.F,
.Fsystem,
.I,
.nostdimport,
.O,
.Onone,
.moduleName,
.moduleLinkName,
.packageName,
.parseAsLibrary,
.parseStdlib,
.runtimeCompatibilityVersion,
.target,
.sdk,
.swiftVersion,
.wholeModuleOptimization,
.wmo,
.Xcc,
.Xfrontend,
.Xllvm,
]
private static let swiftcSpec = Command.FlagSpec(
swiftOptions.filter { !$0.attributes.contains(.noDriver) } + [
// Not currently listed as a driver option, but it used to be. Include
// for better compatibility.
.enableExperimentalCxxInterop
]
)
private static let swiftFrontendSpec = Command.FlagSpec(
swiftOptions.filter { $0.attributes.contains(.frontend) }
)
var flagSpec: Command.FlagSpec {
switch self {
case .clang:
Self.clangSpec
case .swiftc:
Self.swiftcSpec
case .swiftFrontend:
Self.swiftFrontendSpec
}
}
}

View File

@@ -0,0 +1,43 @@
//===--- Lock.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 os
final class Lock: @unchecked Sendable {
private let lockPtr: UnsafeMutablePointer<os_unfair_lock>
init() {
self.lockPtr = UnsafeMutablePointer<os_unfair_lock>.allocate(capacity: 1)
self.lockPtr.initialize(to: os_unfair_lock())
}
func lock() {
os_unfair_lock_lock(self.lockPtr)
}
func unlock() {
os_unfair_lock_unlock(self.lockPtr)
}
@inline(__always)
func withLock<R>(_ body: () throws -> R) rethrows -> R {
lock()
defer {
unlock()
}
return try body()
}
deinit {
self.lockPtr.deinitialize(count: 1)
self.lockPtr.deallocate()
}
}

View File

@@ -0,0 +1,39 @@
//===--- MutexBox.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
//
//===----------------------------------------------------------------------===//
final class MutexBox<T>: @unchecked Sendable {
private let lock = Lock()
private var value: T
init(_ value: T) {
self.value = value
}
@inline(__always)
func withLock<R>(_ body: (inout T) throws -> R) rethrows -> R {
try lock.withLock {
try body(&value)
}
}
}
extension MutexBox {
convenience init<U>() where T == U? {
self.init(nil)
}
convenience init<U, V>() where T == [U: V] {
self.init([:])
}
convenience init<U>() where T == [U] {
self.init([])
}
}

View File

@@ -0,0 +1,30 @@
//===--- Error.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
//
//===----------------------------------------------------------------------===//
enum XcodeGenError: Error, CustomStringConvertible {
case pathNotFound(AbsolutePath)
case noSwiftBuildDir(AbsolutePath, couldBeParent: Bool)
case expectedParent(AbsolutePath)
var description: String {
switch self {
case .pathNotFound(let basePath):
return "'\(basePath)' not found"
case .noSwiftBuildDir(let basePath, let couldBeParent):
let base = "no swift build directory found in '\(basePath)'"
let note = "; did you mean to pass the path of the parent?"
return couldBeParent ? "\(base)\(note)" : base
case .expectedParent(let basePath):
return "expected '\(basePath)' to have parent directory"
}
}
}

View File

@@ -0,0 +1,98 @@
//===--- ClangTarget.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 ClangTarget {
var name: String
var parentPath: RelativePath
var sources: [Source]
var unbuildableSources: [Source]
var headers: [RelativePath]
init(
name: String, parentPath: RelativePath,
sources: [Source], unbuildableSources: [Source] = [],
headers: [RelativePath]
) {
self.name = name
self.parentPath = parentPath
self.sources = sources
self.unbuildableSources = unbuildableSources
self.headers = headers
}
}
extension ClangTarget {
struct Source {
var path: RelativePath
var inferArgs: Bool
}
}
extension RepoBuildDir {
func getCSourceFilePaths(for path: RelativePath) throws -> [RelativePath] {
try getAllRepoSubpaths(of: path).filter(\.isCSourceLike)
}
func getHeaderFilePaths(for path: RelativePath) throws -> [RelativePath] {
try getAllRepoSubpaths(of: path).filter(\.isHeaderLike)
}
func getClangTarget(
for target: ClangTargetSource, knownUnbuildables: Set<RelativePath>
) throws -> ClangTarget? {
let path = target.path
let name = target.name
let sourcePaths = try getCSourceFilePaths(for: path)
let headers = try getHeaderFilePaths(for: path)
if sourcePaths.isEmpty && headers.isEmpty {
return nil
}
var sources: [ClangTarget.Source] = []
var unbuildableSources: [ClangTarget.Source] = []
for path in sourcePaths {
let source: ClangTarget.Source? =
if try clangArgs.hasBuildArgs(for: path) {
.init(path: path, inferArgs: false)
} else if target.inferArgs {
.init(path: path, inferArgs: true)
} else {
nil
}
guard let source else { continue }
// If we're inferring arguments, or have a known unbuildable, treat as not
// buildable. We'll still include it in the project, but in a separate
// target that isn't built by default.
if source.inferArgs || knownUnbuildables.contains(path) {
unbuildableSources.append(source)
continue
}
// If we have no '.o' present for a given file, assume it's not buildable.
// The 'mayHaveUnbuildableFiles' condition is really only used here to
// reduce IO and only check targets we know are problematic.
if target.mayHaveUnbuildableFiles,
try !clangArgs.isObjectFilePresent(for: path) {
log.debug("! Treating '\(path)' as unbuildable; no '.o' file")
unbuildableSources.append(source)
continue
}
sources.append(source)
}
return ClangTarget(
name: name, parentPath: path, sources: sources,
unbuildableSources: unbuildableSources, headers: headers
)
}
}

View File

@@ -0,0 +1,30 @@
//===--- ClangTargetSource.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
//
//===----------------------------------------------------------------------===//
/// The path at which to find source files for a particular target.
struct ClangTargetSource {
var name: String
var path: RelativePath
var mayHaveUnbuildableFiles: Bool
var inferArgs: Bool
init(
at path: RelativePath, named name: String,
mayHaveUnbuildableFiles: Bool,
inferArgs: Bool
) {
self.name = name
self.path = path
self.mayHaveUnbuildableFiles = mayHaveUnbuildableFiles
self.inferArgs = inferArgs
}
}

View File

@@ -0,0 +1,21 @@
//===--- GeneratedProject.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
//
//===----------------------------------------------------------------------===//
public struct GeneratedProject: Sendable {
public let path: AbsolutePath
let allBuildTargets: [Scheme.BuildTarget]
init(at path: AbsolutePath, allBuildTargets: [Scheme.BuildTarget]) {
self.path = path
self.allBuildTargets = allBuildTargets
}
}

View File

@@ -0,0 +1,745 @@
//===--- 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
private var groups: [RelativePath: Xcode.Group] = [:]
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)
}
return (group(for: parent), RelativePath(path.fileName))
}
private func group(for path: RelativePath) -> Xcode.Group {
if let group = groups[path] {
return group
}
let (parentGroup, childPath) = parentGroup(for: path)
let group = parentGroup.addGroup(
path: childPath.rawPath, pathBase: .groupDir, name: path.fileName
)
groups[path] = 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.hasPrefix($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 {
log.warning("Skipping blue folder '\(path)'; already added")
return nil
}
}
let (parentGroup, childPath) = parentGroup(for: path)
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, allowExcluded: Bool = false
) -> Xcode.FileReference? {
let path = ref.path
guard allowExcluded || 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, productType: Xcode.Target.ProductType?,
includeInAllTarget: Bool
) -> Xcode.Target? {
guard targets[name] == nil else {
log.warning("Duplicate target '\(name)', skipping")
return nil
}
let target = project.addTarget(productType: productType, name: name)
targets[name] = target
if includeInAllTarget {
allTarget.addDependency(on: target)
}
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)
}
}
func generateClangTarget(
_ targetInfo: ClangTarget, includeInAllTarget: Bool = true
) throws {
let targetPath = targetInfo.parentPath
guard checkNotExcluded(targetPath, for: "Clang target") else {
return
}
unbuildableSources += targetInfo.unbuildableSources
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, 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, 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
}
let target = generateBaseTarget(
targetInfo.name, 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)
let moduleName = buildArgs.takePrintedLastValue(for: .moduleName)
buildSettings.common.PRODUCT_MODULE_NAME = moduleName
// 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"
}
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
// 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 var target else { continue }
// We may have a Swift target with the same name, disambiguate.
// FIXME: We ought to be able to support mixed-source targets.
if targets[target.name] != nil {
target.name = "\(target.name)-clang"
}
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
}
}
for ref in spec.referencesToAdd {
// Allow important references to bypass exclusion checks.
getOrCreateRepoRef(ref, allowExcluded: ref.isImportant)
}
// 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)
}
}

View File

@@ -0,0 +1,244 @@
//===--- ProjectSpec.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
//
//===----------------------------------------------------------------------===//
/// The specification for a project to generate.
public struct ProjectSpec {
public var name: String
public var buildDir: RepoBuildDir
public var runnableBuildDir: RepoBuildDir
/// Whether to include Clang targets.
public var addClangTargets: Bool
/// Whether to include Swift targets.
public var addSwiftTargets: Bool
/// Whether to add Swift dependencies to the project.
public var addSwiftDependencies: Bool
/// Whether to add targets for runnable executables.
public var addRunnableTargets: Bool
/// Whether to add a build target for runnable targets, if false they will
/// be added as freestanding schemes.
public var addBuildForRunnableTargets: Bool
/// Whether to infer build arguments for files that don't have any, based
/// on the build arguments of surrounding files.
public var inferArgs: Bool
/// If provided, the paths added will be implicitly appended to this path.
let mainRepoDir: RelativePath?
private(set) var clangTargetSources: [ClangTargetSource] = []
private(set) var swiftTargetSources: [SwiftTargetSource] = []
private(set) var referencesToAdd: [PathReference] = []
private(set) var excludedPaths: [ExcludedPath] = []
private(set) var knownUnbuildables: Set<RelativePath> = []
public init(
_ name: String, for buildDir: RepoBuildDir, runnableBuildDir: RepoBuildDir,
addClangTargets: Bool, addSwiftTargets: Bool,
addSwiftDependencies: Bool, addRunnableTargets: Bool,
addBuildForRunnableTargets: Bool, inferArgs: Bool,
mainRepoDir: RelativePath? = nil
) {
self.name = name
self.buildDir = buildDir
self.runnableBuildDir = runnableBuildDir
self.addClangTargets = addClangTargets
self.addSwiftTargets = addSwiftTargets
self.addSwiftDependencies = addSwiftDependencies
self.addRunnableTargets = addRunnableTargets
self.addBuildForRunnableTargets = addBuildForRunnableTargets
self.inferArgs = inferArgs
self.mainRepoDir = mainRepoDir
}
var repoRoot: AbsolutePath {
buildDir.repoPath
}
}
extension ProjectSpec {
public struct ExcludedPath {
var path: RelativePath
var reason: String?
}
struct PathReference {
enum Kind {
case file, folder
}
var kind: Kind
var path: RelativePath
/// Whether this reference should bypass exclusion checks.
var isImportant: Bool
static func file(_ path: RelativePath, isImportant: Bool = false) -> Self {
.init(kind: .file, path: path, isImportant: isImportant)
}
static func folder(_ path: RelativePath, isImportant: Bool = false) -> Self {
.init(kind: .folder, path: path, isImportant: isImportant)
}
func withPath(_ newPath: RelativePath) -> Self {
var result = self
result.path = newPath
return result
}
}
}
extension ProjectSpec {
private var mainRepoPath: AbsolutePath {
// Add the main repo dir if we were asked to.
if let mainRepoDir {
repoRoot.appending(mainRepoDir)
} else {
repoRoot
}
}
private func mapKnownPath(_ path: RelativePath) -> RelativePath {
// Add the main repo dir if we were asked to.
if let mainRepoDir {
mainRepoDir.appending(path)
} else {
path
}
}
private func mapPath(
_ path: RelativePath, for description: String
) -> RelativePath? {
let path = mapKnownPath(path)
guard repoRoot.appending(path).exists else {
log.warning("Skipping \(description) at '\(path)'; does not exist")
return nil
}
return path
}
}
extension ProjectSpec {
public mutating func addExcludedPath(
_ path: RelativePath, reason: String? = nil
) {
guard let path = mapPath(path, for: "exclusion") else { return }
excludedPaths.append(.init(path: path, reason: reason))
}
public mutating func addUnbuildableFile(_ path: RelativePath) {
guard let path = mapPath(path, for: "unbuildable file") else { return }
self.knownUnbuildables.insert(path)
}
public mutating func addReference(
to path: RelativePath, isImportant: Bool = false
) {
guard let path = mapPath(path, for: "file") else { return }
if repoRoot.appending(path).isDirectory {
if isImportant {
// Important folder references should block anything being added under
// them.
excludedPaths.append(.init(path: path))
}
referencesToAdd.append(.folder(path, isImportant: isImportant))
} else {
referencesToAdd.append(.file(path, isImportant: isImportant))
}
}
public mutating func addHeaders(in path: RelativePath) {
guard let path = mapPath(path, for: "headers") else { return }
do {
for header in try buildDir.getHeaderFilePaths(for: path) {
referencesToAdd.append(.file(header))
}
} catch {
log.warning("Skipping headers in \(path); '\(error)'")
}
}
public mutating func addTopLevelDocs() {
do {
for doc in try mainRepoPath.getDirContents() where doc.isDocLike {
referencesToAdd.append(.file(mapKnownPath(doc)))
}
} catch {
log.warning("Skipping top-level docs for \(repoRoot); '\(error)'")
}
}
public mutating func addDocsGroup(at path: RelativePath) {
guard let path = mapPath(path, for: "docs") else { return }
do {
for doc in try buildDir.getAllRepoSubpaths(of: path) where doc.isDocLike {
referencesToAdd.append(.file(doc))
}
} catch {
log.warning("Skipping docs in \(path); '\(error)'")
}
}
public mutating func addClangTarget(
at path: RelativePath, named name: String? = nil,
mayHaveUnbuildableFiles: Bool = false
) {
guard addClangTargets else { return }
guard let path = mapPath(path, for: "Clang target") else { return }
let name = name ?? path.fileName
clangTargetSources.append(ClangTargetSource(
at: path, named: name,
mayHaveUnbuildableFiles: mayHaveUnbuildableFiles,
inferArgs: inferArgs
))
}
public mutating func addClangTargets(
below path: RelativePath, addingPrefix prefix: String? = nil,
mayHaveUnbuildableFiles: Bool = false
) {
guard addClangTargets else { return }
let originalPath = path
guard let path = mapPath(path, for: "Clang targets") else { return }
let absPath = repoRoot.appending(path)
do {
for child in try absPath.getDirContents() {
guard absPath.appending(child).isDirectory else {
continue
}
var name = child.fileName
if let prefix = prefix {
name = prefix + name
}
addClangTarget(
at: originalPath.appending(child), named: name,
mayHaveUnbuildableFiles: mayHaveUnbuildableFiles
)
}
} catch {
log.warning("Skipping Clang targets in \(path); '\(error)'")
}
}
public mutating func addSwiftTargets(
below path: RelativePath
) {
guard addSwiftTargets else { return }
guard let path = mapPath(path, for: "Swift targets") else { return }
swiftTargetSources.append(SwiftTargetSource(below: path))
}
}

View File

@@ -0,0 +1,172 @@
//===--- SchemeGenerator.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
struct Scheme {
var name: String
var buildAction: BuildAction
var runAction: RunAction?
var replaceExisting: Bool
init(_ name: String, replaceExisting: Bool, buildTargets: [BuildTarget]) {
self.name = name
self.replaceExisting = replaceExisting
self.buildAction = .init(targets: buildTargets)
}
init(_ name: String, replaceExisting: Bool, buildTargets: BuildTarget...) {
self.init(
name, replaceExisting: replaceExisting, buildTargets: buildTargets
)
}
mutating func addBuildTarget(
_ target: Xcode.Target, in path: RelativePath
) {
buildAction.targets.append(.init(target, in: path))
}
}
extension Scheme {
struct BuildAction {
var targets: [BuildTarget]
}
struct BuildTarget {
var name: String
var container: RelativePath
init(_ target: Xcode.Target, in path: RelativePath) {
self.name = target.name
self.container = path
}
init(_ name: String, in path: RelativePath) {
self.name = name
self.container = path
}
}
struct RunAction {
var path: AbsolutePath
}
}
struct SchemeGenerator {
let containerDir: AbsolutePath
let disableAutoCreation: Bool
var schemes: [Scheme] = []
init(in containerDir: AbsolutePath, disableAutoCreation: Bool = true) {
self.containerDir = containerDir
self.disableAutoCreation = disableAutoCreation
}
}
extension SchemeGenerator {
mutating func add(_ scheme: Scheme) {
schemes.append(scheme)
}
}
extension SchemeGenerator {
private func disableAutoCreationIfNeeded() throws {
guard disableAutoCreation else { return }
var relPath: RelativePath = "xcshareddata/WorkspaceSettings.xcsettings"
if containerDir.hasExtension(.xcodeproj) {
relPath = "project.xcworkspace/\(relPath)"
}
let settingsPath = containerDir.appending(relPath)
let workspaceSettings = """
<?xml version="1.0" encoding="UTF-8"?>
<plist version="1.0">
<dict>
<key>IDEWorkspaceSharedSettings_AutocreateContextsIfNeeded</key>
<false/>
</dict>
</plist>
"""
try settingsPath.write(workspaceSettings)
log.info("Generated '\(settingsPath)'")
}
private func writeScheme(_ scheme: Scheme) throws {
let path = containerDir.appending(
"xcshareddata/xcschemes/\(scheme.name).xcscheme"
)
// Don't overwrite if we haven't been asked to.
if !scheme.replaceExisting && path.exists {
return
}
var plist = """
<?xml version="1.0" encoding="UTF-8"?>
<Scheme LastUpgradeVersion = "9999" version = "1.3">
<BuildAction parallelizeBuildables = "YES" buildImplicitDependencies = "YES">
<BuildActionEntries>
"""
for buildTarget in scheme.buildAction.targets {
plist += """
<BuildActionEntry buildForTesting = "YES" buildForRunning = "YES" buildForProfiling = "YES" buildForArchiving = "YES" buildForAnalyzing = "YES">
<BuildableReference
BuildableIdentifier = "primary"
BuildableName = "\(buildTarget.name)"
BlueprintName = "\(buildTarget.name)"
ReferencedContainer = "container:\(buildTarget.container)">
</BuildableReference>
</BuildActionEntry>
"""
}
plist += """
</BuildActionEntries>
</BuildAction>
"""
if let runAction = scheme.runAction {
plist += """
<LaunchAction>
<PathRunnable
FilePath = "\(runAction.path.escaped(addQuotesIfNeeded: false))">
</PathRunnable>
</LaunchAction>
"""
}
plist += """
</Scheme>
"""
try path.write(plist)
log.info("Generated '\(path)'")
}
private func writeSchemes() throws {
for scheme in schemes {
try writeScheme(scheme)
}
}
func write() throws {
try disableAutoCreationIfNeeded()
try writeSchemes()
}
}

View File

@@ -0,0 +1,69 @@
//===--- SwiftTarget.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
//
//===----------------------------------------------------------------------===//
final class SwiftTarget {
let name: String
let moduleName: String
var buildRule: BuildRule?
var emitModuleRule: EmitModuleRule?
var dependencies: [SwiftTarget] = []
init(name: String, moduleName: String) {
self.name = name
self.moduleName = moduleName
}
}
extension SwiftTarget: Hashable {
static func == (lhs: SwiftTarget, rhs: SwiftTarget) -> Bool {
ObjectIdentifier(lhs) == ObjectIdentifier(rhs)
}
func hash(into hasher: inout Hasher) {
hasher.combine(ObjectIdentifier(self))
}
}
extension SwiftTarget: CustomDebugStringConvertible {
var debugDescription: String {
name
}
}
extension SwiftTarget {
struct Sources {
var repoSources: [RelativePath] = []
var externalSources: [AbsolutePath] = []
}
struct BuildRule {
var parentPath: RelativePath?
var sources: Sources
var buildArgs: BuildArgs
}
struct EmitModuleRule {
var sources: Sources
var buildArgs: BuildArgs
}
}
extension SwiftTarget {
var buildArgs: BuildArgs {
buildRule?.buildArgs ?? emitModuleRule?.buildArgs ?? .init(for: .swiftc)
}
}
extension RepoBuildDir {
func getSwiftTargets(for source: SwiftTargetSource) throws -> [SwiftTarget] {
try swiftTargets.getTargets(below: source.path)
}
}

View File

@@ -0,0 +1,19 @@
//===--- SwiftTargetSource.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 SwiftTargetSource {
var path: RelativePath
init(below path: RelativePath) {
self.path = path
}
}

View File

@@ -0,0 +1,87 @@
//===--- WorkspaceGenerator.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
//
//===----------------------------------------------------------------------===//
public struct WorkspaceGenerator {
var elements: [Element] = []
public init() {}
}
public extension WorkspaceGenerator {
enum Element {
case xcodeProj(GeneratedProject)
case group(RelativePath, targets: [String])
}
mutating func addProject(_ proj: GeneratedProject) {
elements.append(.xcodeProj(proj))
}
mutating func addGroup(at path: RelativePath, targets: [String]) {
elements.append(.group(path, targets: targets))
}
func write(_ name: String, into dir: AbsolutePath) throws {
var contents = """
<?xml version="1.0" encoding="UTF-8"?>
<Workspace version = "1.0">
"""
for element in elements {
contents += "<FileRef location = "
switch element {
case .xcodeProj(let proj):
// FIXME: This is assuming the workspace will be siblings with the
// project.
contents += "\"container:\(proj.path.fileName)\""
case .group(let path, _):
contents += "\"group:\(path)\""
}
contents += "></FileRef>\n"
}
contents += "</Workspace>"
let workspaceDir = dir.appending("\(name).xcworkspace")
// Skip generating if there's only a single container and it doesn't already
// exist.
guard elements.count > 1 || workspaceDir.exists else { return }
let dataPath = workspaceDir.appending("contents.xcworkspacedata")
try dataPath.write(contents)
log.info("Generated '\(dataPath)'")
var schemes = SchemeGenerator(in: workspaceDir)
let buildTargets = elements
.sorted(by: {
// Sort project schemes first.
switch ($0, $1) {
case (.xcodeProj, .group):
return true
default:
return false
}
})
.flatMap { elt in
switch elt {
case .xcodeProj(let proj):
return proj.allBuildTargets
case .group(let path, let targets):
return targets.map { target in
Scheme.BuildTarget(target, in: path)
}
}
}
schemes.add(Scheme(
"ALL", replaceExisting: true, buildTargets: buildTargets
))
try schemes.write()
}
}

View File

@@ -0,0 +1,161 @@
//===--- AnsiColor.swift - ANSI formatting control codes ------------------===//
//
// This source file is part of the Swift.org open source project
//
// Copyright (c) 2022-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
//
//===----------------------------------------------------------------------===//
//
// Provides ANSI support via Swift string interpolation.
//
//===----------------------------------------------------------------------===//
// https://github.com/apple/swift/blob/f08f86c7/stdlib/public/libexec/swift-backtrace/AnsiColor.swift
enum AnsiColor {
case normal
case black
case red
case green
case yellow
case blue
case magenta
case cyan
case white
case gray
case brightRed
case brightGreen
case brightYellow
case brightBlue
case brightMagenta
case brightCyan
case brightWhite
case rgb(r: Int, g: Int, b: Int)
case grayscale(Int)
var foregroundCode: String {
switch self {
case .normal: return "39"
case .black: return "30"
case .red: return "31"
case .green: return "32"
case .yellow: return "33"
case .blue: return "34"
case .cyan: return "35"
case .magenta: return "36"
case .white: return "37"
case .gray: return "90"
case .brightRed: return "91"
case .brightGreen: return "92"
case .brightYellow: return "93"
case .brightBlue: return "94"
case .brightCyan: return "95"
case .brightMagenta: return "96"
case .brightWhite: return "97"
case let .rgb(r, g, b):
let ndx = 16 + 36 * r + 6 * g + b
return "38;5;\(ndx)"
case let .grayscale(g):
let ndx = 232 + g
return "38;5;\(ndx)"
}
}
var backgroundCode: String {
switch self {
case .normal: return "49"
case .black: return "40"
case .red: return "41"
case .green: return "42"
case .yellow: return "43"
case .blue: return "44"
case .cyan: return "45"
case .magenta: return "46"
case .white: return "47"
case .gray: return "100"
case .brightRed: return "101"
case .brightGreen: return "102"
case .brightYellow: return "103"
case .brightBlue: return "104"
case .brightCyan: return "105"
case .brightMagenta: return "106"
case .brightWhite: return "107"
case let .rgb(r, g, b):
let ndx = 16 + 36 * r + 6 * g + b
return "48;5;\(ndx)"
case let .grayscale(g):
let ndx = 232 + g
return "48;5;\(ndx)"
}
}
}
enum AnsiWeight {
case normal
case bold
case faint
var code: String {
switch self {
case .normal: "22"
case .bold: "1"
case .faint: "2"
}
}
}
enum AnsiAttribute {
case fg(AnsiColor)
case bg(AnsiColor)
case weight(AnsiWeight)
case inverse(Bool)
}
extension DefaultStringInterpolation {
mutating func appendInterpolation(ansi attrs: AnsiAttribute...) {
var code = "\u{1b}["
var first = true
for attr in attrs {
if first {
first = false
} else {
code += ";"
}
switch attr {
case let .fg(color):
code += color.foregroundCode
case let .bg(color):
code += color.backgroundCode
case let .weight(weight):
code += weight.code
case let .inverse(enabled):
if enabled {
code += "7"
} else {
code += "27"
}
}
}
code += "m"
appendInterpolation(code)
}
mutating func appendInterpolation(fg: AnsiColor) {
appendInterpolation(ansi: .fg(fg))
}
mutating func appendInterpolation(bg: AnsiColor) {
appendInterpolation(ansi: .bg(bg))
}
mutating func appendInterpolation(weight: AnsiWeight) {
appendInterpolation(ansi: .weight(weight))
}
mutating func appendInterpolation(inverse: Bool) {
appendInterpolation(ansi: .inverse(inverse))
}
}

View File

@@ -0,0 +1,176 @@
//===--- Logger.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 Foundation
public final class Logger: @unchecked Sendable {
private let stateLock = Lock()
private let outputLock = Lock()
private var _hadError = false
public var hadError: Bool {
get { stateLock.withLock { _hadError } }
set { stateLock.withLock { _hadError = newValue } }
}
private var _logLevel: LogLevel = .debug
public var logLevel: LogLevel {
get { stateLock.withLock { _logLevel } }
set { stateLock.withLock { _logLevel = newValue } }
}
private var _useColor: Bool = true
public var useColor: Bool {
get { stateLock.withLock { _useColor } }
set { stateLock.withLock { _useColor = newValue } }
}
private var _output: LoggableStream?
public var output: LoggableStream? {
get { stateLock.withLock { _output } }
set { stateLock.withLock { _output = newValue } }
}
public init() {}
}
extension Logger {
public enum LogLevel: Comparable {
/// A message with information that isn't useful to the user, but is
/// useful when debugging issues.
case debug
/// A message with mundane information that may be useful to know if
/// you're interested in verbose output, but is otherwise unimportant.
case info
/// A message with information that does not require any intervention from
/// the user, but is nonetheless something they may want to be aware of.
case note
/// A message that describes an issue that ought to be resolved by the
/// user, but still allows the program to exit successfully.
case warning
/// A message that describes an issue where the program cannot exit
/// successfully.
case error
}
private func log(_ message: @autoclosure () -> String, level: LogLevel) {
guard level >= logLevel else { return }
let output = self.output ?? FileHandleStream.stderr
let useColor = self.useColor && output.supportsColor
outputLock.withLock {
level.write(to: output, useColor: useColor)
output.write(": \(message())\n")
}
}
public func debug(_ message: @autoclosure () -> String) {
log(message(), level: .debug)
}
public func info(_ message: @autoclosure () -> String) {
log(message(), level: .info)
}
public func note(_ message: @autoclosure () -> String) {
log(message(), level: .note)
}
public func warning(_ message: @autoclosure () -> String) {
log(message(), level: .warning)
}
public func error(_ message: @autoclosure () -> String) {
hadError = true
log(message(), level: .error)
}
}
public protocol Loggable {
func write(to stream: LoggableStream, useColor: Bool)
}
extension Logger.LogLevel: Loggable, CustomStringConvertible {
public var description: String {
switch self {
case .debug: "debug"
case .info: "info"
case .note: "note"
case .warning: "warning"
case .error: "error"
}
}
private var ansiColor: AnsiColor {
switch self {
case .debug: .magenta
case .info: .blue
case .note: .brightCyan
case .warning: .brightYellow
case .error: .brightRed
}
}
public func write(to stream: LoggableStream, useColor: Bool) {
let str = useColor
? "\(fg: ansiColor)\(weight: .bold)\(self)\(fg: .normal)\(weight: .normal)"
: "\(self)"
stream.write(str)
}
}
public protocol LoggableStream: Sendable {
var supportsColor: Bool { get }
func write(_: String)
}
/// Check whether $TERM supports color. Ideally we'd consult terminfo, but
/// there aren't any particularly nice APIs for that in the SDK AFAIK. We could
/// shell out to tput, but that adds ~100ms of overhead which I don't think is
/// worth it. This simple check (taken from LLVM) is good enough for now.
fileprivate let termSupportsColor: Bool = {
guard let termEnv = getenv("TERM") else { return false }
switch String(cString: termEnv) {
case "ansi", "cygwin", "linux":
return true
case let term where
term.hasPrefix("screen") ||
term.hasPrefix("xterm") ||
term.hasPrefix("vt100") ||
term.hasPrefix("rxvt") ||
term.hasSuffix("color"):
return true
default:
return false
}
}()
public struct FileHandleStream: LoggableStream, @unchecked Sendable {
public let handle: UnsafeMutablePointer<FILE>
public let supportsColor: Bool
public init(_ handle: UnsafeMutablePointer<FILE>) {
self.handle = handle
self.supportsColor = isatty(fileno(handle)) != 0 && termSupportsColor
}
public func write(_ string: String) {
fputs(string, handle)
}
}
extension FileHandleStream {
static let stdout = Self(Darwin.stdout)
static let stderr = Self(Darwin.stderr)
}
public let log = Logger()

View File

@@ -0,0 +1,28 @@
//===--- BuildConfiguration.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
//
//===----------------------------------------------------------------------===//
public enum BuildConfiguration: String {
case release = "Release"
case releaseWithDebugInfo = "RelWithDebInfo"
case debug = "Debug"
}
extension BuildConfiguration {
public var hasDebugInfo: Bool {
switch self {
case .release:
false
case .releaseWithDebugInfo, .debug:
true
}
}
}

View File

@@ -0,0 +1,65 @@
//===--- NinjaBuildDir.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
//
//===----------------------------------------------------------------------===//
public final class NinjaBuildDir: Sendable {
public let path: AbsolutePath
public let projectRootDir: AbsolutePath
public let tripleSuffix: String
private let repoBuildDirs = MutexBox<[Repo: RepoBuildDir]>()
private static func detectTripleSuffix(
buildDir: AbsolutePath
) throws -> String {
for dir in try buildDir.getDirContents() {
guard buildDir.appending(dir).isDirectory,
let triple = dir.fileName.tryDropPrefix("swift-") else {
continue
}
return triple
}
let couldBeParent = buildDir.fileName.hasPrefix("swift-")
throw XcodeGenError.noSwiftBuildDir(buildDir, couldBeParent: couldBeParent)
}
private static func detectProjectRoot(
buildDir: AbsolutePath
) throws -> AbsolutePath {
guard let parent = buildDir.parentDir else {
throw XcodeGenError.expectedParent(buildDir)
}
guard let projectDir = parent.parentDir else {
throw XcodeGenError.expectedParent(parent)
}
return projectDir
}
public init(at path: AbsolutePath, projectRootDir: AbsolutePath?) throws {
guard path.exists else {
throw XcodeGenError.pathNotFound(path)
}
self.path = path
self.tripleSuffix = try Self.detectTripleSuffix(buildDir: path)
self.projectRootDir = try projectRootDir ?? Self.detectProjectRoot(buildDir: path)
}
public func buildDir(for repo: Repo) throws -> RepoBuildDir {
try repoBuildDirs.withLock { repoBuildDirs in
if let buildDir = repoBuildDirs[repo] {
return buildDir
}
let dir = try RepoBuildDir(repo, for: self)
repoBuildDirs[repo] = dir
return dir
}
}
}

View File

@@ -0,0 +1,107 @@
//===--- NinjaBuildFile.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 NinjaBuildFile {
var attributes: [Attribute.Key: Attribute]
var buildRules: [BuildRule] = []
init(
attributes: [Attribute.Key: Attribute],
buildRules: [BuildRule]
) {
self.attributes = attributes
self.buildRules = buildRules
}
}
extension NinjaBuildFile {
var buildConfiguration: BuildConfiguration? {
attributes[.configuration]
.flatMap { BuildConfiguration(rawValue: $0.value) }
}
}
extension NinjaBuildFile {
struct BuildRule: Hashable {
let inputs: [String]
let outputs: [String]
let dependencies: [String]
let attributes: [Attribute.Key: Attribute]
private(set) var isPhony = false
init(
inputs: [String], outputs: [String], dependencies: [String],
attributes: [Attribute.Key : Attribute]
) {
self.inputs = inputs
self.outputs = outputs
self.dependencies = dependencies
self.attributes = attributes
}
static func phony(for outputs: [String], inputs: [String]) -> Self {
var rule = Self(
inputs: inputs, outputs: outputs, dependencies: [], attributes: [:]
)
rule.isPhony = true
return rule
}
}
}
extension NinjaBuildFile {
struct Attribute: Hashable {
var key: Key
var value: String
}
}
extension NinjaBuildFile: CustomDebugStringConvertible {
var debugDescription: String {
buildRules.map(\.debugDescription).joined(separator: "\n")
}
}
extension NinjaBuildFile.BuildRule: CustomDebugStringConvertible {
var debugDescription: String {
"""
{
inputs: \(inputs)
outputs: \(outputs)
dependencies: \(dependencies)
attributes: \(attributes)
isPhony: \(isPhony)
}
"""
}
}
extension NinjaBuildFile.Attribute: CustomStringConvertible {
var description: String {
"\(key.rawValue) = \(value)"
}
}
extension NinjaBuildFile.Attribute {
enum Key: String {
case configuration = "CONFIGURATION"
case defines = "DEFINES"
case flags = "FLAGS"
case includes = "INCLUDES"
case swiftModule = "SWIFT_MODULE"
case swiftModuleName = "SWIFT_MODULE_NAME"
case swiftLibraryName = "SWIFT_LIBRARY_NAME"
case swiftSources = "SWIFT_SOURCES"
case command = "COMMAND"
}
}

View File

@@ -0,0 +1,326 @@
//===--- NinjaParser.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 Foundation
struct NinjaParser {
private var lexer: Lexer
private init(_ input: UnsafeRawBufferPointer) throws {
self.lexer = Lexer(ByteScanner(input))
}
static func parse(_ input: Data) throws -> NinjaBuildFile {
try input.withUnsafeBytes { bytes in
var parser = try Self(bytes)
return try parser.parse()
}
}
}
fileprivate enum NinjaParseError: Error {
case badAttribute
case expected(NinjaParser.Lexeme)
}
fileprivate extension ByteScanner {
mutating func consumeUnescaped(
while pred: (Byte) -> Bool
) -> String? {
let bytes = consume(using: { consumer in
guard let c = consumer.peek, pred(c) else { return false }
// Ninja uses '$' as the escape character.
if c == "$" {
switch consumer.peek(ahead: 1) {
case let c? where c.isSpaceOrTab:
fallthrough
case "$", ":":
// Skip the '$' and take the unescaped character.
consumer.skip()
return consumer.eat()
case let c? where c.isNewline:
// This is a line continuation, skip the newline, and strip any
// following space.
consumer.skip(untilAfter: \.isNewline)
consumer.skip(while: \.isSpaceOrTab)
return true
default:
// Unknown escape sequence, treat the '$' literally.
break
}
}
return consumer.eat()
})
return bytes.isEmpty ? nil : String(utf8: bytes)
}
}
fileprivate extension NinjaParser {
typealias BuildRule = NinjaBuildFile.BuildRule
typealias Attribute = NinjaBuildFile.Attribute
struct ParsedAttribute: Hashable {
var key: String
var value: String
}
enum Lexeme: Hashable {
case attribute(ParsedAttribute)
case element(String)
case build
case newline
case colon
case equal
case pipe
case doublePipe
}
struct Lexer {
private var input: ByteScanner
private(set) var lexeme: Lexeme?
private(set) var isAtStartOfLine = true
private(set) var leadingTriviaCount = 0
init(_ input: ByteScanner) {
self.input = input
self.lexeme = lex()
}
}
var peek: Lexeme? { lexer.lexeme }
@discardableResult
mutating func tryEat(_ lexeme: Lexeme) -> Bool {
guard peek == lexeme else { return false }
eat()
return true
}
mutating func tryEatElement() -> String? {
guard case .element(let str) = peek else { return nil }
eat()
return str
}
@discardableResult
mutating func eat() -> Lexeme? {
defer {
lexer.eat()
}
return peek
}
}
fileprivate extension Byte {
var isNinjaOperator: Bool {
switch self {
case ":", "|", "=":
true
default:
false
}
}
}
fileprivate extension NinjaBuildFile.Attribute {
init?(_ parsed: NinjaParser.ParsedAttribute) {
// Ignore unknown attributes for now.
guard let key = Key(rawValue: parsed.key) else { return nil }
self.init(key: key, value: parsed.value)
}
}
extension NinjaParser.Lexer {
typealias Lexeme = NinjaParser.Lexeme
private mutating func consumeOperator() -> Lexeme {
switch input.eat() {
case ":":
return .colon
case "=":
return .equal
case "|":
if input.tryEat("|") {
return .doublePipe
}
return .pipe
default:
fatalError("Invalid operator character")
}
}
private mutating func consumeElement() -> String? {
input.consumeUnescaped(while: { char in
switch char {
case let c where c.isNinjaOperator || c.isSpaceTabOrNewline:
false
default:
true
}
})
}
private mutating func tryConsumeAttribute(key: String) -> Lexeme? {
input.tryEating { input in
input.skip(while: \.isSpaceOrTab)
guard input.tryEat("=") else { return nil }
input.skip(while: \.isSpaceOrTab)
guard let value = input.consumeUnescaped(while: { !$0.isNewline }) else {
return nil
}
return .attribute(.init(key: key, value: value))
}
}
private mutating func lex() -> Lexeme? {
while true {
isAtStartOfLine = input.previous?.isNewline ?? true
leadingTriviaCount = input.eat(while: \.isSpaceOrTab)?.count ?? 0
guard let c = input.peek else { return nil }
if c == "#" {
input.skip(untilAfter: \.isNewline)
continue
}
if c.isNewline {
input.skip(untilAfter: \.isNewline)
if isAtStartOfLine {
// Ignore empty lines, newlines are only semantically meaningful
// when they delimit non-empty lines.
continue
}
return .newline
}
if c.isNinjaOperator {
return consumeOperator()
}
if isAtStartOfLine && input.tryEat(utf8: "build") {
return .build
}
guard let element = consumeElement() else { return nil }
// If we're on a newline, check to see if we can lex an attribute.
if isAtStartOfLine {
if let attr = tryConsumeAttribute(key: element) {
return attr
}
}
return .element(element)
}
}
@discardableResult
mutating func eat() -> Lexeme? {
defer {
lexeme = lex()
}
return lexeme
}
}
fileprivate extension NinjaParser {
mutating func skipLine() {
while let lexeme = eat(), lexeme != .newline {}
}
mutating func parseAttribute() throws -> ParsedAttribute? {
guard case let .attribute(attr) = peek else { return nil }
eat()
tryEat(.newline)
return attr
}
mutating func parseBuildRule() throws -> BuildRule? {
let indent = lexer.leadingTriviaCount
guard tryEat(.build) else { return nil }
var outputs: [String] = []
while let str = tryEatElement() {
outputs.append(str)
}
// Ignore implicit outputs for now.
if tryEat(.pipe) {
while tryEatElement() != nil {}
}
guard tryEat(.colon) else {
throw NinjaParseError.expected(.colon)
}
var isPhony = false
var inputs: [String] = []
while let str = tryEatElement() {
if str == "phony" {
isPhony = true
} else {
inputs.append(str)
}
}
if isPhony {
skipLine()
return .phony(for: outputs, inputs: inputs)
}
var dependencies: [String] = []
while true {
if let str = tryEatElement() {
dependencies.append(str)
continue
}
if tryEat(.pipe) || tryEat(.doublePipe) {
// Currently we don't distinguish between implicit and explicit deps.
continue
}
break
}
// We're done with the line, skip to the next.
skipLine()
var attributes: [Attribute.Key: Attribute] = [:]
while indent < lexer.leadingTriviaCount, let attr = try parseAttribute() {
if let attr = Attribute(attr) {
attributes[attr.key] = attr
}
}
return BuildRule(
inputs: inputs,
outputs: outputs,
dependencies: dependencies,
attributes: attributes
)
}
mutating func parse() throws -> NinjaBuildFile {
var buildRules: [BuildRule] = []
var attributes: [Attribute.Key: Attribute] = [:]
while peek != nil {
if let rule = try parseBuildRule() {
buildRules.append(rule)
continue
}
if let attr = try parseAttribute() {
if let attr = Attribute(attr) {
attributes[attr.key] = attr
}
continue
}
// Ignore unknown bits of syntax like 'include' for now.
eat()
}
return NinjaBuildFile(attributes: attributes, buildRules: buildRules)
}
}

View File

@@ -0,0 +1,115 @@
//===--- RepoBuildDir.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
//
//===----------------------------------------------------------------------===//
public final class RepoBuildDir: Sendable {
public let projectRootDir: AbsolutePath
public let repo: Repo
public let path: AbsolutePath
public let repoPath: AbsolutePath
public let repoRelativePath: RelativePath
private let repoDirCache: DirectoryCache
private let _ninjaFile = MutexBox<NinjaBuildFile?>()
private let _runnableTargets = MutexBox<RunnableTargets?>()
private let _clangArgs = MutexBox<ClangBuildArgsProvider?>()
private let _swiftTargets = MutexBox<SwiftTargets?>()
init(_ repo: Repo, for parent: NinjaBuildDir) throws {
self.projectRootDir = parent.projectRootDir
self.repo = repo
self.path = parent.path.appending(
"\(repo.buildDirPrefix)-\(parent.tripleSuffix)"
)
self.repoRelativePath = repo.relativePath
self.repoPath = projectRootDir.appending(repo.relativePath)
self.repoDirCache = DirectoryCache(root: repoPath)
guard self.path.exists else {
throw XcodeGenError.pathNotFound(self.path)
}
guard self.repoPath.exists else {
throw XcodeGenError.pathNotFound(self.repoPath)
}
}
}
extension RepoBuildDir {
var clangArgs: ClangBuildArgsProvider {
get throws {
try _clangArgs.withLock { _clangArgs in
if let clangArgs = _clangArgs {
return clangArgs
}
let clangArgs = try ClangBuildArgsProvider(for: self)
_clangArgs = clangArgs
return clangArgs
}
}
}
var swiftTargets: SwiftTargets {
get throws {
try _swiftTargets.withLock { _swiftTargets in
if let swiftTargets = _swiftTargets {
return swiftTargets
}
let swiftTargets = try SwiftTargets(for: self)
_swiftTargets = swiftTargets
return swiftTargets
}
}
}
var ninjaFile: NinjaBuildFile {
get throws {
try _ninjaFile.withLock { _ninjaFile in
if let ninjaFile = _ninjaFile {
return ninjaFile
}
let fileName = path.appending("build.ninja")
guard fileName.exists else {
throw XcodeGenError.pathNotFound(fileName)
}
log.debug("[*] Reading '\(fileName)'")
let ninjaFile = try NinjaParser.parse(fileName.read())
_ninjaFile = ninjaFile
return ninjaFile
}
}
}
var runnableTargets: RunnableTargets {
get throws {
try _runnableTargets.withLock { _runnableTargets in
if let runnableTargets = _runnableTargets {
return runnableTargets
}
let runnableTargets = try RunnableTargets(from: self)
_runnableTargets = runnableTargets
return runnableTargets
}
}
}
public var buildConfiguration: BuildConfiguration? {
get throws {
try ninjaFile.buildConfiguration
}
}
func getAllRepoSubpaths(of parent: RelativePath) throws -> [RelativePath] {
try repoDirCache.getAllSubpaths(of: parent)
}
}

View File

@@ -0,0 +1,115 @@
//===--- AbsolutePath.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 Foundation
import System
public struct AbsolutePath: PathProtocol, Sendable {
public let storage: FilePath
public init(_ storage: FilePath) {
precondition(
storage.isAbsolute, "'Expected \(storage)' to be an absolute path"
)
self.storage = storage.lexicallyNormalized()
}
public var asAnyPath: AnyPath {
.absolute(self)
}
}
public extension AbsolutePath {
var isDirectory: Bool {
var isDir: ObjCBool = false
guard FileManager.default.fileExists(atPath: rawPath, isDirectory: &isDir)
else {
return false
}
return isDir.boolValue
}
func getDirContents() throws -> [RelativePath] {
try FileManager.default.contentsOfDirectory(atPath: rawPath).map { .init($0) }
}
var exists: Bool {
FileManager.default.fileExists(atPath: rawPath)
}
var isExecutable: Bool {
FileManager.default.isExecutableFile(atPath: rawPath)
}
var isSymlink: Bool {
(try? FileManager.default.destinationOfSymbolicLink(atPath: rawPath)) != nil
}
var resolvingSymlinks: Self {
guard let resolved = realpath(rawPath, nil) else { return self }
defer {
free(resolved)
}
return Self(String(cString: resolved))
}
func makeDir(withIntermediateDirectories: Bool = true) throws {
try FileManager.default.createDirectory(
atPath: rawPath, withIntermediateDirectories: withIntermediateDirectories)
}
func remove() {
try? FileManager.default.removeItem(atPath: rawPath)
}
func symlink(to dest: AbsolutePath) throws {
try parentDir?.makeDir()
if isSymlink {
remove()
}
try FileManager.default.createSymbolicLink(
atPath: rawPath, withDestinationPath: dest.rawPath
)
}
func read() throws -> Data {
try Data(contentsOf: URL(fileURLWithPath: rawPath))
}
func write(_ data: Data, as encoding: String.Encoding = .utf8) throws {
try parentDir?.makeDir()
FileManager.default.createFile(atPath: rawPath, contents: data)
}
func write(_ contents: String, as encoding: String.Encoding = .utf8) throws {
try write(contents.data(using: encoding)!)
}
}
extension AbsolutePath: ExpressibleByStringLiteral, ExpressibleByStringInterpolation {
public init(stringLiteral value: StringLiteralType) {
self.init(value)
}
}
extension AbsolutePath: Decodable {
public init(from decoder: Decoder) throws {
let storage = FilePath(
try decoder.singleValueContainer().decode(String.self)
)
guard storage.isAbsolute else {
struct NotAbsoluteError: Error, Sendable {
let path: FilePath
}
throw NotAbsoluteError(path: storage)
}
self.init(storage)
}
}

View File

@@ -0,0 +1,73 @@
//===--- AnyPath.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 System
public enum AnyPath: PathProtocol, Sendable {
case relative(RelativePath)
case absolute(AbsolutePath)
public init<P: PathProtocol>(_ path: P) {
self = path.asAnyPath
}
public init(_ storage: FilePath) {
if storage.isAbsolute {
self = .absolute(.init(storage))
} else {
self = .relative(.init(storage))
}
}
public var storage: FilePath {
switch self {
case .relative(let r):
r.storage
case .absolute(let a):
a.storage
}
}
public var asAnyPath: AnyPath {
self
}
}
extension AnyPath {
public var absoluteInWorkingDir: AbsolutePath {
switch self {
case .relative(let r):
r.absoluteInWorkingDir
case .absolute(let a):
a
}
}
}
extension AnyPath: Decodable {
public init(from decoder: Decoder) throws {
self.init(try decoder.singleValueContainer().decode(String.self))
}
}
extension AnyPath: ExpressibleByArgument {
public init(argument rawPath: String) {
self.init(rawPath)
}
}
extension StringProtocol {
func hasExtension(_ ext: FileExtension) -> Bool {
FilePath(String(self)).extension == ext.rawValue
}
}

View File

@@ -0,0 +1,38 @@
//===--- DirectoryCache.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 Foundation
/// A simple cache for the recursive contents of a directory under a given
/// root path. This is pretty basic and doesn't handle cases where we've already
/// cached the parent.
struct DirectoryCache {
private let root: AbsolutePath
private let storage = MutexBox<[RelativePath: [RelativePath]]>()
init(root: AbsolutePath) {
self.root = root
}
func getAllSubpaths(of path: RelativePath) throws -> [RelativePath] {
if let result = storage.withLock(\.[path]) {
return result
}
let absPath = root.appending(path).rawPath
let result = try FileManager.default.subpathsOfDirectory(atPath: absPath)
.map { path.appending($0) }
storage.withLock { storage in
storage[path] = result
}
return result
}
}

View File

@@ -0,0 +1,27 @@
//===--- FileExtension.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
//
//===----------------------------------------------------------------------===//
public enum FileExtension: String {
case c
case cpp
case def
case gyb
case h
case md
case modulemap
case o
case rst
case swift
case swiftmodule
case td
case xcodeproj
}

View File

@@ -0,0 +1,134 @@
//===--- PathProtocol.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 System
public protocol PathProtocol: Hashable, CustomStringConvertible {
var storage: FilePath { get }
var asAnyPath: AnyPath { get }
init(_ storage: FilePath)
}
public extension PathProtocol {
typealias Component = FilePath.Component
var parentDir: Self? {
// Remove the last component and check to see if it's empty.
var result = storage
guard result.removeLastComponent(), !result.isEmpty else { return nil }
return Self(result)
}
var fileName: String {
storage.lastComponent?.string ?? ""
}
func appending(_ relPath: RelativePath) -> Self {
Self(storage.pushing(relPath.storage))
}
func appending(_ str: String) -> Self {
Self(storage.appending(str))
}
func commonAncestor(with other: Self) -> Self {
precondition(storage.root == other.storage.root)
var result = [Component]()
for (comp, otherComp) in zip(components, other.components) {
guard comp == otherComp else { break }
result.append(comp)
}
return Self(FilePath(root: storage.root, result))
}
/// Attempt to remove `other` as a prefix of `self`, or `nil` if `other` is
/// not a prefix of `self`.
func removingPrefix(_ other: Self) -> RelativePath? {
var result = storage
guard result.removePrefix(other.storage) else { return nil }
return RelativePath(result)
}
func hasExtension(_ ext: FileExtension) -> Bool {
storage.extension == ext.rawValue
}
func hasExtension(_ exts: FileExtension...) -> Bool {
// Note that querying `.extension` involves re-parsing, so only do it
// once here.
let ext = storage.extension
return exts.contains(where: { ext == $0.rawValue })
}
func hasPrefix(_ other: Self) -> Bool {
rawPath.hasPrefix(other.rawPath)
}
var components: FilePath.ComponentView {
storage.components
}
var description: String { storage.string }
init(stringLiteral value: String) {
self.init(value)
}
init(_ rawPath: String) {
self.init(FilePath(rawPath))
}
var rawPath: String {
storage.string
}
func escaped(addQuotesIfNeeded: Bool) -> String {
rawPath.escaped(addQuotesIfNeeded: addQuotesIfNeeded)
}
var escaped: String {
rawPath.escaped
}
}
extension PathProtocol {
/// Whether this is a .swift.gyb file.
var isSwiftGyb: Bool {
hasExtension(.gyb) && rawPath.dropLast(4).hasExtension(.swift)
}
var isHeaderLike: Bool {
if hasExtension(.h, .def, .td, .modulemap) {
return true
}
// Consider all gyb files to be header-like, except .swift.gyb, which
// will be handled separately when creating Swift targets.
if hasExtension(.gyb) && !isSwiftGyb {
return true
}
return false
}
var isCSourceLike: Bool {
hasExtension(.c, .cpp)
}
var isDocLike: Bool {
hasExtension(.md, .rst) || fileName.starts(with: "README")
}
}
extension Collection where Element: PathProtocol {
var commonAncestor: Element? {
guard let first = self.first else { return nil }
return dropFirst().reduce(first, { $0.commonAncestor(with: $1) })
}
}

View File

@@ -0,0 +1,71 @@
//===--- RelativePath.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 Foundation
import System
public struct RelativePath: PathProtocol, Sendable {
public let storage: FilePath
public init(_ storage: FilePath) {
precondition(
storage.isRelative, "Expected '\(storage)' to be a relative path"
)
self.storage = storage.lexicallyNormalized()
}
private init(normalizedComponents: FilePath.ComponentView.SubSequence) {
// Already normalized, no need to do it ourselves.
self.storage = FilePath(root: nil, normalizedComponents)
}
public var asAnyPath: AnyPath {
.relative(self)
}
}
public extension RelativePath {
var absoluteInWorkingDir: AbsolutePath {
.init(FileManager.default.currentDirectoryPath).appending(self)
}
init(_ component: Component) {
self.init(FilePath(root: nil, components: component))
}
/// Incrementally stacked components of the path, starting at the parent.
/// e.g for a/b/c, returns [a, a/b, a/b/c].
@inline(__always)
var stackedComponents: [RelativePath] {
let components = self.components
var stackedComponents: [RelativePath] = []
var index = components.startIndex
while index != components.endIndex {
stackedComponents.append(
RelativePath(normalizedComponents: components[...index])
)
components.formIndex(after: &index)
}
return stackedComponents
}
}
extension RelativePath: ExpressibleByStringLiteral, ExpressibleByStringInterpolation {
public init(stringLiteral value: String) {
self.init(value)
}
}
extension RelativePath: Decodable {
public init(from decoder: Decoder) throws {
self.init(try decoder.singleValueContainer().decode(String.self))
}
}

View File

@@ -0,0 +1,37 @@
//===--- Repo.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
//
//===----------------------------------------------------------------------===//
// TODO: This really ought to be defined in swift-xcodegen
public enum Repo: CaseIterable, Sendable {
case swift
case lldb
case llvm
case cmark
public var relativePath: RelativePath {
switch self {
case .swift: "swift"
case .cmark: "cmark"
case .lldb: "llvm-project/lldb"
case .llvm: "llvm-project"
}
}
public var buildDirPrefix: String {
switch self {
case .swift: "swift"
case .cmark: "cmark"
case .lldb: "lldb"
case .llvm: "llvm"
}
}
}

View File

@@ -0,0 +1,74 @@
//===--- Byte.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 Byte: Hashable {
var rawValue: UInt8
init(_ rawValue: UInt8) {
self.rawValue = rawValue
}
}
// Please forgive me...
func == (lhs: UnicodeScalar, rhs: Byte?) -> Bool {
guard let rhs else { return false }
return lhs.value == rhs.rawValue
}
func == (lhs: Byte?, rhs: UnicodeScalar) -> Bool {
rhs == lhs
}
func != (lhs: UnicodeScalar, rhs: Byte?) -> Bool {
!(lhs == rhs)
}
func != (lhs: Byte?, rhs: UnicodeScalar) -> Bool {
rhs != lhs
}
func ~= (pattern: UnicodeScalar, match: Byte) -> Bool {
pattern == match
}
func ~= (pattern: UnicodeScalar, match: Byte?) -> Bool {
pattern == match
}
extension Byte? {
var isSpaceOrTab: Bool {
self?.isSpaceOrTab == true
}
var isNewline: Bool {
self?.isNewline == true
}
var isSpaceTabOrNewline: Bool {
self?.isSpaceTabOrNewline == true
}
}
extension Byte {
var isSpaceOrTab: Bool {
self == " " || self == "\t"
}
var isNewline: Bool {
self == "\n" || self == "\r"
}
var isSpaceTabOrNewline: Bool {
isSpaceOrTab || isNewline
}
init(ascii scalar: UnicodeScalar) {
assert(scalar.isASCII)
self.rawValue = UInt8(scalar.value)
}
var scalar: UnicodeScalar {
UnicodeScalar(UInt32(rawValue))!
}
var char: Character {
.init(scalar)
}
}

View File

@@ -0,0 +1,420 @@
//===--- ByteScanner.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
//
//===----------------------------------------------------------------------===//
/// Helper for eating bytes.
struct ByteScanner {
typealias Cursor = UnsafeRawBufferPointer.Index
private let input: UnsafeRawBufferPointer
fileprivate(set) var cursor: Cursor
init(_ input: UnsafeRawBufferPointer) {
self.input = input
self.cursor = input.startIndex
}
init(_ input: UnsafeBufferPointer<UInt8>) {
self.init(UnsafeRawBufferPointer(input))
}
var hasInput: Bool { cursor != input.endIndex }
var empty: Bool { !hasInput }
var previous: Byte? {
cursor > input.startIndex ? Byte(input[cursor - 1]) : nil
}
var peek: Byte? {
hasInput ? Byte(input[cursor]) : nil
}
func peek(ahead n: Int) -> Byte? {
precondition(n > 0)
guard n < input.endIndex - cursor else { return nil }
return Byte(input[cursor + n])
}
var whole: UnsafeRawBufferPointer {
input
}
var remaining: UnsafeRawBufferPointer {
.init(rebasing: input[cursor...])
}
func decodeUTF8<R: RangeExpression>(
_ range: R
) -> String where R.Bound == Cursor {
String(utf8: whole[range])
}
mutating func eat() -> Byte? {
guard let byte = peek else { return nil }
cursor += 1
return byte
}
mutating func tryEat() -> Bool {
eat() != nil
}
mutating func tryEat(where pred: (Byte) throws -> Bool) rethrows -> Bool {
guard let c = peek, try pred(c) else { return false }
cursor += 1
return true
}
mutating func tryEat(_ byte: Byte) -> Bool {
tryEat(where: { $0 == byte })
}
mutating func tryEat(_ c: UnicodeScalar) -> Bool {
tryEat(where: { $0 == c })
}
mutating func tryEat<S: Sequence>(_ seq: S) -> Bool where S.Element == UInt8 {
let start = cursor
for byte in seq {
guard tryEat(Byte(byte)) else {
cursor = start
return false
}
}
return true
}
mutating func tryEat(utf8 str: StaticString) -> Bool {
str.withUTF8Buffer { utf8 in
tryEat(utf8)
}
}
// Prefer the StaticString overload where we can.
@_disfavoredOverload
mutating func tryEat(utf8 str: String) -> Bool {
tryEat(str.utf8)
}
mutating func tryEating<T>(
_ body: (inout ByteScanner) -> T?
) -> T? {
var tmp = self
guard let result = body(&tmp) else { return nil }
self = tmp
return result
}
mutating func skip(while pred: (Byte) throws -> Bool) rethrows {
while try tryEat(where: pred) {}
}
mutating func skip(until pred: (Byte) throws -> Bool) rethrows {
try skip(while: { try !pred($0) })
}
mutating func skip(untilAfter pred: (Byte) throws -> Bool) rethrows {
if let char = peek, try !pred(char) {
try skip(until: pred)
}
try skip(while: pred)
}
mutating func eat(
while pred: (Byte) throws -> Bool
) rethrows -> UnsafeRawBufferPointer? {
let start = cursor
while try tryEat(where: pred) {}
return start == cursor
? nil : UnsafeRawBufferPointer(rebasing: input[start ..< cursor])
}
/// Use a byte consumer to eat a series of bytes, returning `true` in `body`
/// to continue consuming, `false` to end.
mutating func consume(
using body: (inout Consumer) throws -> Bool
) rethrows -> Bytes {
var consumer = Consumer(self)
while try body(&consumer) {}
self = consumer.scanner
return consumer.takeResult()
}
/// Similar to `consume(while:)`, but eats a character each time the body
/// returns, and consumes the entire input.
mutating func consumeWhole(
_ body: (inout Consumer) throws -> Void
) rethrows -> Bytes {
var consumer = Consumer(self, consumesWhole: true)
repeat {
guard consumer.hasInput else { break }
try body(&consumer)
} while consumer.eat()
self = consumer.scanner
return consumer.takeResult()
}
}
extension ByteScanner {
/// A wrapper type for a series of bytes consumed by Consumer, which uses
/// a backing buffer to handle cases where intermediate bytes have been
/// skipped. This must not outlive the underlying bytes being processed.
struct Bytes {
private enum Storage {
case array
case slice(Range<ByteScanner.Cursor>)
}
/// The array being stored when `storage == .array`. Defined out of line
/// to avoid COW on mutation.
private var _array: [UInt8] = []
private var array: [UInt8] {
_read {
guard case .array = storage else {
fatalError("Must be .array")
}
yield _array
}
_modify {
guard case .array = storage else {
fatalError("Must be .array")
}
yield &_array
}
}
private var storage: Storage
/// The underlying buffer being scanned.
private let buffer: UnsafeRawBufferPointer
/// The starting cursor.
private let start: ByteScanner.Cursor
/// The past-the-end position for the last cursor that was added.
private var lastCursor: ByteScanner.Cursor
/// If true, we're expecting to consume the entire buffer.
private let consumesWhole: Bool
fileprivate init(for scanner: ByteScanner, consumesWhole: Bool) {
self.storage = .slice(scanner.cursor ..< scanner.cursor)
self.buffer = scanner.whole
self.start = scanner.cursor
self.lastCursor = scanner.cursor
self.consumesWhole = consumesWhole
}
}
}
extension ByteScanner.Bytes {
fileprivate mutating func skip(_ range: Range<ByteScanner.Cursor>) {
append(upTo: range.lowerBound)
lastCursor = range.upperBound
assert(lastCursor <= buffer.endIndex)
}
fileprivate mutating func skip(at cursor: ByteScanner.Cursor) {
skip(cursor ..< cursor + 1)
}
@inline(__always)
fileprivate mutating func switchToArray() {
guard case .slice(let range) = storage else {
fatalError("Must be a slice")
}
storage = .array
if consumesWhole {
array.reserveCapacity(buffer.count)
}
array += buffer[range]
}
fileprivate mutating func prepareToAppend(at cursor: ByteScanner.Cursor) {
append(upTo: cursor)
if case .slice = storage {
// This must switch to owned storage, since it's not something present in
// the underlying buffer.
switchToArray()
}
}
fileprivate mutating func append<S: Sequence>(
contentsOf chars: S, at cursor: ByteScanner.Cursor
) where S.Element == UInt8 {
prepareToAppend(at: cursor)
array.append(contentsOf: chars)
}
fileprivate mutating func append(
_ char: UInt8, at cursor: ByteScanner.Cursor
) {
prepareToAppend(at: cursor)
array.append(char)
}
fileprivate mutating func append(upTo cursor: ByteScanner.Cursor) {
assert(cursor <= buffer.endIndex)
guard cursor > lastCursor else { return }
defer {
lastCursor = cursor
}
switch storage {
case .array:
array += buffer[lastCursor ..< cursor]
case .slice(var range):
if range.isEmpty {
// The slice is empty, we can move the start to the last cursor.
range = lastCursor ..< lastCursor
}
if lastCursor == range.endIndex {
// The slice is continuing from the last cursor, extend it.
storage = .slice(range.startIndex ..< cursor)
} else {
// The last cursor is past the slice, we need to allocate.
switchToArray()
array += buffer[lastCursor ..< cursor]
}
}
}
var isEmpty: Bool {
switch storage {
case .array:
array.isEmpty
case .slice(let range):
range.isEmpty
}
}
/// Whether the set of bytes is known to be the same as the input. It is
/// assumed that if a backing buffer were allocated, the result has changed.
var isUnchanged: Bool {
switch storage {
case .array:
false
case .slice(let r):
// Known to be the same if we're slicing the same input that we were
// created with.
r == start ..< buffer.endIndex
}
}
var count: Int {
switch storage {
case .array:
array.count
case .slice(let range):
range.count
}
}
func withUnsafeBytes<R>(
_ body: (UnsafeRawBufferPointer) throws -> R
) rethrows -> R {
switch storage {
case .array:
try array.withUnsafeBytes(body)
case .slice(let range):
try body(.init(rebasing: buffer[range]))
}
}
}
extension ByteScanner {
/// A simplified ByteScanner inferface that allows for efficient skipping
/// while producing a continguous Bytes output. Additionally, it allows for
/// injecting values into the output through calls to `append`.
struct Consumer {
fileprivate var scanner: ByteScanner
private var result: ByteScanner.Bytes
fileprivate init(_ scanner: ByteScanner, consumesWhole: Bool = false) {
self.scanner = scanner
self.result = .init(for: scanner, consumesWhole: consumesWhole)
}
var peek: Byte? {
scanner.peek
}
func peek(ahead n: Int) -> Byte? {
scanner.peek(ahead: n)
}
var hasInput: Bool {
scanner.hasInput
}
var remaining: UnsafeRawBufferPointer {
scanner.remaining
}
mutating func append(_ byte: Byte) {
result.append(byte.rawValue, at: scanner.cursor)
}
mutating func append(utf8 str: String) {
result.append(contentsOf: str.utf8, at: scanner.cursor)
}
mutating func takeResult() -> ByteScanner.Bytes {
result.append(upTo: scanner.cursor)
return result
}
mutating func eat() -> Bool {
scanner.tryEat()
}
mutating func eatRemaining() {
scanner.cursor = scanner.input.endIndex
}
mutating func skip() {
result.skip(at: scanner.cursor)
_ = scanner.eat()
}
private mutating func _skip(
using body: (inout ByteScanner) throws -> Void
) rethrows {
let start = scanner.cursor
defer {
if scanner.cursor != start {
result.skip(start ..< scanner.cursor)
}
}
try body(&scanner)
}
mutating func skip(while pred: (Byte) throws -> Bool) rethrows {
try _skip(using: { try $0.skip(while: pred) })
}
mutating func skip(until pred: (Byte) throws -> Bool) rethrows {
try _skip(using: { try $0.skip(until: pred) })
}
mutating func skip(untilAfter pred: (Byte) throws -> Bool) rethrows {
try _skip(using: { try $0.skip(untilAfter: pred) })
}
mutating func trySkip<S: Sequence>(_ seq: S) -> Bool where S.Element == UInt8 {
let start = scanner.cursor
guard scanner.tryEat(seq) else { return false }
result.skip(start ..< scanner.cursor)
return true
}
mutating func trySkip(utf8 str: String) -> Bool {
trySkip(str.utf8)
}
}
}

View File

@@ -0,0 +1,134 @@
//===--- Utils.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
//
//===----------------------------------------------------------------------===//
extension Dictionary {
@inline(__always)
mutating func withValue<R>(
for key: Key, default defaultValue: Value, body: (inout Value) throws -> R
) rethrows -> R {
try body(&self[key, default: defaultValue])
}
mutating func insertValue(
_ newValue: @autoclosure () -> Value, for key: Key
) -> Bool {
if self[key] == nil {
self[key] = newValue()
return true
}
return false
}
}
extension Sequence {
func sorted<T: Comparable>(by keyPath: KeyPath<Element, T>) -> [Element] {
sorted(by: { $0[keyPath: keyPath] < $1[keyPath: keyPath] })
}
}
extension String {
init(utf8 buffer: UnsafeRawBufferPointer) {
guard !buffer.isEmpty else {
self = ""
return
}
self = String(unsafeUninitializedCapacity: buffer.count,
initializingUTF8With: { dest in
_ = dest.initialize(from: buffer)
return buffer.count
})
}
init(utf8 buffer: UnsafeBufferPointer<UInt8>) {
self.init(utf8: UnsafeRawBufferPointer(buffer))
}
init(utf8 slice: Slice<UnsafeRawBufferPointer>) {
self = String(utf8: .init(rebasing: slice))
}
init(utf8 buffer: ByteScanner.Bytes) {
self = buffer.withUnsafeBytes(String.init(utf8:))
}
func scanningUTF8<R>(_ scan: (inout ByteScanner) throws -> R) rethrows -> R {
var tmp = self
return try tmp.withUTF8 { utf8 in
var scanner = ByteScanner(utf8)
return try scan(&scanner)
}
}
func tryDropPrefix(_ prefix: String) -> String? {
guard hasPrefix(prefix) else { return nil }
return String(dropFirst(prefix.count))
}
func escaped(addQuotesIfNeeded: Bool) -> String {
scanningUTF8 { scanner in
var needsQuotes = false
let result = scanner.consumeWhole { consumer in
switch consumer.peek {
case "\\", "\"":
consumer.append(Byte(ascii: "\\"))
case " ", "$": // $ is potentially a variable reference
needsQuotes = true
default:
break
}
}
let escaped = result.isUnchanged ? self : String(utf8: result)
return addQuotesIfNeeded && needsQuotes ? "\"\(escaped)\"" : escaped
}
}
var escaped: String {
escaped(addQuotesIfNeeded: true)
}
init(_ str: StaticString) {
self = str.withUTF8Buffer { utf8 in
String(utf8: utf8)
}
}
var isASCII: Bool {
// Thanks, @testable interface!
_classify()._isASCII
}
/// A more efficient version of replacingOccurrences(of:with:)/replacing(_:with:),
/// since the former involves bridging, and the latter currently has no fast
/// paths for strings.
func replacing(_ other: String, with replacement: String) -> String {
guard !other.isEmpty else {
return self
}
guard isASCII else {
// Not ASCII, fall back to slower method.
return replacingOccurrences(of: other, with: replacement)
}
let otherUTF8 = other.utf8
return scanningUTF8 { scanner in
let bytes = scanner.consumeWhole { consumer in
guard otherUTF8.count <= consumer.remaining.count else {
// If there's no way we can eat the string, eat the remaining.
consumer.eatRemaining()
return
}
while consumer.trySkip(otherUTF8) {
consumer.append(utf8: replacement)
}
}
return bytes.isUnchanged ? self : String(utf8: bytes)
}
}
}

View File

@@ -0,0 +1,19 @@
//===----------------------------------------------------------------------===//
//
// This source file is part of the Swift open source project
//
// Copyright (c) 2024 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 struct InternalError: Error {
private let description: String
public init(_ description: String) {
assertionFailure(description)
self.description = description
}
}

View File

@@ -0,0 +1,158 @@
//===----------------------------------------------------------------------===//
//
// This source file is part of the Swift open source project
//
// Copyright (c) 2014-2024 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
//
//===----------------------------------------------------------------------===//
import Foundation
/// A enum representing data types for legacy PropertyList type.
/// Note that the `identifier` enum is not strictly necessary,
/// but useful to semantically distinguish the strings that
/// represents object identifiers from those that are just data.
/// see: https://developer.apple.com/library/mac/documentation/Cocoa/Conceptual/PropertyLists/OldStylePlists/OldStylePLists.html
public enum PropertyList {
case identifier(String)
case string(String)
case array([PropertyList])
case dictionary([String: PropertyList])
var string: String? {
if case .string(let string) = self {
return string
}
return nil
}
var array: [PropertyList]? {
if case .array(let array) = self {
return array
}
return nil
}
}
extension PropertyList: CustomStringConvertible {
public var description: String {
String(decoding: serialize(), as: UTF8.self)
}
}
extension PropertyList {
/// Serializes the Plist enum.
public func serialize() -> Data {
var writer = UTF8Writer()
writePlistRepresentation(to: &writer)
return Data(writer.bytes)
}
/// Escapes the string for plist.
/// Finds the instances of quote (") and backward slash (\) and prepends
/// the escape character backward slash (\).
static func escape(string: String) -> String {
func needsEscape(_ char: UInt8) -> Bool {
return char == UInt8(ascii: "\\") || char == UInt8(ascii: "\"")
}
guard let pos = string.utf8.firstIndex(where: needsEscape) else {
return string
}
var newString = String(string[..<pos])
for char in string.utf8[pos...] {
if needsEscape(char) {
newString += "\\"
}
newString += String(UnicodeScalar(char))
}
return newString
}
}
fileprivate extension PropertyList {
struct UTF8Writer {
var level: Int = 0
var bytes: [UInt8] = []
init() {
self += "// !$*UTF8*$!\n"
}
mutating func withIndent(body: (inout Self) -> Void) {
level += 1
body(&self)
level -= 1
}
static func += (writer: inout UTF8Writer, str: StaticString) {
str.withUTF8Buffer { utf8 in
writer.bytes += utf8
}
}
@_disfavoredOverload
static func += (writer: inout UTF8Writer, str: String) {
writer.bytes += str.utf8
}
mutating func indent() {
for _ in 0 ..< level {
self += " "
}
}
}
/// Private function to generate OPENSTEP-style plist representation.
func writePlistRepresentation(to writer: inout UTF8Writer) {
// Do the appropriate thing for each type of plist node.
switch self {
case .identifier(let ident):
// FIXME: we should assert that the identifier doesn't need quoting
writer += ident
case .string(let string):
writer += "\""
writer += PropertyList.escape(string: string)
writer += "\""
case .array(let array):
writer += "(\n"
writer.withIndent { writer in
for (i, item) in array.enumerated() {
writer.indent()
item.writePlistRepresentation(to: &writer)
writer += (i != array.count - 1) ? ",\n" : "\n"
}
}
writer.indent()
writer += ")"
case .dictionary(let dict):
let dict = dict.sorted(by: {
// Make `isa` sort first (just for readability purposes).
switch ($0.key, $1.key) {
case ("isa", "isa"): return false
case ("isa", _): return true
case (_, "isa"): return false
default: return $0.key < $1.key
}
})
writer += "{\n"
writer.withIndent { writer in
for (key, value) in dict {
writer.indent()
writer += key
writer += " = "
value.writePlistRepresentation(to: &writer)
writer += ";\n"
}
}
writer.indent()
writer += "}"
}
}
}

View File

@@ -0,0 +1,231 @@
//===--- PropertyListEncoder.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
//
//===----------------------------------------------------------------------===//
extension PropertyList {
static func encode<T: Encodable>(_ x: T) throws -> PropertyList {
let encoder = _Encoder()
try x.encode(to: encoder)
return encoder.result.value
}
}
fileprivate extension PropertyList {
final class Result {
enum Underlying {
case empty
case string(String)
case array([PropertyList])
case dictionary([String: PropertyList])
}
fileprivate var underlying: Underlying = .empty
var isEmpty: Bool {
if case .empty = underlying { return true } else { return false }
}
@discardableResult
func makeDictionary() -> Self {
precondition(isEmpty)
underlying = .dictionary([:])
return self
}
@discardableResult
func makeArray() -> Self {
precondition(isEmpty)
underlying = .array([])
return self
}
@discardableResult
func makeString(_ str: String) -> Self {
precondition(isEmpty)
underlying = .string(str)
return self
}
var value: PropertyList {
switch underlying {
case .empty:
fatalError("Didn't encode anything?")
case .array(let array):
return .array(array)
case .dictionary(let dictionary):
return .dictionary(dictionary)
case .string(let str):
return .string(str)
}
}
private var _array: [PropertyList] {
guard case .array(let arr) = underlying else {
fatalError("Must be array")
}
return arr
}
var array: [PropertyList] {
_read {
yield _array
}
_modify {
// Avoid a COW.
var arr = _array
underlying = .empty
defer {
underlying = .array(arr)
}
yield &arr
}
}
private var _dictionary: [String: PropertyList] {
guard case .dictionary(let dict) = underlying else {
fatalError("Must be dictionary")
}
return dict
}
var dictionary: [String: PropertyList] {
_read {
yield _dictionary
}
_modify {
// Avoid a COW.
var dict = _dictionary
underlying = .empty
defer {
underlying = .dictionary(dict)
}
yield &dict
}
}
}
struct _Encoder: Encoder {
var userInfo: [CodingUserInfoKey: Any] { [:] }
var codingPath: [CodingKey] { fatalError("Unsupported") }
var result = Result()
init() {}
func container<Key>(keyedBy type: Key.Type) -> KeyedEncodingContainer<Key> {
.init(KeyedContainer<Key>(result: result.makeDictionary()))
}
func unkeyedContainer() -> UnkeyedEncodingContainer {
UnkeyedContainer(result: result.makeArray())
}
func singleValueContainer() -> SingleValueEncodingContainer {
SingleValueContainer(result: result)
}
}
}
extension PropertyList {
fileprivate struct KeyedContainer<Key: CodingKey>: KeyedEncodingContainerProtocol {
var codingPath: [CodingKey] { fatalError("Unsupported") }
var result: Result
mutating func encode(_ value: String, forKey key: Key) {
result.dictionary[key.stringValue] = .string(value)
}
mutating func encode<T : Encodable>(_ value: T, forKey key: Key) throws {
result.dictionary[key.stringValue] = try .encode(value)
}
mutating func nestedUnkeyedContainer(forKey key: Key) -> UnkeyedEncodingContainer { fatalError("Unsupported") }
mutating func nestedContainer<NestedKey>(keyedBy keyType: NestedKey.Type, forKey key: Key) -> KeyedEncodingContainer<NestedKey> { fatalError("Unsupported") }
mutating func superEncoder(forKey key: Key) -> Encoder { fatalError("Unsupported") }
mutating func superEncoder() -> Encoder { fatalError("Unsupported") }
mutating func encodeNil(forKey key: Key) { fatalError("Unsupported") }
mutating func encode(_ value: Bool, forKey key: Key) { fatalError("Unsupported") }
mutating func encode(_ value: Double, forKey key: Key) { fatalError("Unsupported") }
mutating func encode(_ value: Float, forKey key: Key) { fatalError("Unsupported") }
mutating func encode(_ value: Int, forKey key: Key) { fatalError("Unsupported") }
mutating func encode(_ value: Int8, forKey key: Key) { fatalError("Unsupported") }
mutating func encode(_ value: Int16, forKey key: Key) { fatalError("Unsupported") }
mutating func encode(_ value: Int32, forKey key: Key) { fatalError("Unsupported") }
mutating func encode(_ value: Int64, forKey key: Key) { fatalError("Unsupported") }
mutating func encode(_ value: UInt, forKey key: Key) { fatalError("Unsupported") }
mutating func encode(_ value: UInt8, forKey key: Key) { fatalError("Unsupported") }
mutating func encode(_ value: UInt16, forKey key: Key) { fatalError("Unsupported") }
mutating func encode(_ value: UInt32, forKey key: Key) { fatalError("Unsupported") }
mutating func encode(_ value: UInt64, forKey key: Key) { fatalError("Unsupported") }
}
fileprivate struct UnkeyedContainer: UnkeyedEncodingContainer {
var codingPath: [CodingKey] { fatalError("Unsupported") }
var result: Result
var count: Int {
result.array.count
}
mutating func encode(_ value: String) {
result.array.append(.string(value))
}
mutating func encode<T: Encodable>(_ value: T) throws {
result.array.append(try .encode(value))
}
mutating func nestedContainer<NestedKey: CodingKey>(keyedBy keyType: NestedKey.Type) -> KeyedEncodingContainer<NestedKey> { fatalError("Unsupported") }
mutating func nestedUnkeyedContainer() -> UnkeyedEncodingContainer { fatalError("Unsupported") }
mutating func superEncoder() -> Encoder { fatalError("Unsupported") }
mutating func encodeNil() { fatalError("Unsupported") }
mutating func encode(_ value: Bool) { fatalError("Unsupported") }
mutating func encode(_ value: Double) { fatalError("Unsupported") }
mutating func encode(_ value: Float) { fatalError("Unsupported") }
mutating func encode(_ value: Int) { fatalError("Unsupported") }
mutating func encode(_ value: Int8) { fatalError("Unsupported") }
mutating func encode(_ value: Int16) { fatalError("Unsupported") }
mutating func encode(_ value: Int32) { fatalError("Unsupported") }
mutating func encode(_ value: Int64) { fatalError("Unsupported") }
mutating func encode(_ value: UInt) { fatalError("Unsupported") }
mutating func encode(_ value: UInt8) { fatalError("Unsupported") }
mutating func encode(_ value: UInt16) { fatalError("Unsupported") }
mutating func encode(_ value: UInt32) { fatalError("Unsupported") }
mutating func encode(_ value: UInt64) { fatalError("Unsupported") }
}
fileprivate struct SingleValueContainer: SingleValueEncodingContainer {
var codingPath: [CodingKey] { fatalError("Unsupported") }
var result: Result
mutating func encode<T: Encodable>(_ value: T) throws {
let encoder = _Encoder()
try value.encode(to: encoder)
result.underlying = encoder.result.underlying
}
mutating func encode(_ value: String) throws {
result.makeString(value)
}
mutating func encodeNil() throws { fatalError("Unsupported") }
mutating func encode(_ value: Bool) throws { fatalError("Unsupported") }
mutating func encode(_ value: Double) throws { fatalError("Unsupported") }
mutating func encode(_ value: Float) throws { fatalError("Unsupported") }
mutating func encode(_ value: Int) throws { fatalError("Unsupported") }
mutating func encode(_ value: Int8) throws { fatalError("Unsupported") }
mutating func encode(_ value: Int16) throws { fatalError("Unsupported") }
mutating func encode(_ value: Int32) throws { fatalError("Unsupported") }
mutating func encode(_ value: Int64) throws { fatalError("Unsupported") }
mutating func encode(_ value: UInt) throws { fatalError("Unsupported") }
mutating func encode(_ value: UInt8) throws { fatalError("Unsupported") }
mutating func encode(_ value: UInt16) throws { fatalError("Unsupported") }
mutating func encode(_ value: UInt32) throws { fatalError("Unsupported") }
mutating func encode(_ value: UInt64) throws { fatalError("Unsupported") }
}
}

View File

@@ -0,0 +1,6 @@
# Xcodeproj
Originally from the [now defunct SwiftPM library][1], a minimal library for
creating an xcodeproj.
[1]: https://github.com/apple/swift-package-manager/tree/6595cd2b22f25056b83a7357c07301c45805e69b/Sources/Xcodeproj

View File

@@ -0,0 +1,439 @@
//===----------------------------------------------------------------------===//
//
// This source file is part of the Swift open source project
//
// Copyright (c) 2014-2024 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
//
//===----------------------------------------------------------------------===//
/*
A very simple rendition of the Xcode project model. There is only sufficient
functionality to allow creation of Xcode projects in a somewhat readable way,
and serialization to .xcodeproj plists. There is no consistency checking to
ensure, for example, that build settings have valid values, dependency cycles
are not created, etc.
Everything here is geared toward supporting project generation. The intended
usage model is for custom logic to build up a project using Xcode terminology
(e.g. "group", "reference", "target", "build phase"), but there is almost no
provision for modifying the model after it has been built up. The intent is
to create it as desired from the start.
Rather than try to represent everything that Xcode's project model supports,
the approach is to start small and to add functionality as needed.
Note that this API represents only the project model there is no notion of
workspaces, schemes, etc (although schemes are represented individually in a
separate API). The notion of build settings is also somewhat different from
what it is in Xcode: instead of an open-ended mapping of build configuration
names to dictionaries of build settings, here there is a single set of common
build settings plus two overlay sets for debug and release. The generated
project has just the two Debug and Release configurations, created by merging
the common set into the release and debug sets. This allows a more natural
configuration of the settings, since most values are the same between Debug
and Release. Also, the build settings themselves are represented as structs
of named fields, instead of dictionaries with arbitrary name strings as keys.
It is expected that some of these simplifications will need to be lifted over
time, based on need. That should be done carefully, however, to avoid ending
up with an overly complicated model.
Some things that are incomplete in even this first model:
- copy files build phases are incomplete
- shell script build phases are incomplete
- file types in file references are specified using strings; should be enums
so that the client doesn't have to hardcode the mapping to Xcode file type
identifiers
- debug and release settings override common settings; they should be merged
in a way that respects `$(inhertied)` when the same setting is defined in
common and in debug or release
- there is no good way to control the ordering of the `Products` group in the
main group; it needs to be added last in order to appear after the other
references
*/
public struct Xcode {
/// An Xcode project, consisting of a tree of groups and file references,
/// a list of targets, and some additional information. Note that schemes
/// are outside of the project data model.
public class Project {
public let mainGroup: Group
public var buildSettings: BuildSettingsTable
public var productGroup: Group?
public var projectDir: String
public var targets: [Target]
public init() {
self.mainGroup = Group(path: "")
self.buildSettings = BuildSettingsTable()
self.productGroup = nil
self.projectDir = ""
self.targets = []
}
/// Creates and adds a new target (which does not initially have any
/// build phases).
public func addTarget(objectID: String? = nil, productType: Target.ProductType? = nil, name: String) -> Target {
let target = Target(objectID: objectID ?? "TARGET_\(name)", productType: productType, name: name)
targets.append(target)
return target
}
}
/// Abstract base class for all items in the group hierarchy.
public class Reference {
/// Relative path of the reference. It is usually a literal, but may
/// in fact contain build settings.
public var path: String
/// Determines the base path for the reference's relative path.
public var pathBase: RefPathBase
/// Name of the reference, if different from the last path component
/// (if not set, Xcode will use the last path component as the name).
public var name: String?
/// Determines the base path for a reference's relative path (this is
/// what for some reason is called a "source tree" in Xcode).
public enum RefPathBase: String {
/// An absolute path
case absolute = "<absolute>"
/// Indicates that the path is relative to the source root (i.e.
/// the "project directory").
case projectDir = "SOURCE_ROOT"
/// Indicates that the path is relative to the path of the parent
/// group.
case groupDir = "<group>"
/// Indicates that the path is relative to the effective build
/// directory (which varies depending on active scheme, active run
/// destination, or even an overridden build setting.
case buildDir = "BUILT_PRODUCTS_DIR"
}
init(path: String, pathBase: RefPathBase = .groupDir, name: String? = nil) {
self.path = path
self.pathBase = pathBase
self.name = name
}
/// Whether this is either a group or directory reference (blue folder).
public var isDirectoryLike: Bool {
if self is Xcode.Group {
return true
}
if let ref = self as? Xcode.FileReference {
return ref.isDirectory
}
return false
}
}
/// A reference to a file system entity (a file, folder, etc).
public final class FileReference: Reference {
public var objectID: String?
public var fileType: String?
public var isDirectory: Bool
init(path: String, isDirectory: Bool, pathBase: RefPathBase = .groupDir, name: String? = nil, fileType: String? = nil, objectID: String? = nil) {
self.isDirectory = isDirectory
super.init(path: path, pathBase: pathBase, name: name)
self.objectID = objectID
self.fileType = fileType
}
}
/// A group that can contain References (FileReferences and other Groups).
/// The resolved path of a group is used as the base path for any child
/// references whose source tree type is GroupRelative.
public final class Group: Reference {
public var subitems = [Reference]()
/// Creates and appends a new Group to the list of subitems.
/// The new group is returned so that it can be configured.
@discardableResult
public func addGroup(
path: String,
pathBase: RefPathBase = .groupDir,
name: String? = nil
) -> Group {
let group = Group(path: path, pathBase: pathBase, name: name)
subitems.append(group)
return group
}
/// Creates and appends a new FileReference to the list of subitems.
@discardableResult
public func addFileReference(
path: String,
isDirectory: Bool,
pathBase: RefPathBase = .groupDir,
name: String? = nil,
fileType: String? = nil,
objectID: String? = nil
) -> FileReference {
let fref = FileReference(path: path, isDirectory: isDirectory, pathBase: pathBase, name: name, fileType: fileType, objectID: objectID)
subitems.append(fref)
return fref
}
}
/// An Xcode target, representing a single entity to build.
public final class Target {
public var objectID: String?
public var name: String
public var productName: String
public var productType: ProductType?
public var buildSettings: BuildSettingsTable
public var buildPhases: [BuildPhase]
public var productReference: FileReference?
public var dependencies: [TargetDependency]
public enum ProductType: String {
case application = "com.apple.product-type.application"
case staticArchive = "com.apple.product-type.library.static"
case dynamicLibrary = "com.apple.product-type.library.dynamic"
case framework = "com.apple.product-type.framework"
case executable = "com.apple.product-type.tool"
case unitTest = "com.apple.product-type.bundle.unit-test"
}
init(objectID: String?, productType: ProductType?, name: String) {
self.objectID = objectID
self.name = name
self.productType = productType
self.productName = name
self.buildSettings = BuildSettingsTable()
self.buildPhases = []
self.dependencies = []
}
// FIXME: There's a lot repetition in these methods; using generics to
// try to avoid that raised other issues in terms of requirements on
// the Reference class, though.
/// Adds a "headers" build phase, i.e. one that copies headers into a
/// directory of the product, after suitable processing.
@discardableResult
public func addHeadersBuildPhase() -> HeadersBuildPhase {
let phase = HeadersBuildPhase()
buildPhases.append(phase)
return phase
}
/// Adds a "sources" build phase, i.e. one that compiles sources and
/// provides them to be linked into the executable code of the product.
@discardableResult
public func addSourcesBuildPhase() -> SourcesBuildPhase {
let phase = SourcesBuildPhase()
buildPhases.append(phase)
return phase
}
/// Adds a "frameworks" build phase, i.e. one that links compiled code
/// and libraries into the executable of the product.
@discardableResult
public func addFrameworksBuildPhase() -> FrameworksBuildPhase {
let phase = FrameworksBuildPhase()
buildPhases.append(phase)
return phase
}
/// Adds a "copy files" build phase, i.e. one that copies files to an
/// arbitrary location relative to the product.
@discardableResult
public func addCopyFilesBuildPhase(dstDir: String) -> CopyFilesBuildPhase {
let phase = CopyFilesBuildPhase(dstDir: dstDir)
buildPhases.append(phase)
return phase
}
/// Adds a "shell script" build phase, i.e. one that runs a custom
/// shell script as part of the build.
@discardableResult
public func addShellScriptBuildPhase(
script: String, inputs: [String], outputs: [String], alwaysRun: Bool
) -> ShellScriptBuildPhase {
let phase = ShellScriptBuildPhase(
script: script, inputs: inputs, outputs: outputs, alwaysRun: alwaysRun
)
buildPhases.append(phase)
return phase
}
/// Adds a dependency on another target.
/// FIXME: We do not check for cycles. Should we? This is an extremely
/// minimal API so it's not clear that we should.
public func addDependency(on target: Target) {
dependencies.append(TargetDependency(target: target))
}
/// A simple wrapper to prevent ownership cycles in the `dependencies`
/// property.
public struct TargetDependency {
public unowned var target: Target
}
}
/// Abstract base class for all build phases in a target.
public class BuildPhase {
public var files: [BuildFile] = []
/// Adds a new build file that refers to `fileRef`.
@discardableResult
public func addBuildFile(fileRef: FileReference) -> BuildFile {
let buildFile = BuildFile(fileRef: fileRef)
files.append(buildFile)
return buildFile
}
}
/// A "headers" build phase, i.e. one that copies headers into a directory
/// of the product, after suitable processing.
public final class HeadersBuildPhase: BuildPhase {
// Nothing extra yet.
}
/// A "sources" build phase, i.e. one that compiles sources and provides
/// them to be linked into the executable code of the product.
public final class SourcesBuildPhase: BuildPhase {
// Nothing extra yet.
}
/// A "frameworks" build phase, i.e. one that links compiled code and
/// libraries into the executable of the product.
public final class FrameworksBuildPhase: BuildPhase {
// Nothing extra yet.
}
/// A "copy files" build phase, i.e. one that copies files to an arbitrary
/// location relative to the product.
public final class CopyFilesBuildPhase: BuildPhase {
public var dstDir: String
init(dstDir: String) {
self.dstDir = dstDir
}
}
/// A "shell script" build phase, i.e. one that runs a custom shell script.
public final class ShellScriptBuildPhase: BuildPhase {
public var script: String
public var inputs: [String]
public var outputs: [String]
public var alwaysRun: Bool
init(script: String, inputs: [String], outputs: [String], alwaysRun: Bool) {
self.script = script
self.inputs = inputs
self.outputs = outputs
self.alwaysRun = alwaysRun
}
}
/// A build file, representing the membership of a file reference in a
/// build phase of a target.
public final class BuildFile {
public var fileRef: FileReference?
init(fileRef: FileReference) {
self.fileRef = fileRef
}
public var settings = Settings()
/// A set of file settings.
public struct Settings: Encodable {
public var ATTRIBUTES: [String]?
public var COMPILER_FLAGS: String?
public init() {
}
}
}
/// A table of build settings, which for the sake of simplicity consists
/// (in this simplified model) of a set of common settings, and a set of
/// overlay settings for Debug and Release builds. There can also be a
/// file reference to an .xcconfig file on which to base the settings.
public final class BuildSettingsTable {
/// Common build settings are in both generated configurations (Debug
/// and Release).
public var common = BuildSettings()
/// Debug build settings are overlaid over the common settings in the
/// generated Debug configuration.
public var debug = BuildSettings()
/// Release build settings are overlaid over the common settings in the
/// generated Release configuration.
public var release = BuildSettings()
/// An optional file reference to an .xcconfig file.
public var xcconfigFileRef: FileReference?
public init() {
}
/// A set of build settings, which is represented as a struct of optional
/// build settings. This is not optimally efficient, but it is great for
/// code completion and type-checking.
public struct BuildSettings: Encodable {
// Note: although some of these build settings sound like booleans,
// they are all either strings or arrays of strings, because even
// a boolean may be a macro reference expression.
public var BUILT_PRODUCTS_DIR: String?
public var CLANG_CXX_LANGUAGE_STANDARD: String?
public var CLANG_ENABLE_MODULES: String?
public var CLANG_ENABLE_OBJC_ARC: String?
public var COMBINE_HIDPI_IMAGES: String?
public var COPY_PHASE_STRIP: String?
public var CURRENT_PROJECT_VERSION: String?
public var DEBUG_INFORMATION_FORMAT: String?
public var DEFINES_MODULE: String?
public var DYLIB_INSTALL_NAME_BASE: String?
public var EMBEDDED_CONTENT_CONTAINS_SWIFT: String?
public var ENABLE_NS_ASSERTIONS: String?
public var ENABLE_TESTABILITY: String?
public var FRAMEWORK_SEARCH_PATHS: [String]?
public var GCC_C_LANGUAGE_STANDARD: String?
public var GCC_OPTIMIZATION_LEVEL: String?
public var GCC_PREPROCESSOR_DEFINITIONS: [String]?
public var GCC_GENERATE_DEBUGGING_SYMBOLS: String?
public var GCC_WARN_64_TO_32_BIT_CONVERSION: String?
public var HEADER_SEARCH_PATHS: [String]?
public var INFOPLIST_FILE: String?
public var LD_RUNPATH_SEARCH_PATHS: [String]?
public var LIBRARY_SEARCH_PATHS: [String]?
public var MACOSX_DEPLOYMENT_TARGET: String?
public var IPHONEOS_DEPLOYMENT_TARGET: String?
public var TVOS_DEPLOYMENT_TARGET: String?
public var WATCHOS_DEPLOYMENT_TARGET: String?
public var DRIVERKIT_DEPLOYMENT_TARGET: String?
public var MODULEMAP_FILE: String?
public var ONLY_ACTIVE_ARCH: String?
public var OTHER_CFLAGS: [String]?
public var OTHER_CPLUSPLUSFLAGS: [String]?
public var OTHER_LDFLAGS: [String]?
public var OTHER_SWIFT_FLAGS: [String]?
public var PRODUCT_BUNDLE_IDENTIFIER: String?
public var PRODUCT_MODULE_NAME: String?
public var PRODUCT_NAME: String?
public var PROJECT_DIR: String?
public var PROJECT_NAME: String?
public var SDKROOT: String?
public var SKIP_INSTALL: String?
public var SUPPORTED_PLATFORMS: [String]?
public var SUPPORTS_MACCATALYST: String?
public var SWIFT_ACTIVE_COMPILATION_CONDITIONS: [String]?
public var SWIFT_COMPILATION_MODE: String?
public var SWIFT_ENABLE_EXPLICIT_MODULES: String?
public var SWIFT_FORCE_STATIC_LINK_STDLIB: String?
public var SWIFT_FORCE_DYNAMIC_LINK_STDLIB: String?
public var SWIFT_INCLUDE_PATHS: [String]?
public var SWIFT_MODULE_ALIASES: [String: String]?
public var SWIFT_OPTIMIZATION_LEVEL: String?
public var SWIFT_VERSION: String?
public var TARGET_NAME: String?
public var TARGET_BUILD_DIR: String?
public var USE_HEADERMAP: String?
public var LD: String?
}
}
}

View File

@@ -0,0 +1,544 @@
//===----------------------------------------------------------------------===//
//
// This source file is part of the Swift open source project
//
// Copyright (c) 2014-2024 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
//
//===----------------------------------------------------------------------===//
/*
An extemely simple rendition of the Xcode project model into a plist. There
is only enough functionality to allow serialization of Xcode projects.
*/
extension Xcode.Project: PropertyListSerializable {
/// Generates and returns the contents of a `project.pbxproj` plist. Does
/// not generate any ancillary files, such as a set of schemes.
///
/// Many complexities of the Xcode project model are not represented; we
/// should not add functionality to this model unless it's needed, since
/// implementation of the full Xcode project model would be unnecessarily
/// complex.
public func generatePlist() throws -> PropertyList {
// The project plist is a bit special in that it's the archive for the
// whole file. We create a plist serializer and serialize the entire
// object graph to it, and then return an archive dictionary containing
// the serialized object dictionaries.
let serializer = PropertyListSerializer()
try serializer.serialize(object: self)
return .dictionary([
"archiveVersion": .string("1"),
"objectVersion": .string("46"), // Xcode 8.0
"rootObject": .identifier(serializer.id(of: self)),
"objects": .dictionary(serializer.idsToDicts),
])
}
/// Called by the Serializer to serialize the Project.
fileprivate func serialize(to serializer: PropertyListSerializer) throws -> [String: PropertyList] {
// Create a `PBXProject` plist dictionary.
// Note: we skip things like the `Products` group; they get autocreated
// by Xcode when it opens the project and notices that they are missing.
// Note: we also skip schemes, since they are not in the project plist.
var dict = [String: PropertyList]()
dict["isa"] = .string("PBXProject")
// Since the project file is generated, we opt out of upgrade-checking.
// FIXME: Should we really? Why would we not want to get upgraded?
dict["attributes"] = .dictionary(["LastUpgradeCheck": .string("9999"),
"LastSwiftMigration": .string("9999")])
dict["compatibilityVersion"] = .string("Xcode 3.2")
dict["developmentRegion"] = .string("en")
// Build settings are a bit tricky; in Xcode, each is stored in a named
// XCBuildConfiguration object, and the list of build configurations is
// in turn stored in an XCConfigurationList. In our simplified model,
// we have a BuildSettingsTable, with three sets of settings: one for
// the common settings, and one each for the Debug and Release overlays.
// So we consider the BuildSettingsTable to be the configuration list.
dict["buildConfigurationList"] = try .identifier(serializer.serialize(object: buildSettings))
dict["mainGroup"] = try .identifier(serializer.serialize(object: mainGroup))
dict["hasScannedForEncodings"] = .string("0")
dict["knownRegions"] = .array([.string("en")])
if let productGroup = productGroup {
dict["productRefGroup"] = .identifier(serializer.id(of: productGroup))
}
dict["projectDirPath"] = .string(projectDir)
// Ensure that targets are output in a sorted order.
let sortedTargets = targets.sorted(by: { $0.name < $1.name })
dict["targets"] = try .array(sortedTargets.map({ target in
try .identifier(serializer.serialize(object: target))
}))
return dict
}
}
/// Private helper function that constructs and returns a partial property list
/// dictionary for references. The caller can add to the returned dictionary.
/// FIXME: It would be nicer to be able to use inheritance to serialize the
/// attributes inherited from Reference, but but in Swift 3.0 we get an error
/// that "declarations in extensions cannot override yet".
fileprivate func makeReferenceDict(
reference: Xcode.Reference,
serializer: PropertyListSerializer,
xcodeClassName: String
) -> [String: PropertyList] {
var dict = [String: PropertyList]()
dict["isa"] = .string(xcodeClassName)
dict["path"] = .string(reference.path)
if let name = reference.name {
dict["name"] = .string(name)
}
dict["sourceTree"] = .string(reference.pathBase.rawValue)
return dict
}
extension Xcode.Group: PropertyListSerializable {
/// Called by the Serializer to serialize the Group.
fileprivate func serialize(to serializer: PropertyListSerializer) throws -> [String: PropertyList] {
// Create a `PBXGroup` plist dictionary.
// FIXME: It would be nicer to be able to use inheritance for the code
// inherited from Reference, but but in Swift 3.0 we get an error that
// "declarations in extensions cannot override yet".
var dict = makeReferenceDict(reference: self, serializer: serializer, xcodeClassName: "PBXGroup")
dict["children"] = try .array(subitems.map({ reference in
// For the same reason, we have to cast as `PropertyListSerializable`
// here; as soon as we try to make Reference conform to the protocol,
// we get the problem of not being able to override `serialize(to:)`.
try .identifier(serializer.serialize(object: reference as! PropertyListSerializable))
}))
return dict
}
}
extension Xcode.FileReference: PropertyListSerializable {
/// Called by the Serializer to serialize the FileReference.
fileprivate func serialize(to serializer: PropertyListSerializer) -> [String: PropertyList] {
// Create a `PBXFileReference` plist dictionary.
// FIXME: It would be nicer to be able to use inheritance for the code
// inherited from Reference, but but in Swift 3.0 we get an error that
// "declarations in extensions cannot override yet".
var dict = makeReferenceDict(reference: self, serializer: serializer, xcodeClassName: "PBXFileReference")
if let fileType = fileType {
dict["explicitFileType"] = .string(fileType)
}
// FileReferences don't need to store a name if it's the same as the path.
if name == path {
dict["name"] = nil
}
return dict
}
}
extension Xcode.Target: PropertyListSerializable {
/// Called by the Serializer to serialize the Target.
fileprivate func serialize(to serializer: PropertyListSerializer) throws -> [String: PropertyList] {
// Create either a `PBXNativeTarget` or an `PBXAggregateTarget` plist
// dictionary (depending on whether or not we have a product type).
var dict = [String: PropertyList]()
dict["isa"] = .string(productType == nil ? "PBXAggregateTarget" : "PBXNativeTarget")
dict["name"] = .string(name)
// Build settings are a bit tricky; in Xcode, each is stored in a named
// XCBuildConfiguration object, and the list of build configurations is
// in turn stored in an XCConfigurationList. In our simplified model,
// we have a BuildSettingsTable, with three sets of settings: one for
// the common settings, and one each for the Debug and Release overlays.
// So we consider the BuildSettingsTable to be the configuration list.
// This is the same situation as for Project.
dict["buildConfigurationList"] = try .identifier(serializer.serialize(object: buildSettings))
dict["buildPhases"] = try .array(buildPhases.map({ phase in
// Here we have the same problem as for Reference; we cannot inherit
// functionality since we're in an extension.
try .identifier(serializer.serialize(object: phase as! PropertyListSerializable))
}))
/// Private wrapper class for a target dependency relation. This is
/// glue between our value-based settings structures and the Xcode
/// project model's identity-based TargetDependency objects.
class TargetDependency: PropertyListSerializable {
var target: Xcode.Target
init(target: Xcode.Target) {
self.target = target
}
func serialize(to serializer: PropertyListSerializer) -> [String: PropertyList] {
// Create a `PBXTargetDependency` plist dictionary.
var dict = [String: PropertyList]()
dict["isa"] = .string("PBXTargetDependency")
dict["target"] = .identifier(serializer.id(of: target))
return dict
}
}
dict["dependencies"] = try .array(dependencies.map({ dep in
// In the Xcode project model, target dependencies are objects,
// so we need a helper class here.
try .identifier(serializer.serialize(object: TargetDependency(target: dep.target)))
}))
dict["productName"] = .string(productName)
if let productType = productType {
dict["productType"] = .string(productType.rawValue)
}
if let productReference = productReference {
dict["productReference"] = .identifier(serializer.id(of: productReference))
}
return dict
}
}
/// Private helper function that constructs and returns a partial property list
/// dictionary for build phases. The caller can add to the returned dictionary.
/// FIXME: It would be nicer to be able to use inheritance to serialize the
/// attributes inherited from BuildPhase, but but in Swift 3.0 we get an error
/// that "declarations in extensions cannot override yet".
fileprivate func makeBuildPhaseDict(
buildPhase: Xcode.BuildPhase,
serializer: PropertyListSerializer,
xcodeClassName: String
) throws -> [String: PropertyList] {
var dict = [String: PropertyList]()
dict["isa"] = .string(xcodeClassName)
dict["files"] = try .array(buildPhase.files.map({ file in
try .identifier(serializer.serialize(object: file))
}))
return dict
}
extension Xcode.HeadersBuildPhase: PropertyListSerializable {
/// Called by the Serializer to serialize the HeadersBuildPhase.
fileprivate func serialize(to serializer: PropertyListSerializer) throws -> [String: PropertyList] {
// Create a `PBXHeadersBuildPhase` plist dictionary.
// FIXME: It would be nicer to be able to use inheritance for the code
// inherited from BuildPhase, but but in Swift 3.0 we get an error that
// "declarations in extensions cannot override yet".
return try makeBuildPhaseDict(buildPhase: self, serializer: serializer, xcodeClassName: "PBXHeadersBuildPhase")
}
}
extension Xcode.SourcesBuildPhase: PropertyListSerializable {
/// Called by the Serializer to serialize the SourcesBuildPhase.
fileprivate func serialize(to serializer: PropertyListSerializer) throws -> [String: PropertyList] {
// Create a `PBXSourcesBuildPhase` plist dictionary.
// FIXME: It would be nicer to be able to use inheritance for the code
// inherited from BuildPhase, but but in Swift 3.0 we get an error that
// "declarations in extensions cannot override yet".
return try makeBuildPhaseDict(buildPhase: self, serializer: serializer, xcodeClassName: "PBXSourcesBuildPhase")
}
}
extension Xcode.FrameworksBuildPhase: PropertyListSerializable {
/// Called by the Serializer to serialize the FrameworksBuildPhase.
fileprivate func serialize(to serializer: PropertyListSerializer) throws -> [String: PropertyList] {
// Create a `PBXFrameworksBuildPhase` plist dictionary.
// FIXME: It would be nicer to be able to use inheritance for the code
// inherited from BuildPhase, but but in Swift 3.0 we get an error that
// "declarations in extensions cannot override yet".
return try makeBuildPhaseDict(buildPhase: self, serializer: serializer, xcodeClassName: "PBXFrameworksBuildPhase")
}
}
extension Xcode.CopyFilesBuildPhase: PropertyListSerializable {
/// Called by the Serializer to serialize the FrameworksBuildPhase.
fileprivate func serialize(to serializer: PropertyListSerializer) throws -> [String: PropertyList] {
// Create a `PBXCopyFilesBuildPhase` plist dictionary.
// FIXME: It would be nicer to be able to use inheritance for the code
// inherited from BuildPhase, but but in Swift 3.0 we get an error that
// "declarations in extensions cannot override yet".
var dict = try makeBuildPhaseDict(
buildPhase: self,
serializer: serializer,
xcodeClassName: "PBXCopyFilesBuildPhase"
)
dict["dstPath"] = .string("") // FIXME: needs to be real
dict["dstSubfolderSpec"] = .string("") // FIXME: needs to be real
return dict
}
}
extension Xcode.ShellScriptBuildPhase: PropertyListSerializable {
/// Called by the Serializer to serialize the ShellScriptBuildPhase.
fileprivate func serialize(to serializer: PropertyListSerializer) throws -> [String: PropertyList] {
// Create a `PBXShellScriptBuildPhase` plist dictionary.
// FIXME: It would be nicer to be able to use inheritance for the code
// inherited from BuildPhase, but but in Swift 3.0 we get an error that
// "declarations in extensions cannot override yet".
var dict = try makeBuildPhaseDict(
buildPhase: self,
serializer: serializer,
xcodeClassName: "PBXShellScriptBuildPhase")
dict["shellPath"] = .string("/bin/sh") // FIXME: should be settable
dict["shellScript"] = .string(script)
dict["inputPaths"] = .array(inputs.map { .string($0) })
dict["outputPaths"] = .array(outputs.map { .string($0) })
dict["alwaysOutOfDate"] = .string(alwaysRun ? "1" : "0")
return dict
}
}
extension Xcode.BuildFile: PropertyListSerializable {
/// Called by the Serializer to serialize the BuildFile.
fileprivate func serialize(to serializer: PropertyListSerializer) throws -> [String: PropertyList] {
// Create a `PBXBuildFile` plist dictionary.
var dict = [String: PropertyList]()
dict["isa"] = .string("PBXBuildFile")
if let fileRef = fileRef {
dict["fileRef"] = .identifier(serializer.id(of: fileRef))
}
let settingsDict = try PropertyList.encode(settings)
if !settingsDict.isEmpty {
dict["settings"] = settingsDict
}
return dict
}
}
extension Xcode.BuildSettingsTable: PropertyListSerializable {
/// Called by the Serializer to serialize the BuildFile. It is serialized
/// as an XCBuildConfigurationList and two additional XCBuildConfiguration
/// objects (one for debug and one for release).
fileprivate func serialize(to serializer: PropertyListSerializer) throws -> [String: PropertyList] {
/// Private wrapper class for BuildSettings structures. This is glue
/// between our value-based settings structures and the Xcode project
/// model's identity-based XCBuildConfiguration objects.
class BuildSettingsDictWrapper: PropertyListSerializable {
let name: String
var baseSettings: BuildSettings
var overlaySettings: BuildSettings
let xcconfigFileRef: Xcode.FileReference?
init(
name: String,
baseSettings: BuildSettings,
overlaySettings: BuildSettings,
xcconfigFileRef: Xcode.FileReference?
) {
self.name = name
self.baseSettings = baseSettings
self.overlaySettings = overlaySettings
self.xcconfigFileRef = xcconfigFileRef
}
func serialize(to serializer: PropertyListSerializer) throws -> [String: PropertyList] {
// Create a `XCBuildConfiguration` plist dictionary.
var dict = [String: PropertyList]()
dict["isa"] = .string("XCBuildConfiguration")
dict["name"] = .string(name)
// Combine the base settings and the overlay settings.
dict["buildSettings"] = try combineBuildSettingsPropertyLists(
baseSettings: .encode(baseSettings),
overlaySettings: .encode(overlaySettings)
)
// Add a reference to the base configuration, if there is one.
if let xcconfigFileRef = xcconfigFileRef {
dict["baseConfigurationReference"] = .identifier(serializer.id(of: xcconfigFileRef))
}
return dict
}
}
// Create a `XCConfigurationList` plist dictionary.
var dict = [String: PropertyList]()
dict["isa"] = .string("XCConfigurationList")
dict["buildConfigurations"] = .array([
// We use a private wrapper to "objectify" our two build settings
// structures (which, being structs, are value types).
try .identifier(serializer.serialize(object: BuildSettingsDictWrapper(
name: "Debug",
baseSettings: common,
overlaySettings: debug,
xcconfigFileRef: xcconfigFileRef))),
try .identifier(serializer.serialize(object: BuildSettingsDictWrapper(
name: "Release",
baseSettings: common,
overlaySettings: release,
xcconfigFileRef: xcconfigFileRef))),
])
// FIXME: What is this, and why are we setting it?
dict["defaultConfigurationIsVisible"] = .string("0")
// FIXME: Should we allow this to be set in the model?
dict["defaultConfigurationName"] = .string("Release")
return dict
}
}
/// Private helper function that combines a base property list and an overlay
/// property list, respecting the semantics of `$(inherited)` as we go.
fileprivate func combineBuildSettingsPropertyLists(
baseSettings: PropertyList,
overlaySettings: PropertyList
) throws -> PropertyList {
// Extract the base and overlay dictionaries.
guard case let .dictionary(baseDict) = baseSettings else {
throw InternalError("base settings plist must be a dictionary")
}
guard case let .dictionary(overlayDict) = overlaySettings else {
throw InternalError("overlay settings plist must be a dictionary")
}
// Iterate over the overlay values and apply them to the base.
var resultDict = baseDict
for (name, value) in overlayDict {
if let array = baseDict[name]?.array, let overlayArray = value.array, overlayArray.first?.string == "$(inherited)" {
resultDict[name] = .array(array + overlayArray.dropFirst())
} else {
resultDict[name] = value
}
}
return .dictionary(resultDict)
}
/// A simple property list serializer with the same semantics as the Xcode
/// property list serializer. Not generally reusable at this point, but only
/// because of implementation details (architecturally it isn't tied to Xcode).
fileprivate class PropertyListSerializer {
/// Private struct that represents a strong reference to a serializable
/// object. This prevents any temporary objects from being deallocated
/// during the serialization and replaced with other objects having the
/// same object identifier (a violation of our assumptions)
struct SerializedObjectRef: Hashable, Equatable {
let object: PropertyListSerializable
init(_ object: PropertyListSerializable) {
self.object = object
}
func hash(into hasher: inout Hasher) {
hasher.combine(ObjectIdentifier(object))
}
static func == (lhs: SerializedObjectRef, rhs: SerializedObjectRef) -> Bool {
return lhs.object === rhs.object
}
}
/// Maps objects to the identifiers that have been assigned to them. The
/// next identifier to be assigned is always one greater than the number
/// of entries in the mapping.
var objsToIds = [SerializedObjectRef: String]()
/// Maps serialized objects ids to dictionaries. This may contain fewer
/// entries than `objsToIds`, since ids are assigned upon reference, but
/// plist dictionaries are created only upon actual serialization. This
/// dictionary is what gets written to the property list.
var idsToDicts = [String: PropertyList]()
/// Returns the quoted identifier for the object, assigning one if needed.
func id(of object: PropertyListSerializable) -> String {
// We need a "serialized object ref" wrapper for the `objsToIds` map.
let serObjRef = SerializedObjectRef(object)
if let id = objsToIds[serObjRef] {
return "\"\(id)\""
}
// We currently always assign identifiers starting at 1 and going up.
// FIXME: This is a suboptimal format for object identifier strings;
// for debugging purposes they should at least sort in numeric order.
let id = object.objectID ?? "OBJ_\(objsToIds.count + 1)"
objsToIds[serObjRef] = id
return "\"\(id)\""
}
/// Serializes `object` by asking it to construct a plist dictionary and
/// then adding that dictionary to the serializer. This may in turn cause
/// recursive invocations of `serialize(object:)`; the closure of these
/// invocations end up serializing the whole object graph.
@discardableResult
func serialize(object: PropertyListSerializable) throws -> String {
// Assign an id for the object, if it doesn't already have one.
let id = self.id(of: object)
// If that id is already in `idsToDicts`, we've detected recursion or
// repeated serialization.
guard idsToDicts[id] == nil else {
throw InternalError("tried to serialize \(object) twice")
}
// Set a sentinel value in the `idsToDicts` mapping to detect recursion.
idsToDicts[id] = .dictionary([:])
// Now recursively serialize the object, and store the result (replacing
// the sentinel).
idsToDicts[id] = try .dictionary(object.serialize(to: self))
// Finally, return the identifier so the caller can store it (usually in
// an attribute in its own serialization dictionary).
return id
}
}
fileprivate protocol PropertyListSerializable: AnyObject {
/// Called by the Serializer to construct and return a dictionary for a
/// serializable object. The entries in the dictionary should represent
/// the receiver's attributes and relationships, as PropertyList values.
///
/// Every object that is written to the Serializer is assigned an id (an
/// arbitrary but unique string). Forward references can use `id(of:)`
/// of the Serializer to assign and access the id before the object is
/// actually written.
///
/// Implementations can use the Serializer's `serialize(object:)` method
/// to serialize owned objects (getting an id to the serialized object,
/// which can be stored in one of the attributes) or can use the `id(of:)`
/// method to store a reference to an unowned object.
///
/// The implementation of this method for each serializable objects looks
/// something like this:
///
/// // Create a `PBXSomeClassOrOther` plist dictionary.
/// var dict = [String: PropertyList]()
/// dict["isa"] = .string("PBXSomeClassOrOther")
/// dict["name"] = .string(name)
/// if let path = path { dict["path"] = .string(path) }
/// dict["mainGroup"] = .identifier(serializer.serialize(object: mainGroup))
/// dict["subitems"] = .array(subitems.map({ .string($0.id) }))
/// dict["cross-ref"] = .identifier(serializer.id(of: unownedObject))
/// return dict
///
/// FIXME: I'm not totally happy with how this looks. It's far too clunky
/// and could be made more elegant. However, since the Xcode project model
/// is static, this is not something that will need to evolve over time.
/// What does need to evolve, which is how the project model is constructed
/// from the package contents, is where the elegance and simplicity really
/// matters. So this is acceptable for now in the interest of getting it
/// done.
/// A custom ID to use for the instance, if enabled.
///
/// This ID must be unique across the entire serialized graph.
var objectID: String? { get }
/// Should create and return a property list dictionary of the object's
/// attributes. This function may also use the serializer's `serialize()`
/// function to serialize other objects, and may use `id(of:)` to access
/// ids of objects that either have or will be serialized.
func serialize(to serializer: PropertyListSerializer) throws -> [String: PropertyList]
}
extension PropertyListSerializable {
var objectID: String? {
return nil
}
}
extension PropertyList {
var isEmpty: Bool {
switch self {
case let .identifier(string): return string.isEmpty
case let .string(string): return string.isEmpty
case let .array(array): return array.isEmpty
case let .dictionary(dictionary): return dictionary.isEmpty
}
}
}

View File

@@ -0,0 +1,227 @@
//===--- Options.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 SwiftXcodeGen
enum LogLevelOption: String, CaseIterable {
case debug, info, note, warning, error
}
extension LogLevelOption: ExpressibleByArgument {
init?(argument: String) {
self.init(rawValue: argument)
}
}
extension Logger.LogLevel {
init(_ option: LogLevelOption) {
self = switch option {
case .debug: .debug
case .info: .info
case .note: .note
case .warning: .warning
case .error: .error
}
}
}
extension ArgumentHelp {
static func hidden(_ abstract: String) -> Self {
.init(abstract, visibility: .hidden)
}
}
struct LLVMProjectOptions: ParsableArguments {
@Flag(
name: .customLong("clang"), inversion: .prefixedNo,
help: "Generate an xcodeproj for Clang"
)
var addClang: Bool = false
@Flag(
name: .customLong("clang-tools-extra"), inversion: .prefixedNo,
help: """
When generating a project for Clang, whether to include clang-tools-extra
"""
)
var addClangToolsExtra: Bool = true
// FIXME: Semantic functionality is currently not supported, unhide when
// fixed.
@Flag(
name: .customLong("compiler-rt"), inversion: .prefixedNo,
help: .hidden("""
When generating a project for LLVM, whether to include compiler-rt.
""")
)
var addCompilerRT: Bool = false
@Flag(
name: .customLong("lldb"), inversion: .prefixedNo,
help: "Generate an xcodeproj for LLDB"
)
var addLLDB: Bool = false
@Flag(
name: .customLong("llvm"), inversion: .prefixedNo,
help: "Generate an xcodeproj for LLVM"
)
var addLLVM: Bool = false
}
struct SwiftTargetOptions: ParsableArguments {
@Flag(
name: .customLong("swift-targets"), inversion: .prefixedNo,
help: """
Generate targets for Swift files, e.g ASTGen, SwiftCompilerSources. Note
this by default excludes the standard library, see '--stdlib-swift'.
"""
)
var addSwiftTargets: Bool = true
@Flag(
name: .customLong("swift-dependencies"), inversion: .prefixedNo,
help: """
When generating Swift targets, add dependencies (e.g swift-syntax) to the
generated project. This makes build times slower, but improves syntax
highlighting for targets that depend on them.
"""
)
var addSwiftDependencies: Bool = true
}
struct RunnableTargetOptions: ParsableArguments {
@Option(
name: .customLong("runnable-build-dir"),
help: """
If specified, runnable targets will use this build directory. Useful for
configurations where a separate debug build directory is used.
"""
)
var runnableBuildDir: AnyPath?
@Flag(
name: .customLong("runnable-targets"), inversion: .prefixedNo,
help: """
Whether to add runnable targets for e.g swift-frontend. This is useful
for debugging in Xcode.
"""
)
var addRunnableTargets: Bool = true
@Flag(
name: .customLong("build-runnable-targets"), inversion: .prefixedNo,
help: """
If runnable targets are enabled, whether to add a build action for them.
If false, they will be added as freestanding schemes.
"""
)
var addBuildForRunnableTargets: Bool = true
}
struct ProjectOptions: ParsableArguments {
// Hidden as mostly only useful for testing purposes.
@Flag(
name: .customLong("clang-targets"), inversion: .prefixedNo,
help: .hidden
)
var addClangTargets: Bool = true
@Flag(
name: .customLong("compiler-libs"), inversion: .prefixedNo,
help: "Generate targets for compiler libraries"
)
var addCompilerLibs: Bool = true
@Flag(
name: .customLong("compiler-tools"), inversion: .prefixedNo,
help: "Generate targets for compiler tools"
)
var addCompilerTools: Bool = true
@Flag(
name: .customLong("docs"), inversion: .prefixedNo,
help: "Add doc groups to the generated projects"
)
var addDocs: Bool = true
@Flag(
name: [.customLong("stdlib"), .customLong("stdlib-cxx")],
inversion: .prefixedNo,
help: "Generate a target for C/C++ files in the standard library"
)
var addStdlibCxx: Bool = true
@Flag(
name: .customLong("stdlib-swift"), inversion: .prefixedNo,
help: """
Generate targets for Swift files in the standard library. This requires
using Xcode with with a main development snapshot (and as such is disabled
by default).
"""
)
var addStdlibSwift: Bool = false
@Flag(
name: .customLong("test-folders"), inversion: .prefixedNo,
help: "Add folder references for test files"
)
var addTestFolders: Bool = true
@Flag(
name: .customLong("unittests"), inversion: .prefixedNo,
help: "Generate a target for the unittests"
)
var addUnitTests: Bool = true
@Flag(
name: .customLong("infer-args"), inversion: .prefixedNo,
help: """
Whether to infer build arguments for files that don't have any, based
on the build arguments of surrounding files. This is mainly useful for
files that aren't built in the default config, but are still useful to
edit (e.g sourcekitdAPI-InProc.cpp).
"""
)
var inferArgs: Bool = true
@Option(help: .hidden)
var blueFolders: String = ""
}
struct MiscOptions: ParsableArguments {
@Option(help: """
The project root directory, which is the parent directory of the Swift repo.
By default this is inferred from the build directory path.
""")
var projectRootDir: AnyPath?
@Option(help: """
The output directory to write the Xcode project to. Defaults to the project
root directory.
""")
var outputDir: AnyPath?
@Option(help: "The log level verbosity (default: info)")
var logLevel: LogLevelOption?
@Flag(
name: .long, inversion: .prefixedNo,
help: "Parallelize generation of projects"
)
var parallel: Bool = true
@Flag(
name: .shortAndLong,
help: "Quiet output; equivalent to --log-level warning"
)
var quiet: Bool = false
}

View File

@@ -0,0 +1,359 @@
//===--- 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 Darwin
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,
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.resolvingSymlinks
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)
}
@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
)
if self.addTestFolders {
spec.addReference(to: "../clang-tools-extra/test", isImportant: true)
} 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", isImportant: true)
} 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, Error> {
let task = Task(operation: body)
if !self.parallel {
_ = try await task.value
}
return task
}
func generate() async throws {
let buildDirPath = buildDir.absoluteInWorkingDir.resolvingSymlinks
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 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
}
let swiftWorkspace = try await getWorkspace(for: swiftProj.value)
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 run() async {
// Set the log level
log.logLevel = .init(self.logLevel ?? (self.quiet ? .warning : .info))
do {
try await generate()
} catch {
log.error("\(error)")
}
if log.hadError {
Darwin.exit(1)
}
}
}

View File

@@ -0,0 +1,264 @@
//===--- CompileCommandsTests.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 XCTest
@testable import SwiftXcodeGen
fileprivate func assertParse(
_ str: String, executable: String? = nil, args: [Command.Argument],
knownCommandOnly: Bool = false,
file: StaticString = #file, line: UInt = #line
) {
do {
let command = try knownCommandOnly ? CommandParser.parseKnownCommandOnly(str)
: CommandParser.parseCommand(str)
guard let command else {
XCTFail("Failed to parse command")
return
}
if let executable {
XCTAssertEqual(executable, command.executable.rawPath, file: file, line: line)
}
XCTAssertEqual(args, command.args, file: file, line: line)
} catch {
XCTFail("\(error)", file: file, line: line)
}
}
class CompileCommandsTests: XCTestCase {
func testClangCommandParse() {
assertParse("x -a -b", executable: "x", args: [.value("-a"), .value("-b")])
assertParse("x -D -I", executable: "x", args: [.value("-D"), .value("-I")])
assertParse(
"x y clang -DX -I",
executable: "clang",
args: [.option(.D, spacing: .unspaced, value: "X"), .flag(.I)],
knownCommandOnly: true
)
assertParse(
"x y x/y/clang -DX -I",
executable: "x/y/clang",
args: [.option(.D, spacing: .unspaced, value: "X"), .flag(.I)],
knownCommandOnly: true
)
assertParse(
"clang -DX -I",
args: [.option(.D, spacing: .unspaced, value: "X"), .flag(.I)]
)
assertParse("clang++ -D I", args: [
.option(.D, spacing: .spaced, value: "I")
])
assertParse("clang -DI", args: [
.option(.D, spacing: .unspaced, value: "I")
])
assertParse("clang -DIII", args: [
.option(.D, spacing: .unspaced, value: "III")
])
assertParse("clang -DIII I", args: [
.option(.D, spacing: .unspaced, value: "III"), .value("I")
])
assertParse(
#"clang -D"III" I"#, args: [
.option(.D, spacing: .unspaced, value: #"III"#), .value("I")
]
)
assertParse(
#"clang -D\"III\" -I"#, args: [
.option(.D, spacing: .unspaced, value: #""III""#), .flag(.I)
]
)
assertParse(
#"clang -D"a b" -I"#, args: [
.option(.D, spacing: .unspaced, value: #"a b"#), .flag(.I)
]
)
assertParse(
#"clang -Da\ b -I"#, args: [
.option(.D, spacing: .unspaced, value: #"a b"#), .flag(.I)
]
)
assertParse(
#"clang -I"III""#, args: [
.option(.I, spacing: .unspaced, value: #"III"#)
]
)
assertParse(
#"clang -I\"III\""#, args: [
.option(.I, spacing: .unspaced, value: #""III""#)
]
)
assertParse(
#"clang -I"a b""#, args: [
.option(.I, spacing: .unspaced, value: #"a b"#)
]
)
assertParse(
#"clang -Ia\ b"#, args: [
.option(.I, spacing: .unspaced, value: #"a b"#)
]
)
assertParse(
#"clang -I="III""#, args: [
.option(.I, spacing: .equals, value: #"III"#)
]
)
assertParse(
#"clang -I="#, args: [
.option(.I, spacing: .unspaced, value: #"="#)
]
)
assertParse(
#"clang -I=\"III\""#, args: [
.option(.I, spacing: .equals, value: #""III""#)
]
)
assertParse(
#"clang -I="a b""#, args: [
.option(.I, spacing: .equals, value: #"a b"#)
]
)
assertParse(
#"clang -I=a\ b"#, args: [
.option(.I, spacing: .equals, value: #"a b"#)
]
)
assertParse(
#"clang -Wnosomething"#, args: [
.option(.W, spacing: .unspaced, value: #"nosomething"#)
]
)
assertParse(
#"clang --I=a"#, args: [.value("--I=a")]
)
assertParse(
#"clang --Da"#, args: [.value("--Da")]
)
assertParse(
#"clang --Wa"#, args: [.value("--Wa")]
)
}
func testSwiftCommandParse() {
assertParse(
#"swiftc -FX"#,
args: [.option(.F, spacing: .unspaced, value: "X")]
)
assertParse(
#"swiftc -F X"#,
args: [.option(.F, spacing: .spaced, value: "X")]
)
assertParse(
#"swiftc -F=X"#,
args: [.option(.F, spacing: .equals, value: "X")]
)
assertParse(
#"swiftc -Fsystem X"#,
args: [.option(.Fsystem, spacing: .spaced, value: "X")]
)
}
func testCommandEscape() {
XCTAssertEqual(Command.Argument.flag(.I).printedArgs, ["-I"])
XCTAssertEqual(Command.Argument.value("hello").printedArgs, ["hello"])
XCTAssertEqual(Command.Argument.value("he llo").printedArgs, [#""he llo""#])
XCTAssertEqual(Command.Argument.value(#""hello""#).printedArgs, [#"\"hello\""#])
XCTAssertEqual(Command.Argument.value(#""he llo""#).printedArgs, [#""\"he llo\"""#])
XCTAssertEqual(
Command.Argument.option(
.I, spacing: .unspaced, value: "he llo"
).printedArgs,
[#"-I"he llo""#]
)
XCTAssertEqual(
Command.Argument.option(
.I, spacing: .spaced, value: "he llo"
).printedArgs,
["-I", #""he llo""#]
)
XCTAssertEqual(
Command.Argument.option(
.I, spacing: .unspaced, value: #""he llo""#
).printedArgs,
[#"-I"\"he llo\"""#]
)
XCTAssertEqual(
Command.Argument.option(
.I, spacing: .spaced, value: #""he llo""#
).printedArgs,
["-I", #""\"he llo\"""#]
)
XCTAssertEqual(
try CommandParser.parseCommand(#"swift \\ \ "#).printed,
#"swift \\ " ""#
)
XCTAssertEqual(
try CommandParser.parseCommand(#"swift "\\ ""#).printed,
#"swift "\\ ""#
)
}
func testEmptyArg() {
// The empty string immediately after '-I' is effectively ignored.
assertParse(#"swiftc -I"" """#, args: [
.option(.I, spacing: .spaced, value: ""),
])
assertParse(#"swiftc -I "" """#, args: [
.option(.I, spacing: .spaced, value: ""),
.value(""),
])
assertParse(#"swiftc -I "" "" "#, args: [
.option(.I, spacing: .spaced, value: ""),
.value(""),
])
assertParse(#"swiftc -I "#, args: [
.flag(.I),
])
}
func testSpaceBeforeCommand() {
assertParse(" swiftc ", executable: "swiftc", args: [])
assertParse("\t\tswiftc\t\ta b\t", executable: "swiftc", args: [
.value("a"),
.value("b"),
])
}
}

View File

@@ -0,0 +1,189 @@
//===--- NinjaParserTests.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 XCTest
@testable import SwiftXcodeGen
fileprivate func expectEqual<T: Equatable>(
expected: [T], actual: [T], description: String,
file: StaticString = #file, line: UInt = #line
) {
guard expected.count == actual.count else {
XCTFail(
"""
Expected \(expected.count) '\(description)', \
got \(actual.count) (\(actual))
""",
file: file, line: line
)
return
}
for (expected, actual) in zip(expected, actual) {
XCTAssertEqual(expected, actual, file: file, line: line)
}
}
fileprivate func expectEqual<T, U: Equatable>(
_ expected: T, _ actual: T, _ kp: KeyPath<T, U>,
file: StaticString = #file, line: UInt = #line
) {
XCTAssertEqual(
expected[keyPath: kp], actual[keyPath: kp], file: file, line: line
)
}
fileprivate func expectEqual<T, U: Equatable>(
_ expected: T, _ actual: T, _ kp: KeyPath<T, [U]>,
file: StaticString = #file, line: UInt = #line
) {
expectEqual(
expected: expected[keyPath: kp], actual: actual[keyPath: kp],
description: "\(kp)", file: file, line: line
)
}
fileprivate func assertParse(
_ str: String,
attributes: [NinjaBuildFile.Attribute] = [],
rules: [NinjaBuildFile.BuildRule],
file: StaticString = #file, line: UInt = #line
) {
do {
let buildFile = try NinjaParser.parse(Data(str.utf8))
guard rules.count == buildFile.buildRules.count else {
XCTFail(
"Expected \(rules.count) rules, got \(buildFile.buildRules.count)",
file: file, line: line
)
return
}
XCTAssertEqual(
Dictionary(uniqueKeysWithValues: attributes.map { ($0.key, $0) }),
buildFile.attributes,
file: file, line: line
)
for (expected, actual) in zip(rules, buildFile.buildRules) {
expectEqual(expected, actual, \.inputs, file: file, line: line)
expectEqual(expected, actual, \.outputs, file: file, line: line)
expectEqual(expected, actual, \.dependencies, file: file, line: line)
expectEqual(expected, actual, \.attributes, file: file, line: line)
expectEqual(expected, actual, \.isPhony, file: file, line: line)
XCTAssertEqual(expected, actual, file: file, line: line)
}
} catch {
XCTFail("\(error)", file: file, line: line)
}
}
class NinjaParserTests: XCTestCase {
func testBuildRule() throws {
assertParse("""
# ignore comment, build foo.o: a.swift | dep || orderdep
#another build comment
build foo.o foo.swiftmodule: a.swift | dep || orderdep
notpartofthebuildrule
""", rules: [
.init(
inputs: ["a.swift"],
outputs: ["foo.o", "foo.swiftmodule"],
dependencies: ["dep", "orderdep"],
attributes: [:]
)
]
)
}
func testPhonyRule() throws {
assertParse("""
build foo.swiftmodule : phony bar.swiftmodule
""", rules: [
.phony(
for: ["foo.swiftmodule"],
inputs: ["bar.swiftmodule"]
)
]
)
}
func testAttributes() throws {
assertParse("""
x = y
CONFIGURATION = Debug
build foo.o: xyz foo.swift | baz.o
UNKNOWN = foobar
SWIFT_MODULE_NAME = foobar
#ignore trivia between attributes
\u{20}
#ignore trivia between attributes
FLAGS = -I /a/b -wmo
ANOTHER_UNKNOWN = a b c
build baz.o: CUSTOM_COMMAND baz.swift
COMMAND = /bin/swiftc -I /a/b -wmo
FLAGS = -I /c/d -wmo
""", attributes: [
.init(key: .configuration, value: "Debug"),
// This is considered top-level since it's not indented.
.init(key: .flags, value: "-I /c/d -wmo")
],
rules: [
.init(
inputs: ["xyz", "foo.swift"],
outputs: ["foo.o"],
dependencies: ["baz.o"],
attributes: [
.swiftModuleName: .init(key: .swiftModuleName, value: "foobar"),
.flags: .init(key: .flags, value: "-I /a/b -wmo"),
]
),
.init(
inputs: ["CUSTOM_COMMAND", "baz.swift"],
outputs: ["baz.o"],
dependencies: [],
attributes: [
.command: .init(key: .command, value: "/bin/swiftc -I /a/b -wmo"),
]
)
]
)
}
func testEscape() throws {
for newline in ["\n", "\r", "\r\n"] {
assertParse("""
build foo.o$:: xyz$ foo$$.swift | baz$ bar.o
FLAGS = -I /a$\(newline)\
/b -wmo
COMMAND = swiftc$$
""", rules: [
.init(
inputs: ["xyz foo$.swift"],
outputs: ["foo.o:"],
dependencies: ["baz bar.o"],
attributes: [
.flags: .init(key: .flags, value: "-I /a/b -wmo"),
.command: .init(key: .command, value: "swiftc$")
]
)
]
)
}
}
}

View File

@@ -0,0 +1,28 @@
//===--- PathTests.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 XCTest
@testable import SwiftXcodeGen
class PathTests: XCTestCase {
func testRelativeParent() throws {
XCTAssertEqual(RelativePath("").parentDir, nil)
XCTAssertEqual(RelativePath("foo").parentDir, nil)
XCTAssertEqual(RelativePath("foo/bar").parentDir, "foo")
}
func testAbsoluteParent() throws {
XCTAssertEqual(AbsolutePath("/").parentDir, nil)
XCTAssertEqual(AbsolutePath("/foo").parentDir, "/")
XCTAssertEqual(AbsolutePath("/foo/bar").parentDir, "/foo")
}
}

View File

@@ -0,0 +1,25 @@
//===--- ScannerTests.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 XCTest
@testable import SwiftXcodeGen
class ScannerTests: XCTestCase {
func testReplacement() {
// Currently implemented using BinaryScanner for ASCII cases.
XCTAssertEqual("b", "a".replacing("a", with: "b"))
XCTAssertEqual("bbbb", "abaa".replacing("a", with: "b"))
XCTAssertEqual("a", "a".replacing("aaaa", with: "b"))
XCTAssertEqual("cca", "ababa".replacing("ab", with: "c"))
XCTAssertEqual("ccbccbcc", "ababa".replacing("a", with: "cc"))
}
}

View File

@@ -0,0 +1,2 @@
#!/bin/zsh
exec swift run -c release --package-path "$0:A:h" swift-xcodegen "$@"

View File

@@ -0,0 +1,29 @@
# RUN: %empty-directory(%t)
# RUN: %empty-directory(%t/src/swift/utils)
# RUN: %empty-directory(%t/out)
# RUN: export PATH=%original_path_env
# RUN: export SWIFTCI_USE_LOCAL_DEPS=1
# REQUIRES: OS=macosx
# REQUIRES: standalone_build
# REQUIRES: target-same-as-host
# REQUIRES: issue_77407
# First copy swift-xcodegen to the temporary location
# so we don't touch the user's build, and make sure
# we're doing a clean build.
# RUN: cp -r %swift_src_root/utils/swift-xcodegen %t/src/swift/utils/swift-xcodegen
# RUN: rm -rf %t/src/swift/utils/swift-xcodegen/.build
# Add symlinks for local dependencies
# RUN: ln -s %swift_src_root/../swift-* %t/src
# RUN: ln -s %swift_src_root/../llbuild %t/src
# RUN: ln -s %swift_src_root/../yams %t/src
# Run the xcodegen test suite
# RUN: xcrun swift-test --package-path %t/src/swift/utils/swift-xcodegen
# Then check to see that xcodegen can generate a project successfully
# RUN: xcrun swift-run --package-path %t/src/swift/utils/swift-xcodegen swift-xcodegen --project-root-dir %swift_src_root/.. --output-dir %t/out %swift_obj_root/..
# RUN: ls %t/out/Swift.xcodeproj > /dev/null