├── .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 | --------------------------------------------------------------------------------