mirror of
https://github.com/apple/swift.git
synced 2025-12-14 20:36:38 +01:00
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`).
327 lines
7.9 KiB
Swift
327 lines
7.9 KiB
Swift
//===--- 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)
|
|
}
|
|
}
|