From 81398dffa3b82d71a37eb9397709e2663e9da6a9 Mon Sep 17 00:00:00 2001 From: Alex Agapov Date: Wed, 12 Aug 2026 11:56:23 +0500 Subject: [PATCH 1/2] fix: classify Swift compilation steps from Xcode 14 and later Xcode renamed the Swift compilation tasks: `CompileSwift` became `SwiftCompile` and `CompileSwiftSources` became `SwiftDriver`. `getDetailType` still matches only the old spellings, so on a current log every Swift step is reported as `.other`, and reporters that group by step type lose the compilation entirely. The guard in `getSwiftIndividualSteps` is keyed to the same old name. Fixing the classification alone would make it worse: a batch compilation names its files in the command and the log already carries a subSection per file, so with the step recognized as `.swiftCompilation` and the guard missing the modern name, every file gets reported twice. Both places are updated together. Measured on an Xcode 26.5 log of a ~400 module iOS app (5.9 min build), same log before and after, release build from source: other 314.4 min -> 9.0 min swiftCompilation 0 min -> 282.8 min swiftAggregatedCompilation 0 min -> 22.6 min detail steps 24869 -> 24869 The step count and the sum of durations stay identical, so nothing is duplicated or dropped; only the type of the existing steps changes. `SwiftEmitModule`, `EmitSwiftModule` and `CodeSign` account for the 9 minutes that remain unclassified. They have no matching case in `DetailStepType`, so they are left for a separate change. Co-Authored-By: Claude Opus 5 (1M context) Signed-off-by: Alex Agapov --- Sources/XCLogParser/parser/BuildStep.swift | 8 ++ .../IDEActivityLogSection+Parsing.swift | 5 +- .../DetailStepTypeTests.swift | 69 +++++++++++++++ .../SwiftIndividualStepsTests.swift | 87 +++++++++++++++++++ 4 files changed, 168 insertions(+), 1 deletion(-) create mode 100644 Tests/XCLogParserTests/DetailStepTypeTests.swift create mode 100644 Tests/XCLogParserTests/SwiftIndividualStepsTests.swift diff --git a/Sources/XCLogParser/parser/BuildStep.swift b/Sources/XCLogParser/parser/BuildStep.swift index 4c082f52..cbfc65cf 100644 --- a/Sources/XCLogParser/parser/BuildStep.swift +++ b/Sources/XCLogParser/parser/BuildStep.swift @@ -98,6 +98,10 @@ public enum DetailStepType: String, Encodable { return .cCompilation case Prefix("CompileSwift "): return .swiftCompilation + // Xcode 14 and later name the same task `SwiftCompile`, both for a single file and for a + // batch of them. `CompileSwift` still appears in logs from older Xcode versions. + case Prefix("SwiftCompile "): + return .swiftCompilation case Prefix("Ld "): return .linker case Prefix("PhaseScriptExecution "): @@ -122,6 +126,10 @@ public enum DetailStepType: String, Encodable { return .XIBCompilation case Prefix("CompileSwiftSources "): return .swiftAggregatedCompilation + // The driver job that replaced `CompileSwiftSources`. The trailing space matters: without it + // the prefix also swallows `SwiftDriverJobDiscovery`, which is not a compilation step. + case Prefix("SwiftDriver "): + return .swiftAggregatedCompilation case Prefix("PrecompileSwiftBridgingHeader "): return .precompileBridgingHeader case Prefix("ValidateEmbeddedBinary "): diff --git a/Sources/XCLogParser/parser/IDEActivityLogSection+Parsing.swift b/Sources/XCLogParser/parser/IDEActivityLogSection+Parsing.swift index 189a1eb7..6deab19d 100644 --- a/Sources/XCLogParser/parser/IDEActivityLogSection+Parsing.swift +++ b/Sources/XCLogParser/parser/IDEActivityLogSection+Parsing.swift @@ -86,7 +86,10 @@ extension IDEActivityLogSection { public func getSwiftIndividualSteps(buildStep: BuildStep, parentCommandDetailDesc: String, currentIndex: inout Int) -> [BuildStep]? { - let pattern = #"^CompileSwift\s\w+\s\w+\s.+\.swift\s"# + // Both spellings of the task: `CompileSwift` up to Xcode 13, `SwiftCompile` from Xcode 14. + // Without the modern one the guard stops recognizing a command that already names the files, + // and the steps synthesized below duplicate the per-file subSections the log provides. + let pattern = #"^(?:CompileSwift|SwiftCompile)\s\w+\s\w+\s.+\.swift\s"# guard commandDetailDesc.range(of: pattern, options: .regularExpression) == nil else { return nil } diff --git a/Tests/XCLogParserTests/DetailStepTypeTests.swift b/Tests/XCLogParserTests/DetailStepTypeTests.swift new file mode 100644 index 00000000..7e7457cf --- /dev/null +++ b/Tests/XCLogParserTests/DetailStepTypeTests.swift @@ -0,0 +1,69 @@ +// Copyright (c) 2019 Spotify AB. +// +// Licensed to the Apache Software Foundation (ASF) under one +// or more contributor license agreements. See the NOTICE file +// distributed with this work for additional information +// regarding copyright ownership. The ASF licenses this file +// to you under the Apache License, Version 2.0 (the +// "License"); you may not use this file except in compliance +// with the License. You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, +// software distributed under the License is distributed on an +// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY +// KIND, either express or implied. See the License for the +// specific language governing permissions and limitations +// under the License. + +import XCTest +@testable import XCLogParser + +/// Signatures in this file are taken from an Xcode 26.5 build log, with paths and module names +/// replaced. Xcode renamed the Swift compilation tasks, so both spellings have to be recognized. +class DetailStepTypeTests: XCTestCase { + + func testSwiftCompileOfASingleFileIsASwiftCompilation() { + let signature = "SwiftCompile normal arm64 /path/to/Foo/Bar.swift " + + "(in target 'Foo' from project 'Foo')" + + XCTAssertEqual(.swiftCompilation, DetailStepType.getDetailType(signature: signature)) + } + + func testSwiftCompileOfABatchIsASwiftCompilation() { + let signature = "SwiftCompile normal arm64 Compiling\\ Bar.swift,\\ Baz.swift " + + "(in target 'Foo' from project 'Foo')" + + XCTAssertEqual(.swiftCompilation, DetailStepType.getDetailType(signature: signature)) + } + + func testCompileSwiftFromOlderXcodeVersionsIsStillASwiftCompilation() { + let signature = "CompileSwift normal arm64 /path/to/Foo/Bar.swift" + + XCTAssertEqual(.swiftCompilation, DetailStepType.getDetailType(signature: signature)) + } + + func testSwiftDriverIsAnAggregatedCompilation() { + let signature = "SwiftDriver Foo normal arm64 com.apple.xcode.tools.swift.compiler " + + "(in target 'Foo' from project 'Foo')" + + XCTAssertEqual(.swiftAggregatedCompilation, DetailStepType.getDetailType(signature: signature)) + } + + func testCompileSwiftSourcesFromOlderXcodeVersionsIsStillAnAggregatedCompilation() { + let signature = "CompileSwiftSources normal arm64 com.apple.xcode.tools.swift.compiler" + + XCTAssertEqual(.swiftAggregatedCompilation, DetailStepType.getDetailType(signature: signature)) + } + + /// `SwiftDriverJobDiscovery` shares its first eleven characters with `SwiftDriver`, and it is + /// not a compilation step: a prefix without the trailing space would swallow it. + func testSwiftDriverJobDiscoveryIsNotAnAggregatedCompilation() { + let signature = "SwiftDriverJobDiscovery normal arm64 Emitting module for Foo " + + "(in target 'Foo' from project 'Foo')" + + XCTAssertEqual(.other, DetailStepType.getDetailType(signature: signature)) + } + +} diff --git a/Tests/XCLogParserTests/SwiftIndividualStepsTests.swift b/Tests/XCLogParserTests/SwiftIndividualStepsTests.swift new file mode 100644 index 00000000..42a56a31 --- /dev/null +++ b/Tests/XCLogParserTests/SwiftIndividualStepsTests.swift @@ -0,0 +1,87 @@ +// Copyright (c) 2019 Spotify AB. +// +// Licensed to the Apache Software Foundation (ASF) under one +// or more contributor license agreements. See the NOTICE file +// distributed with this work for additional information +// regarding copyright ownership. The ASF licenses this file +// to you under the Apache License, Version 2.0 (the +// "License"); you may not use this file except in compliance +// with the License. You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, +// software distributed under the License is distributed on an +// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY +// KIND, either express or implied. See the License for the +// specific language governing permissions and limitations +// under the License. + +import XCTest +@testable import XCLogParser + +class SwiftIndividualStepsTests: XCTestCase { + + /// From Xcode 14 on, a batch compilation names its files in the command and the log carries a + /// subSection per file. Synthesizing steps here would report every file twice. + func testNoStepsAreSynthesizedWhenTheCommandNamesTheFiles() { + let command = "SwiftCompile normal arm64 Compiling\\ Bar.swift,\\ Baz.swift " + + "/path/to/Foo/Bar.swift /path/to/Foo/Baz.swift (in target 'Foo' from project 'Foo')" + let section = makeSection(commandDetailDesc: command) + var index = 0 + + let steps = section.getSwiftIndividualSteps(buildStep: makeStep(), + parentCommandDetailDesc: "", + currentIndex: &index) + + XCTAssertNil(steps) + } + + /// A whole module command lists the files without compiling them as separate sections, so the + /// per-file steps have to be derived from the command itself. + func testStepsAreSynthesizedForAWholeModuleCommand() { + let command = "/usr/bin/swiftc -module-name Foo -whole-module-optimization " + + "/path/to/Foo/Bar.swift /path/to/Foo/Baz.swift" + let section = makeSection(commandDetailDesc: command) + var index = 0 + + let steps = section.getSwiftIndividualSteps(buildStep: makeStep(), + parentCommandDetailDesc: "", + currentIndex: &index) + + XCTAssertEqual(["Compile /path/to/Foo/Bar.swift", "Compile /path/to/Foo/Baz.swift"], + steps?.map { $0.title }) + } + + private func makeStep() -> BuildStep { + makeFakeBuildStep(title: "Compiling Bar.swift, Baz.swift", + type: .detail, + detailStepType: .swiftCompilation, + startTimestamp: 0, + fetchedFromCache: false) + } + + private func makeSection(commandDetailDesc: String) -> IDEActivityLogSection { + IDEActivityLogSection(sectionType: 1, + domainType: "", + title: "Compiling Bar.swift, Baz.swift", + signature: "SwiftCompile normal arm64", + timeStartedRecording: 0, + timeStoppedRecording: 0, + subSections: [], + text: "", + messages: [], + wasCancelled: false, + isQuiet: false, + wasFetchedFromCache: false, + subtitle: "", + location: DVTDocumentLocation(documentURLString: "", timestamp: 0), + commandDetailDesc: commandDetailDesc, + uniqueIdentifier: "", + localizedResultString: "", + xcbuildSignature: "", + attachments: [], + unknown: 0) + } + +} From 4e84054c4669f0f99b002a6f8ba610813d230922 Mon Sep 17 00:00:00 2001 From: Alex Agapov Date: Wed, 12 Aug 2026 12:05:15 +0500 Subject: [PATCH 2/2] fix: correct how the two task spellings are described A single Xcode 26.5 log carries both: 86884 steps signed `CompileSwift` from targets that compile whole module, next to 666 `SwiftCompile` and 457 `SwiftDriver` steps from the integrated driver. Calling the old spelling a leftover of older Xcode versions was wrong, and the comments said so. Behaviour is unchanged; this only rewrites comments and two test names. Co-Authored-By: Claude Opus 5 (1M context) Signed-off-by: Alex Agapov --- Sources/XCLogParser/parser/BuildStep.swift | 5 +++-- .../XCLogParser/parser/IDEActivityLogSection+Parsing.swift | 6 +++--- Tests/XCLogParserTests/DetailStepTypeTests.swift | 7 ++++--- 3 files changed, 10 insertions(+), 8 deletions(-) diff --git a/Sources/XCLogParser/parser/BuildStep.swift b/Sources/XCLogParser/parser/BuildStep.swift index cbfc65cf..d97a1b17 100644 --- a/Sources/XCLogParser/parser/BuildStep.swift +++ b/Sources/XCLogParser/parser/BuildStep.swift @@ -98,8 +98,9 @@ public enum DetailStepType: String, Encodable { return .cCompilation case Prefix("CompileSwift "): return .swiftCompilation - // Xcode 14 and later name the same task `SwiftCompile`, both for a single file and for a - // batch of them. `CompileSwift` still appears in logs from older Xcode versions. + // The same task under the name the integrated driver uses, for a single file and for a batch + // of them. Both spellings show up in one log: targets that compile whole module keep + // emitting `CompileSwift`. case Prefix("SwiftCompile "): return .swiftCompilation case Prefix("Ld "): diff --git a/Sources/XCLogParser/parser/IDEActivityLogSection+Parsing.swift b/Sources/XCLogParser/parser/IDEActivityLogSection+Parsing.swift index 6deab19d..328241f4 100644 --- a/Sources/XCLogParser/parser/IDEActivityLogSection+Parsing.swift +++ b/Sources/XCLogParser/parser/IDEActivityLogSection+Parsing.swift @@ -86,9 +86,9 @@ extension IDEActivityLogSection { public func getSwiftIndividualSteps(buildStep: BuildStep, parentCommandDetailDesc: String, currentIndex: inout Int) -> [BuildStep]? { - // Both spellings of the task: `CompileSwift` up to Xcode 13, `SwiftCompile` from Xcode 14. - // Without the modern one the guard stops recognizing a command that already names the files, - // and the steps synthesized below duplicate the per-file subSections the log provides. + // Both spellings of the task, which appear side by side in a single log. Without the + // integrated driver's one the guard stops recognizing a command that already names the + // files, and the steps synthesized below duplicate the subSections the log provides. let pattern = #"^(?:CompileSwift|SwiftCompile)\s\w+\s\w+\s.+\.swift\s"# guard commandDetailDesc.range(of: pattern, options: .regularExpression) == nil else { return nil diff --git a/Tests/XCLogParserTests/DetailStepTypeTests.swift b/Tests/XCLogParserTests/DetailStepTypeTests.swift index 7e7457cf..6f01cb1d 100644 --- a/Tests/XCLogParserTests/DetailStepTypeTests.swift +++ b/Tests/XCLogParserTests/DetailStepTypeTests.swift @@ -21,7 +21,8 @@ import XCTest @testable import XCLogParser /// Signatures in this file are taken from an Xcode 26.5 build log, with paths and module names -/// replaced. Xcode renamed the Swift compilation tasks, so both spellings have to be recognized. +/// replaced. A single log carries both spellings of the Swift compilation tasks, so both have to +/// be recognized. class DetailStepTypeTests: XCTestCase { func testSwiftCompileOfASingleFileIsASwiftCompilation() { @@ -38,7 +39,7 @@ class DetailStepTypeTests: XCTestCase { XCTAssertEqual(.swiftCompilation, DetailStepType.getDetailType(signature: signature)) } - func testCompileSwiftFromOlderXcodeVersionsIsStillASwiftCompilation() { + func testCompileSwiftIsStillASwiftCompilation() { let signature = "CompileSwift normal arm64 /path/to/Foo/Bar.swift" XCTAssertEqual(.swiftCompilation, DetailStepType.getDetailType(signature: signature)) @@ -51,7 +52,7 @@ class DetailStepTypeTests: XCTestCase { XCTAssertEqual(.swiftAggregatedCompilation, DetailStepType.getDetailType(signature: signature)) } - func testCompileSwiftSourcesFromOlderXcodeVersionsIsStillAnAggregatedCompilation() { + func testCompileSwiftSourcesIsStillAnAggregatedCompilation() { let signature = "CompileSwiftSources normal arm64 com.apple.xcode.tools.swift.compiler" XCTAssertEqual(.swiftAggregatedCompilation, DetailStepType.getDetailType(signature: signature))