├── .gitattributes
├── .gitignore
├── Documentation
├── Grammar.md
├── Types.md
└── Variables.md
├── Package.swift
├── Sources
└── antimony
│ ├── Configuration
│ ├── BuildConfiguration.swift
│ ├── BuildConfigurationDelegate.swift
│ ├── BuildConfigurationOptions.swift
│ ├── Label.swift
│ ├── Loader.swift
│ └── Target.swift
│ ├── Error.swift
│ ├── Extensions
│ └── SwiftDriver+Extensions.swift
│ ├── JobEmitter.swift
│ ├── NULLExecutor.swift
│ ├── NinjaWriter.swift
│ ├── SourceFile.swift
│ └── Utils
│ └── FileURL.swift
└── Tools
└── sb
└── sb.swift
/.gitattributes:
--------------------------------------------------------------------------------
1 | .gitignore eol=lf
2 | *.swift eol=lf
3 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | .vscode/
2 | out/
3 |
4 | *.sw?
5 |
--------------------------------------------------------------------------------
/Documentation/Grammar.md:
--------------------------------------------------------------------------------
1 |
2 | Whitespace is comprised of spaces (\U{0020}), horizontal tabs (\U{0009}),
3 | carriage returns (\U{000d}), and newlines (\U{000a}).
4 |
5 | Comments start at the character '#' and stop at the next newline.
6 |
7 | ```ebnf
8 | file = statement-list .
9 |
10 | (* statemnt *)
11 | statement = assignment | call | condition .
12 | assignment = ( identifier | array-access | scope-access ) assignment-operator expression .
13 | call = identifier '(' [ expression-list ] ')' [ block ] .
14 | condition = 'if' '(' expression ')' block
15 | [ 'else' ( condition | block ) ] .
16 | block = '{' statement-list '}' .
17 | statement-list = { statement } .
18 |
19 | (* expression *)
20 | expression = unary-expression
21 | | expression binary-operator expression .
22 | unary-expression = primary-expression
23 | | unary-operator unary-expression .
24 | primary-expression = identifier
25 | | integer
26 | | string
27 | | call
28 | | array-access
29 | | scope-access
30 | | block
31 | | '(' expression ')'
32 | | '[' [ expression-list [ ',' ] ]']' .
33 | array-access = identifier '[' expression ']' .
34 | scope-acess = identifier '.' identifier .
35 | expression-list = expression { ',' expression } .
36 |
37 | (* operators *)
38 | assignment-operator = '=' | '+=' | '-=' .
39 | unary-operator = '!' .
40 | binary-operator = '+' | '-'
41 | | '<' | '<=' | '>' | '>='
42 | | '==' | '!='
43 | | '&&'
44 | | '||' .
45 |
46 | (* terminals *)
47 | identifier = letter { letter | digit } .
48 | letter = ? 'A' ... 'Z' | 'a' ... 'z' | '_' ? .
49 | digit = ? '0' ... '9' ? .
50 |
51 | integer = [ '-' ] digit { digit } .
52 |
53 | string = '"' { char | escape | expansion } '"' .
54 | escape = '\' ( '$' | '"' | char ) .
55 | expansion = '$' ( identifier | bracket-expansion | hex-value } .
56 | bracket-expansion = '{' ( identifier | array-access | scope-access ) '}' .
57 | hex-value = '0x' ( digit | hex-letter ) { ( digit | hex-letter ) } .
58 | hex-letter = 'a' | 'b' | 'c' | 'd' | 'e' | 'f' | 'A' | 'B' | 'C' | 'D' | 'E' | 'F' .
59 | char = ? any character except '$', '"', or newline ? .
60 | ```
61 |
--------------------------------------------------------------------------------
/Documentation/Types.md:
--------------------------------------------------------------------------------
1 |
2 | The BUILD language is dynamically typed. The following types are defined within
3 | the language:
4 |
5 |
6 | - boolean
7 | -
8 | Booleans use the reserved keywords `true` and `false` as boolean identifiers.
9 | The boolean type is strongly typed and is not implicitly casted between
10 | integeral and boolean types.
11 |
12 | - integer
13 | -
14 | Integers are always 64-bit signed values. Integers are always written as
15 | decimal values.
16 |
17 | - string
18 | -
19 | Strings are encoded as UTF-8 values, irrespective of the OS's preferred
20 | encoding. When interacting with the filesystem on Windows, the strings will be
21 | re-encoded to UTF-16.
22 |
23 | - list
24 | -
25 | Lists are arbitrary length, ordered lists of values. Lists are indexed using
26 | 0-based indicies and are mutable.
27 |
28 | - scope
29 | -
30 | Scopes are lexical scoping which can be used for associating keys with values
31 | within the region. Scopes are used to bound the lifetime of variables within
32 | the body of a function call or template evaluation.
33 |
34 |
35 |
--------------------------------------------------------------------------------
/Documentation/Variables.md:
--------------------------------------------------------------------------------
1 |
2 | The following variables are pre-defined built-in values for defining the build:
3 |
4 |
5 | - antimony_version
6 | -
7 | [integer] The version of antimony.
8 | Encodes the antimony version as a integral representation using 2-digits per component and encoding the version as $major * 10000 + minor * 100 + patch$.
9 |
10 | - build_cpu
11 | -
12 | [string] The processor architecture for the build machine.
13 | This encodes the CPU identifier for the build machine. The value is spelt according to the Swift `arch` conditional function parameter.
14 |
15 | - build_os
16 | -
17 | [string] The operating system for the build machine.
18 | This encodes the OS identifier for the build machine. The value is spelt according to teh Swift `os` conditional function parameter but in lowercase.
19 |
20 | - host_cpu
21 | -
22 | [string] The processor architecture for the host machine.
23 | This encodes the CPU identifier for the host machine where the binaries will run. The value is spelt according to the Swift `arch` conditional function parameter.
24 |
25 | - host_os
26 | -
27 | [string] The operating system for the host machine.
28 | This encodes the OS identifier for the host machine where the binaries will run. The value is spelt according to the Swift `arch` conditional function parameter.
29 |
30 |
31 |
--------------------------------------------------------------------------------
/Package.swift:
--------------------------------------------------------------------------------
1 | // swift-tools-version:5.9
2 |
3 | import PackageDescription
4 |
5 | _ = Package(name: "antimony",
6 | platforms: [
7 | .macOS(.v12),
8 | ],
9 | products: [
10 | .executable(name: "sb", targets: ["sb"])
11 | ],
12 | dependencies: [
13 | .package(url: "https://github.com/apple/swift-argument-parser",
14 | from: "1.2.0"),
15 | .package(url: "https://github.com/apple/swift-driver",
16 | branch: "main"),
17 | ],
18 | targets: [
19 | .executableTarget(name: "sb", dependencies: [
20 | "antimony",
21 | .product(name: "ArgumentParser", package: "swift-argument-parser"),
22 | ], path: "Tools/sb"),
23 | .target(name: "antimony", dependencies: [
24 | .product(name: "SwiftDriver", package: "swift-driver"),
25 | ])
26 | ])
27 |
--------------------------------------------------------------------------------
/Sources/antimony/Configuration/BuildConfiguration.swift:
--------------------------------------------------------------------------------
1 | // Copyright © 2024 Saleem Abdulrasool
2 | // SPDX-License-Identifier: BSD-3-Clause
3 |
4 | import class Foundation.FileManager
5 | import struct Foundation.URL
6 |
7 | public class BuildConfiguration {
8 | var loader: Loader?
9 |
10 | private var registered: [Label:Target] = [:]
11 | private let fs = Foundation.FileManager.default
12 |
13 | private func repo() throws -> URL? {
14 | var root = URL(fileURLWithPath: fs.currentDirectoryPath)
15 | repeat {
16 | if fs.fileExists(atPath: root.appendingPathComponent(".sb").path) {
17 | return root
18 | }
19 | root.deleteLastPathComponent()
20 | } while !(root.path == "/..")
21 |
22 | return nil
23 | }
24 |
25 | public init() {}
26 |
27 | public func load(_ options: BuildConfigurationOptions,
28 | for operation: Operation) async throws {
29 | self.loader = LegacyLoader_DO_NOT_USE_OR_YOU_WILL_BE_FIRED(self)
30 | guard let loader else { return }
31 |
32 | guard let root = try options.root?.url ?? repo() else {
33 | // throw DirectoryNotFound(.source)
34 | return
35 | }
36 | let out = URL(fileURLWithPath: options.location, isDirectory: true,
37 | relativeTo: URL(fileURLWithPath: fs.currentDirectoryPath))
38 |
39 | try loader.load(URL(fileURLWithPath: "placeholder.json", relativeTo: root),
40 | root: root)
41 |
42 | switch operation {
43 | case .format:
44 | for target in registered.values {
45 | print(target)
46 | }
47 |
48 | case .generate:
49 | try fs.createDirectory(at: out, withIntermediateDirectories: true)
50 |
51 | let writer = NinjaWriter()
52 | let emitter = JobEmitter(for: self, into: writer)
53 | for target in registered.values {
54 | try emitter.emitJobs(for: target, flags: ["-use-ld=lld"], at: out)
55 | }
56 | try writer.write(to: URL(fileURLWithPath: "build.ninja", relativeTo: out))
57 | }
58 | }
59 | }
60 |
61 | extension BuildConfiguration: BuildConfigurationDelegate {
62 | public func resolved(label: Label, to target: Target) {
63 | registered[label] = target
64 | }
65 |
66 | public func lookup(_ label: Label) -> Target? {
67 | registered[label]
68 | }
69 | }
70 |
--------------------------------------------------------------------------------
/Sources/antimony/Configuration/BuildConfigurationDelegate.swift:
--------------------------------------------------------------------------------
1 | // Copyright © 2024 Saleem Abdulrasool
2 | // SPDX-License-Identifier: BSD-3-Clause
3 |
4 | public protocol BuildConfigurationDelegate: AnyObject {
5 | func resolved(label: Label, to target: Target)
6 | func lookup(_ label: Label) -> Target?
7 | }
8 |
--------------------------------------------------------------------------------
/Sources/antimony/Configuration/BuildConfigurationOptions.swift:
--------------------------------------------------------------------------------
1 | // Copyright © 2024 Saleem Abdulrasool
2 | // SPDX-License-Identifier: BSD-3-Clause
3 |
4 | import ArgumentParser
5 |
6 | public final class BuildConfigurationOptions: ParsableArguments {
7 | @Option
8 | var root: FileURL?
9 |
10 | @Argument(help: .init(valueName: "out_dir"))
11 | var location: String
12 |
13 | public init() {}
14 | }
15 |
--------------------------------------------------------------------------------
/Sources/antimony/Configuration/Label.swift:
--------------------------------------------------------------------------------
1 | // Copyright © 2024 Saleem Abdulrasool
2 | // SPDX-License-Identifier: BSD-3-Clause
3 |
4 | import WinSDK
5 | import struct Foundation.URL
6 |
7 | /// A label is a unique identifier for a BUILD entity.
8 | ///
9 | /// Everything participating in the build graph is identified by a label. It is
10 | /// rooted at the workspace root (identified explicitly via `--root` or the
11 | /// precense of the `.sb` file). A label contains multiple parts, some of which
12 | /// may be elided.
13 | ///
14 | /// A workspace may contain multiple repositories. The canonical name of the
15 | /// repository is identified by the `@@` sigil while the apparent name uses the
16 | /// `@` sigil. A canonical repository name is a unique name within the context
17 | /// of the workspace. The main repository uses the empty string as the canonical
18 | /// repository name. The apparent repository name is used to identify the
19 | /// repository in the context of a specific repository within the workspace. The
20 | /// repository name must be limited to the alphanumeric and the `+`, `-`, or `_`
21 | /// symbols.
22 | ///
23 | /// The label references the root of the repository using the `//` sigil
24 | /// (inspired by the POSIX alternate root specification). When the label is
25 | /// referencing the same repository, the repository name may be elided. The
26 | /// component following the `//` sigil is the relative path within the
27 | /// repository. An absolute path may be used in place of the root relative path.
28 | ///
29 | /// A `:` separates the repository path from the label identifier. The label may
30 | /// be elided, in which case it inherits the name of the directory. The label is
31 | /// required to unique within the directory. The label name is limited to the
32 | /// alphanumeric characters, `+`, `-`, and `_` symbols.
33 | ///
34 | /// A subsequent label is encoded in `(` and `)` and is used to identify a
35 | /// toolchain which is used to build the target.
36 | ///
37 | /// Examples:
38 | /// - `:antimony`
39 | /// - `//antimony`
40 | /// - `//antimony:antimony`
41 | /// - `//antimony:antimony(//build/toolchain/windows:swift)`
42 | /// - `S:\SourceCache\compnerd\antimony\antimony:antimony(S:\SourceCache\compnerd\build\toolchain\windows:swift)`
43 | /// - `@antimony//antimony:antimony(@antimony//build/toolchain/windows:swift)`
44 | /// - `@@antimony//antimony:antimony(@@antimony//build/toolchain/windows:swift)`
45 | ///
46 | public struct Label {
47 | public let directory: URL
48 | public let name: String
49 |
50 | public init() {
51 | self.directory = URL(fileURLWithPath: "")
52 | self.name = ""
53 | }
54 |
55 | public init(_ directory: URL, _ name: String) {
56 | self.directory = directory
57 | self.name = name
58 | }
59 |
60 | public init(resolving label: String, in directory: URL) throws {
61 | guard let components = label.firstIndex(of: ":").map({
62 | (label[..<$0], label[label.index($0, offsetBy: 1)...])
63 | }) else {
64 | throw AntimonyError()
65 | }
66 | self.directory = URL(fileURLWithPath: String(components.0))
67 | self.name = String(components.1)
68 | }
69 |
70 | public var isNull: Bool {
71 | directory.path.isEmpty
72 | }
73 | }
74 |
75 | extension Label: Comparable {
76 | public static func < (_ lhs: Self, _ rhs: Self) -> Bool {
77 | if lhs.directory == rhs.directory {
78 | return lhs.name < rhs.name
79 | }
80 | return lhs.directory.path < rhs.directory.path
81 | }
82 | }
83 |
84 | extension Label: CustomStringConvertible {
85 | public var description: String {
86 | if isNull { return "" }
87 | if directory.relativePath == "." { return ":\(name)" }
88 | return "//\(directory.relativePath):\(name)"
89 | }
90 | }
91 |
92 | extension Label: Equatable {
93 | public static func == (_ lhs: Self, _ rhs: Self) -> Bool {
94 | lhs.directory == rhs.directory && lhs.name == rhs.name
95 | }
96 | }
97 |
98 | extension Label: Hashable {
99 | public func hash(into hasher: Hasher) {
100 | // TODO(compnerd) implement hashing
101 | }
102 | }
103 |
--------------------------------------------------------------------------------
/Sources/antimony/Configuration/Loader.swift:
--------------------------------------------------------------------------------
1 | // Copyright © 2024 Saleem Abdulrasool
2 | // SPDX-License-Identifier: BSD-3-Clause
3 |
4 | import class Foundation.JSONDecoder
5 | import struct Foundation.Data
6 | import struct Foundation.URL
7 |
8 | public enum Operation {
9 | case format
10 | case generate
11 | }
12 |
13 | public protocol Loader: AnyObject {
14 | init(_ delegate: BuildConfigurationDelegate)
15 | func load(_ file: URL, root url: URL) throws
16 | }
17 |
18 | /// Load a placeholder representation of the build configuration.
19 | ///
20 | /// The placeholder representation is a JSON representation of the build
21 | /// to ease the parsing. It is of the following form:
22 | /// ```
23 | /// {
24 | /// "executables" : {
25 | /// "sb": {
26 | /// "directory": "Sources/Tools/sb",
27 | /// "sources": ["sb.swift"],
28 | /// "private_dependencies": [
29 | /// "Sources/antimony:antimony",
30 | /// ".build/checkouts/swift-argument-parser/Sources/ArgumentParser:ArgumentParser",
31 | /// ],
32 | /// "swiftflags": ["-parse-as-library"]
33 | /// }
34 | /// },
35 | /// "static_libraries": { ... },
36 | /// "dynamic_libraries" : { ... }
37 | /// }
38 | /// ```
39 | public class LegacyLoader_DO_NOT_USE_OR_YOU_WILL_BE_FIRED: Loader {
40 | struct BUILD: Codable {
41 | struct Target: Codable {
42 | enum CodingKeys: String, CodingKey {
43 | case directory, sources, defines, libs
44 | case includes = "include_dirs"
45 | case privateDependencies = "private_dependencies"
46 | case publicDependencies = "public_dependencies"
47 | case swiftFlags = "swiftflags"
48 | }
49 |
50 | let directory: String
51 | let sources: [String]
52 | let defines: [String]?
53 | let libs: [String]?
54 | let includes: [String]?
55 | let privateDependencies: [String]?
56 | let publicDependencies: [String]?
57 | let swiftFlags: [String]?
58 | }
59 |
60 | enum CodingKeys: String, CodingKey {
61 | case executable = "executables"
62 | case `static` = "static_libraries"
63 | case dynamic = "dynamic_libraries"
64 | }
65 |
66 | let executable: [String:Target]?
67 | let `static`: [String:Target]?
68 | let dynamic: [String:Target]?
69 | }
70 |
71 | public weak var delegate: BuildConfigurationDelegate?
72 |
73 | public required init(_ delegate: BuildConfigurationDelegate) {
74 | self.delegate = delegate
75 | }
76 |
77 | public func load(_ file: URL, root: URL) throws {
78 | let data = try Data(contentsOf: file, options: [.uncached])
79 |
80 | // Deserialise a JSON representation; we currently cannot parse the
81 | // starlark-esque build definition, so use a JSON serialisation as
82 | // a placeholder.
83 | //
84 | // We expect the following JSON structure:
85 | // ```
86 | // { "executables" : { }, "static_libraries": { }, "dynamic_libraries" : { } }
87 | // ```
88 | let build = try JSONDecoder().decode(BUILD.self, from: data)
89 |
90 | let elements: [(KeyPath, Target.OutputType)] = [
91 | (\.executable, .executable),
92 | (\.static, .static),
93 | (\.dynamic, .dynamic),
94 | ]
95 |
96 | for element in elements {
97 | guard let targets = build[keyPath: element.0] else { continue }
98 | for target in targets {
99 | let srcdir = URL(fileURLWithPath: target.value.directory,
100 | relativeTo: file.deletingLastPathComponent())
101 | let label = Label(srcdir, target.key)
102 | let sources = Set(target.value.sources.compactMap { SourceFile(URL(fileURLWithPath: $0, relativeTo: srcdir)) })
103 |
104 | var dependencies: Target.Dependencies = ([], [], [], [])
105 | dependencies.private = target.value.privateDependencies?.compactMap { try? Label(resolving: $0, in: root) } ?? []
106 | dependencies.public = target.value.publicDependencies?.compactMap { try? Label(resolving: $0, in: root) } ?? []
107 |
108 | let target = {
109 | $0.defines = target.value.defines ?? []
110 | $0.dependencies = dependencies
111 | $0.includes = target.value.includes ?? []
112 | $0.libs = target.value.libs ?? []
113 | $0.flags.swift = target.value.swiftFlags ?? []
114 | return $0
115 | }(Target(label, sources, type: element.1))
116 |
117 | self.delegate?.resolved(label: label, to: target)
118 | }
119 | }
120 | }
121 | }
122 |
--------------------------------------------------------------------------------
/Sources/antimony/Configuration/Target.swift:
--------------------------------------------------------------------------------
1 | // Copyright © 2024 Saleem Abdulrasool
2 | // SPDX-License-Identifier: BSD-3-Clause
3 |
4 | extension Target {
5 | public enum OutputType {
6 | case unknown
7 | case group // group
8 | case executable // executable
9 | case dynamic // shared_library
10 | case module // loadable_module
11 | case `static` // static_library
12 | case sources // source_set
13 | case resource // copy
14 | case action // action
15 | case iteration // action_foreach
16 | case generator // generated_file
17 | }
18 | }
19 |
20 | extension Target.OutputType: CustomStringConvertible {
21 | public var description: String {
22 | switch self {
23 | case .unknown: "unknown"
24 | case .group: "group"
25 | case .executable: "executable"
26 | case .dynamic: "shared_library"
27 | case .module: "loadable_module"
28 | case .static: "static_library"
29 | case .sources: "source_set"
30 | case .resource: "copy"
31 | case .action: "action"
32 | case .iteration: "action_foreach"
33 | case .generator: "generated_file"
34 | }
35 | }
36 | }
37 |
38 | public class Target {
39 | public typealias Dependencies =
40 | (data: [Label], generated: [Label], private: [Label], public: [Label])
41 |
42 | public private(set) var label: Label
43 | public private(set) var sources: Set
44 |
45 | public var type: OutputType
46 | public var defines: [String] = []
47 | public var includes: [String] = []
48 | public var libs: [String] = []
49 | public var flags: (c: [String], swift: [String]) = ([], [])
50 |
51 | public var dependencies: Dependencies = ([], [], [], [])
52 |
53 | public init(_ label: Label, _ sources: Set = [],
54 | type: OutputType = .unknown) {
55 | self.label = label
56 | self.sources = sources
57 | self.type = type
58 | }
59 | }
60 |
61 | extension Target {
62 | public var isSwiftTarget: Bool {
63 | self.sources.lazy.first(where: { $0.type == .swift }) == nil ? false : true
64 | }
65 |
66 | public var isCTarget: Bool {
67 | self.sources.lazy.first(where: { $0.type == .c }) == nil ? false : true
68 | }
69 | }
70 |
71 | extension Target: CustomStringConvertible {
72 | public var description: String {
73 | let deps = dependencies.public.isEmpty ? "" : """
74 | deps = [
75 | \(dependencies.public.map { #""\#($0)""# }.joined(separator: ",\n ")),
76 | ]
77 | """
78 | let defines = defines.isEmpty ? "" : """
79 | defines = [
80 | \(defines.map { #""\#($0)"# }.joined(separator: ",\n ")),
81 | ]
82 | """
83 | let includes = includes.isEmpty ? "" : """
84 | include_dirs = [
85 | \(includes.map { #""\#($0)""# }.joined(separator: ",\n ")),
86 | ]
87 | """
88 | let sources = sources.isEmpty ? "" : """
89 | sources = [
90 | \(sources.map(\.path).map { #""\#($0)""# }.joined(separator: ",\n ")),
91 | ]
92 | """
93 |
94 | return """
95 | \(type)("\(label.name)") {
96 | \([sources, defines, includes, deps].filter { !$0.isEmpty }.joined(separator: "\n"))
97 | }
98 | """
99 | }
100 | }
101 |
--------------------------------------------------------------------------------
/Sources/antimony/Error.swift:
--------------------------------------------------------------------------------
1 | // Copyright © 2024 Saleem Abdulrasool
2 | // SPDX-License-Identifier: BSD-3-Clause
3 |
4 | public struct AntimonyError: Error {
5 | }
6 |
--------------------------------------------------------------------------------
/Sources/antimony/Extensions/SwiftDriver+Extensions.swift:
--------------------------------------------------------------------------------
1 | // Copyright © 2024 Saleem Abdulrasool
2 | // SPDX-License-Identifier: BSD-3-Clause
3 |
4 | import SwiftDriver
5 | import TSCBasic
6 |
7 | extension Array where Element == TypedVirtualPath {
8 | internal func paths(objdir: RelativePath, excluding: Set = []) -> [String] {
9 | self.map {
10 | if excluding.contains($0.file.description) { return $0.file.description }
11 | return objdir.appending(component: $0.file.description).pathString
12 | }
13 | }
14 |
15 | internal func paths() -> [String] {
16 | self.map(\.file.description)
17 | }
18 | }
19 |
20 | extension Job {
21 | internal var name: String {
22 | switch kind {
23 | case .compile:
24 | guard let input = displayInputs.first?.file.basename else {
25 | return "COMPILE_\(moduleName)"
26 | }
27 | return "COMPILE_\(moduleName)_\(input)"
28 |
29 | case .link:
30 | return "LINK_\(moduleName)"
31 |
32 | case .emitModule:
33 | return "EMIT_MODULE_\(moduleName)"
34 |
35 | case .mergeModule:
36 | return "MERGE_MODULE_\(moduleName)"
37 |
38 | default: fatalError("do not know how to handle job type '\(kind)'")
39 | }
40 | }
41 | }
42 |
--------------------------------------------------------------------------------
/Sources/antimony/JobEmitter.swift:
--------------------------------------------------------------------------------
1 | // Copyright © 2024 Saleem Abdulrasool
2 | // SPDX-License-Identifier: BSD-3-Clause
3 |
4 | import struct Foundation.URL
5 | import SwiftDriver
6 | import TSCBasic
7 |
8 | extension Array where Element == String {
9 | fileprivate func quoted() -> Self {
10 | map {
11 | if $0.firstIndex(of: " ") == nil { return $0 }
12 | return #""\#($0)""#
13 | }
14 | }
15 | }
16 |
17 | public struct JobEmitter {
18 | public var writer: NinjaWriter
19 | public let configuration: BuildConfiguration
20 |
21 | public init(for configuration: BuildConfiguration, into ninja: NinjaWriter) {
22 | self.writer = ninja
23 | self.configuration = configuration
24 | }
25 |
26 | public func emitJobs(for target: Target, flags: [String] = [], at out: URL) throws {
27 | let objdir = try RelativePath(validating: "\(target.label.name).dir")
28 | let bindir = try RelativePath(validating: "bin")
29 | let libdir = try RelativePath(validating: "lib")
30 |
31 | var driver: Driver
32 |
33 | // NOTE: the absolute diretory is required to support emission of file lists
34 | let resolver: ArgsResolver =
35 | try ArgsResolver(fileSystem: localFileSystem,
36 | temporaryDirectory: .absolute(AbsolutePath(validating: objdir.pathString,
37 | relativeTo: AbsolutePath(validating: out.path))))
38 | let executor: DriverExecutor = NULLExecutor(resolver: resolver)
39 |
40 | #if os(Windows)
41 | #if arch(arm64)
42 | let triple: String = "aarch64-unknown-windows-msvc"
43 | #elseif arch(x86_64)
44 | let triple: String = "x86_64-unknown-windows-msvc"
45 | #endif
46 | #elseif os(Linux)
47 | #if arch(arm64)
48 | let triple: String = "aarch64-unknown-linux-gnu"
49 | #elseif arch(x86_64)
50 | let triple: String = "x86_64-unknown-linux-gnu"
51 | #endif
52 | #endif
53 |
54 | let module: RelativePath =
55 | objdir.appending(components: "swift",
56 | "\(target.label.name).swiftmodule",
57 | "\(triple).swiftmodule")
58 | let output: RelativePath
59 |
60 | let dependencies: [Target] = target.dependencies.public.compactMap(configuration.lookup) + target.dependencies.private.compactMap(configuration.lookup)
61 | let headerIncludes = target.dependencies.private.compactMap(configuration.lookup).filter(\.isCTarget).map {
62 | let source = $0.label.directory
63 | return $0.includes.compactMap { try? AbsolutePath(validating: source.appendingPathComponent($0).path) }
64 | }.flatMap { $0 }
65 | let swiftIncludes = dependencies.filter(\.isSwiftTarget).compactMap { try? RelativePath(validating: "\($0.label.name).dir/swift") }
66 | let libs = dependencies.filter(\.isSwiftTarget).compactMap { libdir.appending(component: "\($0.label.name).lib").pathString }
67 |
68 | let arguments = [
69 | target.defines.map { "-D\($0)" },
70 | swiftIncludes.map(\.pathString).map { "-I\($0)" },
71 | headerIncludes.map(\.pathString).map { "-I\($0)" },
72 | target.sources.map(\.path),
73 | target.libs.map { "-l\($0)" },
74 | libs,
75 | flags,
76 | target.isSwiftTarget ? target.flags.swift : []
77 | ].flatMap { $0 }
78 |
79 | switch target.type {
80 | case .executable:
81 | output = bindir.appending(component: "\(target.label.name).exe")
82 | driver = try Driver(args: [
83 | "swiftc.exe",
84 | "-emit-dependencies",
85 | "-emit-executable",
86 | "-module-name",
87 | target.label.name,
88 | "-o", output.pathString,
89 | ] + arguments, executor: executor)
90 |
91 | case .static:
92 | output = libdir.appending(component: "\(target.label.name).lib")
93 | driver = try Driver(args: [
94 | "swiftc.exe",
95 | "-parse-as-library",
96 | "-static",
97 | "-emit-dependencies",
98 | "-emit-library",
99 | "-emit-module",
100 | "-emit-module-path",
101 | module.pathString,
102 | "-module-name",
103 | target.label.name,
104 | "-o", output.pathString,
105 | ] + arguments, executor: executor)
106 |
107 | case .dynamic:
108 | output = bindir.appending(component: "\(target.label.name).dll")
109 | driver = try Driver(args: [
110 | "swiftc.exe",
111 | "-parse-as-library",
112 | "-emit-dependencies",
113 | "-emit-library",
114 | "-emit-module",
115 | "-emit-module-path",
116 | module.pathString,
117 | "-module-name",
118 | target.label.name,
119 | "-o", output.pathString,
120 | ] + arguments, executor: executor)
121 |
122 | default:
123 | fatalError("do not know how to build target type '\(target.type)'")
124 | }
125 |
126 | let jobs = try driver.planBuild()
127 | let deps: [String] =
128 | dependencies.filter(\.isSwiftTarget)
129 | .compactMap { "\($0.label.name).dir/swift/\($0.label.name).swiftmodule/\(triple).swiftmodule" }
130 |
131 | for job in jobs {
132 | let command: String =
133 | try resolver.resolveArgumentList(for: job)
134 | .quoted()
135 | .joined(separator: " ")
136 |
137 | switch job.kind {
138 | case .compile:
139 | writer.rule(job.name, command: command,
140 | description: job.description, restat: true)
141 | writer.build(job.outputs.paths(objdir: objdir), rule: job.name,
142 | inputs: job.inputs.paths(objdir: objdir),
143 | implicitInputs: deps)
144 |
145 | case .emitModule:
146 | writer.rule(job.name, command: command,
147 | description: job.description, restat: true)
148 | writer.build(job.outputs.paths(), rule: job.name,
149 | inputs: job.inputs.paths(objdir: objdir),
150 | implicitInputs: deps)
151 |
152 | case .link:
153 | writer.rule(job.name, command: command, description: job.description)
154 | writer.build(job.outputs.paths(), rule: job.name,
155 | inputs: job.inputs.paths(objdir: objdir,
156 | excluding: Set(libs)))
157 |
158 | case .mergeModule:
159 | writer.rule(job.name, command: command, description: job.description)
160 | writer.build(job.outputs.paths(), rule: job.name,
161 | inputs: job.inputs.paths(objdir: objdir))
162 |
163 | default: fatalError("do not know how to handle job type '\(job.kind)'")
164 | }
165 | }
166 |
167 | writer.phony(target.label.name,
168 | outputs: jobs.filter { [.link, .emitModule].contains($0.kind) }
169 | .map(\.outputs)
170 | .flatMap { $0 }
171 | .filter {
172 | return switch $0.type {
173 | case .emitModuleDependencies: false
174 | default: true
175 | }
176 | }
177 | .paths())
178 | }
179 | }
180 |
--------------------------------------------------------------------------------
/Sources/antimony/NULLExecutor.swift:
--------------------------------------------------------------------------------
1 | // Copyright © 2024 Saleem Abdulrasool
2 | // SPDX-License-Identifier: BSD-3-Clause
3 |
4 | import SwiftDriver
5 | import TSCBasic
6 |
7 | struct NULLExecutor: DriverExecutor {
8 | let resolver: ArgsResolver
9 |
10 | func execute(job: Job, forceResponseFiles: Bool,
11 | recordedInputModificationDates: [TypedVirtualPath:TimePoint])
12 | throws -> ProcessResult {
13 | ProcessResult(arguments: [], environmentBlock: [:],
14 | exitStatus: .terminated(code: 0), output: .success([]),
15 | stderrOutput: .success([]))
16 | }
17 |
18 | func execute(workload: DriverExecutorWorkload, delegate: JobExecutionDelegate,
19 | numParallelJobs: Int, forceResponseFiles: Bool,
20 | recordedInputModificationDates: [TypedVirtualPath:TimePoint])
21 | throws {
22 | fatalError()
23 | }
24 |
25 | func checkNonZeroExit(args: String..., environment: [String:String])
26 | throws -> String {
27 | try Process.checkNonZeroExit(arguments: args, environmentBlock: .init(environment))
28 | }
29 |
30 | func description(of job: Job, forceResponseFiles: Bool) throws -> String {
31 | String(describing: job)
32 | }
33 | }
34 |
--------------------------------------------------------------------------------
/Sources/antimony/NinjaWriter.swift:
--------------------------------------------------------------------------------
1 | // Copyright © 2024 Saleem Abdulrasool
2 | // SPDX-License-Identifier: BSD-3-Clause
3 |
4 | import struct Foundation.Data
5 | import struct Foundation.URL
6 |
7 | extension String {
8 | fileprivate func escapedPath() -> String {
9 | self.replacing("$ ", with: "$$ ").replacing(" ", with: "$ ").replacing(":", with: "$:")
10 | }
11 |
12 | fileprivate func escapedRuleName() -> String {
13 | self.replacing("+", with: "_").replacing(" ", with: "_")
14 | }
15 | }
16 |
17 | extension String.SubSequence {
18 | /// Returns the number of `$` characters preceeding the character at index `end`.
19 | fileprivate func escapes(before end: Index) -> Int {
20 | guard let start = self[.. self.width {
45 | // Attempt to wrap the text if possible.
46 | let width = self.width - indentation.count - " $".count
47 | var index: String.Index? = text.index(text.startIndex, offsetBy: width)
48 |
49 | // Find the rightmost unescaped space satisfying our width constraint.
50 | while index == nil ? false : text.escapes(before: index!) % 2 != 0 {
51 | index = text[..
2 | // SPDX-License-Identifier: BSD-3-Clause
3 |
4 | import struct Foundation.URL
5 |
6 | extension SourceFile {
7 | public enum SourceType {
8 | case unknown
9 | case assembly
10 | case c
11 | case cplusplus
12 | case modulemap
13 | case swift
14 | case swiftmodule
15 | }
16 | }
17 |
18 | extension SourceFile.SourceType: CaseIterable {}
19 |
20 | public class SourceFile {
21 | let url: URL
22 |
23 | public init(_ url: URL) {
24 | precondition(url.isFileURL)
25 | self.url = url
26 | }
27 |
28 | public var path: String {
29 | self.url.withUnsafeFileSystemRepresentation { String(cString: $0!) }
30 | }
31 |
32 | public var type: SourceType {
33 | return switch url.pathExtension {
34 | case "s", "S": .assembly
35 | case "c": .c
36 | case "cc", "cxx", "cpp": .cplusplus
37 | case "modulemap": .modulemap
38 | case "swift": .swift
39 | case "swiftmodule": .swiftmodule
40 | default: .unknown
41 | }
42 | }
43 | }
44 |
45 | extension SourceFile: Equatable {
46 | public static func == (_ lhs: SourceFile, _ rhs: SourceFile) -> Bool {
47 | lhs.url == rhs.url
48 | }
49 | }
50 |
51 | extension SourceFile: Hashable {
52 | public func hash(into hasher: inout Hasher) {
53 | url.hash(into: &hasher)
54 | }
55 | }
56 |
--------------------------------------------------------------------------------
/Sources/antimony/Utils/FileURL.swift:
--------------------------------------------------------------------------------
1 | // Copyright © 2024 Saleem Abdulrasool
2 | // SPDX-License-Identifier: BSD-3-Clause
3 |
4 | import ArgumentParser
5 | import struct Foundation.URL
6 |
7 | internal struct FileURL: ExpressibleByArgument {
8 | public let url: URL
9 |
10 | public init?(argument: String) {
11 | self.url = URL(fileURLWithPath: argument)
12 | }
13 | }
14 |
--------------------------------------------------------------------------------
/Tools/sb/sb.swift:
--------------------------------------------------------------------------------
1 | // Copyright © 2024 Saleem Abdulrasool
2 | // SPDX-License-Identifier: BSD-3-Clause
3 |
4 | import antimony
5 |
6 | import ArgumentParser
7 | import struct Foundation.URL
8 |
9 | private struct Format: AsyncParsableCommand {
10 | static var configuration: CommandConfiguration {
11 | CommandConfiguration(commandName: "format", abstract: "Format .sb files")
12 | }
13 |
14 | @OptionGroup
15 | var options: BuildConfigurationOptions
16 |
17 | mutating func run() async throws {
18 | let configuration = BuildConfiguration()
19 | try await configuration.load(options, for: .format)
20 | }
21 | }
22 |
23 | private struct Generate: AsyncParsableCommand {
24 | static var configuration: CommandConfiguration {
25 | CommandConfiguration(commandName: "gen", abstract: "Generate Ninja files")
26 | }
27 |
28 | @OptionGroup
29 | var options: BuildConfigurationOptions
30 |
31 | mutating func run() async throws {
32 | let configuration = BuildConfiguration()
33 | try await configuration.load(options, for: .generate)
34 | }
35 | }
36 |
37 | @main
38 | private struct SB: AsyncParsableCommand {
39 | static var configuration: CommandConfiguration {
40 | CommandConfiguration(subcommands: [Format.self, Generate.self])
41 | }
42 |
43 | @Flag
44 | var version: Bool = false
45 |
46 | private func DoVersion() {
47 | }
48 |
49 | mutating func run() throws {
50 | if version { return DoVersion() }
51 | }
52 | }
53 |
--------------------------------------------------------------------------------