Skip to content

Commit df32362

Browse files
authored
Merge pull request #81350 from hamishknight/cmd-prefix
[xcodegen] A couple of command-line handling tweaks
2 parents a95bb31 + 8503ce0 commit df32362

File tree

8 files changed

+99
-102
lines changed

8 files changed

+99
-102
lines changed

utils/swift-xcodegen/Sources/SwiftXcodeGen/BuildArgs/ClangBuildArgsProvider.swift

Lines changed: 13 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -50,25 +50,32 @@ struct ClangBuildArgsProvider {
5050
commandsToAdd[relFilePath] = (output, command.command.args)
5151
}
5252
for (path, (output, commandArgs)) in commandsToAdd {
53-
// Only include arguments that have known flags.
54-
args.insert(commandArgs.filter({ $0.flag != nil }), for: path)
53+
let commandArgs = commandArgs.filter { arg in
54+
// Only include arguments that have known flags.
55+
// Ignore `-fdiagnostics-color`, we don't want ANSI escape sequences
56+
// in Xcode build logs.
57+
guard let flag = arg.flag, flag != .fDiagnosticsColor else {
58+
return false
59+
}
60+
return true
61+
}
62+
args.insert(commandArgs, for: path)
5563
outputs[path] = output
5664
}
5765
}
5866

5967
/// Retrieve the arguments at a given path, including those in the parent.
6068
func getArgs(for path: RelativePath) -> BuildArgs {
61-
// Sort the arguments to get a deterministic ordering.
6269
// FIXME: We ought to get the command from the arg tree.
63-
.init(for: .clang, args: args.getArgs(for: path).sorted())
70+
.init(for: .clang, args: args.getArgs(for: path))
6471
}
6572

6673
/// Retrieve the arguments at a given path, excluding those already covered
6774
/// by a parent.
6875
func getUniqueArgs(
6976
for path: RelativePath, parent: RelativePath, infer: Bool = false
7077
) -> BuildArgs {
71-
var fileArgs: Set<Command.Argument> = []
78+
var fileArgs: [Command.Argument] = []
7279
if hasBuildArgs(for: path) {
7380
fileArgs = args.getUniqueArgs(for: path, parent: parent)
7481
} else if infer {
@@ -78,14 +85,8 @@ struct ClangBuildArgsProvider {
7885
fileArgs = args.getUniqueArgs(for: component, parent: parent)
7986
}
8087
}
81-
// Sort the arguments to get a deterministic ordering.
8288
// FIXME: We ought to get the command from the arg tree.
83-
return .init(for: .clang, args: fileArgs.sorted())
84-
}
85-
86-
/// Whether the given path has any unique args not covered by `parent`.
87-
func hasUniqueArgs(for path: RelativePath, parent: RelativePath) -> Bool {
88-
args.hasUniqueArgs(for: path, parent: parent)
89+
return .init(for: .clang, args: fileArgs)
8990
}
9091

9192
/// Whether the given file has build arguments.

utils/swift-xcodegen/Sources/SwiftXcodeGen/BuildArgs/CommandArgTree.swift

Lines changed: 14 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -11,49 +11,43 @@
1111
//===----------------------------------------------------------------------===//
1212

1313
/// A tree of compile command arguments, indexed by path such that those unique
14-
/// to a particular file can be queried, with common arguments associated
15-
/// with a common parent.
14+
/// to a particular file can be queried, with common-prefixed arguments
15+
/// associated with a common parent.
1616
struct CommandArgTree {
17-
private var storage: [RelativePath: Set<Command.Argument>]
17+
private var storage: [RelativePath: [Command.Argument]]
1818

1919
init() {
2020
self.storage = [:]
2121
}
2222

2323
mutating func insert(_ args: [Command.Argument], for path: RelativePath) {
24-
let args = Set(args)
2524
for component in path.stackedComponents {
2625
// If we haven't added any arguments, add them. If we're adding arguments
2726
// for the file itself, this is the only way we'll add arguments,
28-
// otherwise we can form an intersection with the other arguments.
27+
// otherwise we can form a common prefix with the other arguments.
2928
let inserted = storage.insertValue(args, for: component)
3029
guard !inserted && component != path else { continue }
3130

32-
// We use subscript(_:default:) to mutate in-place without CoW.
33-
storage[component, default: []].formIntersection(args)
31+
// We use withValue(for:default:) to mutate in-place without CoW.
32+
storage.withValue(for: component, default: []) { existingArgs in
33+
let slice = existingArgs.commonPrefix(with: args)
34+
existingArgs.removeSubrange(slice.endIndex...)
35+
}
3436
}
3537
}
3638

3739
/// Retrieve the arguments at a given path, including those in the parent.
38-
func getArgs(for path: RelativePath) -> Set<Command.Argument> {
40+
func getArgs(for path: RelativePath) -> [Command.Argument] {
3941
storage[path] ?? []
4042
}
4143

4244
/// Retrieve the arguments at a given path, excluding those already covered
4345
/// by a given parent.
4446
func getUniqueArgs(
4547
for path: RelativePath, parent: RelativePath
46-
) -> Set<Command.Argument> {
47-
getArgs(for: path).subtracting(getArgs(for: parent))
48-
}
49-
50-
/// Whether the given path has any unique args not covered by `parent`.
51-
func hasUniqueArgs(for path: RelativePath, parent: RelativePath) -> Bool {
52-
let args = getArgs(for: path)
53-
guard !args.isEmpty else { return false }
54-
// Assuming `parent` is an ancestor of path, the arguments for parent is
55-
// guaranteed to be a subset of the arguments for `path`. As such, we
56-
// only have to compare sizes here.
57-
return args.count != getArgs(for: parent).count
48+
) -> [Command.Argument] {
49+
let childArgs = getArgs(for: path)
50+
let parentArgs = getArgs(for: parent)
51+
return Array(childArgs[parentArgs.count...])
5852
}
5953
}

utils/swift-xcodegen/Sources/SwiftXcodeGen/BuildArgs/SwiftTargets.swift

Lines changed: 2 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,6 @@ struct SwiftTargets {
1717
private var dependenciesByTargetName: [String: Set<String>] = [:]
1818
private var targetsByName: [String: SwiftTarget] = [:]
1919
private var targetsByOutput: [String: SwiftTarget] = [:]
20-
private var addedFiles: Set<RelativePath> = []
2120

2221
// Track some state for debugging
2322
private var debugLogUnknownFlags: Set<String> = []
@@ -204,22 +203,10 @@ struct SwiftTargets {
204203
var buildRule: SwiftTarget.BuildRule?
205204
var emitModuleRule: SwiftTarget.EmitModuleRule?
206205
if forBuild && !repoSources.isEmpty {
207-
// Bail if we've already recorded a target with one of these inputs.
208-
// TODO: Attempt to merge?
209-
// TODO: Should we be doing this later?
210-
for input in repoSources {
211-
guard addedFiles.insert(input).inserted else {
212-
log.debug("""
213-
! Skipping '\(name)' with output '\(primaryOutput)'; \
214-
contains input '\(input)' already added
215-
""")
216-
return
217-
}
218-
}
219206
// We've already ensured that `repoSources` is non-empty.
220-
let parent = repoSources.commonAncestor!
221207
buildRule = .init(
222-
parentPath: parent, sources: sources, buildArgs: buildArgs
208+
parentPath: repoSources.commonAncestor!, sources: sources,
209+
buildArgs: buildArgs
223210
)
224211
}
225212
if forModule {

utils/swift-xcodegen/Sources/SwiftXcodeGen/Command/Command.swift

Lines changed: 0 additions & 44 deletions
Original file line numberDiff line numberDiff line change
@@ -240,47 +240,3 @@ extension Command.Flag: CustomStringConvertible {
240240
extension Command.Option: CustomStringConvertible {
241241
var description: String { printed }
242242
}
243-
244-
// MARK: Comparable
245-
// We sort the resulting command-line arguments to ensure deterministic
246-
// ordering.
247-
248-
extension Command.Flag: Comparable {
249-
static func < (lhs: Self, rhs: Self) -> Bool {
250-
guard lhs.dash == rhs.dash else {
251-
return lhs.dash < rhs.dash
252-
}
253-
return lhs.name.rawValue < rhs.name.rawValue
254-
}
255-
}
256-
257-
extension Command.Option: Comparable {
258-
static func < (lhs: Self, rhs: Self) -> Bool {
259-
guard lhs.flag == rhs.flag else {
260-
return lhs.flag < rhs.flag
261-
}
262-
guard lhs.spacing == rhs.spacing else {
263-
return lhs.spacing < rhs.spacing
264-
}
265-
return lhs.value < rhs.value
266-
}
267-
}
268-
269-
extension Command.Argument: Comparable {
270-
static func < (lhs: Self, rhs: Self) -> Bool {
271-
switch (lhs, rhs) {
272-
// Sort flags < options < values
273-
case (.flag, .option): true
274-
case (.flag, .value): true
275-
case (.option, .value): true
276-
277-
case (.option, .flag): false
278-
case (.value, .flag): false
279-
case (.value, .option): false
280-
281-
case (.flag(let lhs), .flag(let rhs)): lhs < rhs
282-
case (.option(let lhs), .option(let rhs)): lhs < rhs
283-
case (.value(let lhs), .value(let rhs)): lhs < rhs
284-
}
285-
}
286-
}

utils/swift-xcodegen/Sources/SwiftXcodeGen/Command/CommandParser.swift

Lines changed: 30 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@
1313
struct CommandParser {
1414
private var input: ByteScanner
1515
private var knownCommand: KnownCommand?
16+
private var stopAtShellOperator = false
1617

1718
private init(_ input: UnsafeBufferPointer<UInt8>) {
1819
self.input = ByteScanner(input)
@@ -26,6 +27,8 @@ struct CommandParser {
2627
}
2728
}
2829

30+
/// Parse an arbitrary shell command, returning the first single invocation
31+
/// of a known command.
2932
static func parseKnownCommandOnly(_ input: String) throws -> Command? {
3033
var input = input
3134
return try input.withUTF8 { bytes in
@@ -35,6 +38,9 @@ struct CommandParser {
3538
) else {
3639
return nil
3740
}
41+
// We're parsing an arbitrary shell command so stop if we hit a shell
42+
// operator like '&&'
43+
parser.stopAtShellOperator = true
3844
return Command(executable: executable, args: try parser.consumeArguments())
3945
}
4046
}
@@ -62,7 +68,7 @@ struct CommandParser {
6268
) throws -> AnyPath? {
6369
var executable: AnyPath
6470
repeat {
65-
guard let executableUTF8 = try input.consumeElement() else {
71+
guard let executableUTF8 = try consumeElement() else {
6672
return nil
6773
}
6874
executable = AnyPath(String(utf8: executableUTF8))
@@ -119,17 +125,27 @@ fileprivate extension ByteScanner.Consumer {
119125
}
120126
}
121127

122-
fileprivate extension ByteScanner {
123-
mutating func consumeElement() throws -> Bytes? {
128+
extension CommandParser {
129+
mutating func consumeElement() throws -> ByteScanner.Bytes? {
124130
// Eat any leading whitespace.
125-
skip(while: \.isSpaceOrTab)
131+
input.skip(while: \.isSpaceOrTab)
126132

127133
// If we're now at the end of the input, nothing can be parsed.
128-
guard hasInput else { return nil }
129-
130-
// Consume the element, stopping at the first space.
131-
return try consume(using: { consumer in
132-
switch consumer.peek {
134+
guard input.hasInput else { return nil }
135+
136+
// Consume the element, stopping at the first space or shell operator.
137+
let start = input.cursor
138+
let elt = try input.consume(using: { consumer in
139+
guard let char = consumer.peek else { return false }
140+
if stopAtShellOperator {
141+
switch char {
142+
case "<", ">", "(", ")", "|", "&", ";":
143+
return false
144+
default:
145+
break
146+
}
147+
}
148+
switch char {
133149
case \.isSpaceOrTab:
134150
return false
135151
case "\"":
@@ -139,6 +155,9 @@ fileprivate extension ByteScanner {
139155
return consumer.consumeUnescaped()
140156
}
141157
})
158+
// Note that we may have an empty element while still moving the cursor
159+
// for e.g '-I ""', which is an option with an empty value.
160+
return start != input.cursor ? elt : nil
142161
}
143162
}
144163

@@ -167,7 +186,7 @@ extension CommandParser {
167186
return makeOption(spacing: .unspaced, String(utf8: option.remaining))
168187
}
169188
if spacing.contains(.spaced), !option.hasInput,
170-
let value = try input.consumeElement() {
189+
let value = try consumeElement() {
171190
return makeOption(spacing: .spaced, String(utf8: value))
172191
}
173192
return option.empty ? .flag(flag) : nil
@@ -188,7 +207,7 @@ extension CommandParser {
188207
}
189208

190209
mutating func consumeArgument() throws -> Command.Argument? {
191-
guard let element = try input.consumeElement() else { return nil }
210+
guard let element = try consumeElement() else { return nil }
192211
return try element.withUnsafeBytes { bytes in
193212
var option = ByteScanner(bytes)
194213
var numDashes = 0

utils/swift-xcodegen/Sources/SwiftXcodeGen/Command/KnownCommand.swift

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -128,6 +128,7 @@ extension Command.Flag {
128128
static let isystem = dash("isystem")
129129
static let isysroot = dash("isysroot")
130130
static let f = dash("f")
131+
static let fDiagnosticsColor = dash("fdiagnostics-color")
131132
static let U = dash("U")
132133
static let W = dash("W")
133134
static let std = dash("std")
@@ -211,6 +212,8 @@ extension KnownCommand {
211212
// FIXME: We ought to see if we can get away with preserving unknown flags.
212213
.init(.f, option: .unspaced),
213214

215+
.init(.fDiagnosticsColor),
216+
214217
// FIXME: Really we ought to map to Xcode's SDK
215218
.init(.isystem, option: .unspaced, .spaced),
216219
.init(.isysroot, option: .unspaced, .spaced),

utils/swift-xcodegen/Sources/SwiftXcodeGen/Utils.swift

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -34,6 +34,18 @@ extension Sequence {
3434
}
3535
}
3636

37+
extension Collection where Element: Equatable {
38+
func commonPrefix(with other: some Collection<Element>) -> SubSequence {
39+
var (i, j) = (self.startIndex, other.startIndex)
40+
while i < self.endIndex, j < other.endIndex {
41+
guard self[i] == other[j] else { break }
42+
self.formIndex(after: &i)
43+
other.formIndex(after: &j)
44+
}
45+
return self[..<i]
46+
}
47+
}
48+
3749
extension String {
3850
init(utf8 buffer: UnsafeRawBufferPointer) {
3951
guard !buffer.isEmpty else {

utils/swift-xcodegen/Tests/SwiftXcodeGenTest/CompileCommandsTests.swift

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -54,6 +54,31 @@ class CompileCommandsTests: XCTestCase {
5454
knownCommandOnly: true
5555
)
5656

57+
for op in ["&&", "||", ">", "<", ">>", ";", "(", ")"] {
58+
assertParse(
59+
"x y x/y/clang -DX -I \(op) ignored",
60+
executable: "x/y/clang",
61+
args: [.option(.D, spacing: .unspaced, value: "X"), .flag(.I)],
62+
knownCommandOnly: true
63+
)
64+
assertParse(
65+
"x y x/y/clang -DX -I x\(op) ignored",
66+
executable: "x/y/clang",
67+
args: [
68+
.option(.D, spacing: .unspaced, value: "X"),
69+
.option(.I, spacing: .spaced, value: "x")
70+
],
71+
knownCommandOnly: true
72+
)
73+
}
74+
75+
assertParse(
76+
#"x/y/clang \< x\< "<""#,
77+
executable: "x/y/clang",
78+
args: [.value("<"), .value("x<"), .value("<")],
79+
knownCommandOnly: true
80+
)
81+
5782
assertParse(
5883
"clang -DX -I",
5984
args: [.option(.D, spacing: .unspaced, value: "X"), .flag(.I)]

0 commit comments

Comments
 (0)