mirror of
https://github.com/apple/swift.git
synced 2025-12-14 20:36:38 +01:00
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:
2
.github/CODEOWNERS
vendored
2
.github/CODEOWNERS
vendored
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
2
utils/generate-xcode
Executable file
@@ -0,0 +1,2 @@
|
||||
#!/bin/zsh
|
||||
exec "$0:A:h/swift-xcodegen/swift-xcodegen" "$@"
|
||||
6
utils/swift-xcodegen/.gitignore
vendored
Normal file
6
utils/swift-xcodegen/.gitignore
vendored
Normal file
@@ -0,0 +1,6 @@
|
||||
.DS_Store
|
||||
/.build
|
||||
/Packages
|
||||
/*.xcodeproj
|
||||
xcuserdata/
|
||||
.swiftpm
|
||||
59
utils/swift-xcodegen/Package.resolved
Normal file
59
utils/swift-xcodegen/Package.resolved
Normal 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
|
||||
}
|
||||
50
utils/swift-xcodegen/Package.swift
Normal file
50
utils/swift-xcodegen/Package.swift
Normal 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"),
|
||||
]
|
||||
}
|
||||
108
utils/swift-xcodegen/README.md
Normal file
108
utils/swift-xcodegen/README.md
Normal 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
|
||||
@@ -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 }
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
@@ -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))
|
||||
}
|
||||
}
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
@@ -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 == "$"
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
}
|
||||
286
utils/swift-xcodegen/Sources/SwiftXcodeGen/Command/Command.swift
Normal file
286
utils/swift-xcodegen/Sources/SwiftXcodeGen/Command/Command.swift
Normal 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
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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]
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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()
|
||||
}
|
||||
}
|
||||
@@ -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([])
|
||||
}
|
||||
}
|
||||
30
utils/swift-xcodegen/Sources/SwiftXcodeGen/Error.swift
Normal file
30
utils/swift-xcodegen/Sources/SwiftXcodeGen/Error.swift
Normal 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"
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
)
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
@@ -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))
|
||||
}
|
||||
}
|
||||
@@ -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()
|
||||
}
|
||||
}
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
@@ -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()
|
||||
}
|
||||
}
|
||||
@@ -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))
|
||||
}
|
||||
}
|
||||
|
||||
176
utils/swift-xcodegen/Sources/SwiftXcodeGen/Logging/Logger.swift
Normal file
176
utils/swift-xcodegen/Sources/SwiftXcodeGen/Logging/Logger.swift
Normal 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()
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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"
|
||||
}
|
||||
}
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
@@ -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) })
|
||||
}
|
||||
}
|
||||
@@ -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))
|
||||
}
|
||||
}
|
||||
37
utils/swift-xcodegen/Sources/SwiftXcodeGen/Repo.swift
Normal file
37
utils/swift-xcodegen/Sources/SwiftXcodeGen/Repo.swift
Normal 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"
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
}
|
||||
134
utils/swift-xcodegen/Sources/SwiftXcodeGen/Utils.swift
Normal file
134
utils/swift-xcodegen/Sources/SwiftXcodeGen/Utils.swift
Normal 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)
|
||||
}
|
||||
}
|
||||
}
|
||||
19
utils/swift-xcodegen/Sources/Xcodeproj/Errors.swift
Normal file
19
utils/swift-xcodegen/Sources/Xcodeproj/Errors.swift
Normal 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
|
||||
}
|
||||
}
|
||||
158
utils/swift-xcodegen/Sources/Xcodeproj/PropertyList.swift
Normal file
158
utils/swift-xcodegen/Sources/Xcodeproj/PropertyList.swift
Normal 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 += "}"
|
||||
}
|
||||
}
|
||||
}
|
||||
231
utils/swift-xcodegen/Sources/Xcodeproj/PropertyListEncoder.swift
Normal file
231
utils/swift-xcodegen/Sources/Xcodeproj/PropertyListEncoder.swift
Normal 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") }
|
||||
}
|
||||
}
|
||||
6
utils/swift-xcodegen/Sources/Xcodeproj/README.md
Normal file
6
utils/swift-xcodegen/Sources/Xcodeproj/README.md
Normal 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
|
||||
439
utils/swift-xcodegen/Sources/Xcodeproj/XcodeProjectModel.swift
Normal file
439
utils/swift-xcodegen/Sources/Xcodeproj/XcodeProjectModel.swift
Normal 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?
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
}
|
||||
227
utils/swift-xcodegen/Sources/swift-xcodegen/Options.swift
Normal file
227
utils/swift-xcodegen/Sources/swift-xcodegen/Options.swift
Normal 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
|
||||
}
|
||||
359
utils/swift-xcodegen/Sources/swift-xcodegen/SwiftXcodegen.swift
Normal file
359
utils/swift-xcodegen/Sources/swift-xcodegen/SwiftXcodegen.swift
Normal 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)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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"),
|
||||
])
|
||||
}
|
||||
}
|
||||
@@ -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$")
|
||||
]
|
||||
)
|
||||
]
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
28
utils/swift-xcodegen/Tests/SwiftXcodeGenTest/PathTests.swift
Normal file
28
utils/swift-xcodegen/Tests/SwiftXcodeGenTest/PathTests.swift
Normal 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")
|
||||
}
|
||||
}
|
||||
@@ -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"))
|
||||
}
|
||||
}
|
||||
2
utils/swift-xcodegen/swift-xcodegen
Executable file
2
utils/swift-xcodegen/swift-xcodegen
Executable file
@@ -0,0 +1,2 @@
|
||||
#!/bin/zsh
|
||||
exec swift run -c release --package-path "$0:A:h" swift-xcodegen "$@"
|
||||
29
validation-test/BuildSystem/swift-xcodegen.test
Normal file
29
validation-test/BuildSystem/swift-xcodegen.test
Normal 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
|
||||
Reference in New Issue
Block a user