Normalize generated schemes to match Xcode's output

XcodeGen (through XcodeProj) serializes schemes slightly differently to
Xcode, so Xcode rewrites them as soon as it touches them (running tests,
switching test plans, opening the scheme editor), leaving recurring local
changes behind.

Add a small Swift script, run from XcodeGen's postGenCommand, that rewrites
the generated schemes the way Xcode saves them: it drops default-valued
attributes and empty CommandLineArguments, orders TestPlanReference
attributes, and moves TestPlans to the end of the TestAction, omitting the
redundant empty Testables.

Regenerated schemes are byte-identical to the copies saved by Xcode. The
script can be removed once https://github.com/tuist/XcodeProj/pull/1189
ships in XcodeGen.
This commit is contained in:
Stefan Ceriu
2026-10-14 09:58:36 +03:00
parent b6ccbbc6c3
commit 11651bcd23
10 changed files with 78 additions and 120 deletions
+27
View File
@@ -0,0 +1,27 @@
import Foundation
// XcodeGen (via XcodeProj) serializes schemes slightly differently to Xcode, so Xcode
// rewrites them as soon as it touches them. Match Xcode's output to avoid the churn.
// Remove once https://github.com/tuist/XcodeProj/pull/1189 ships in XcodeGen.
let schemesDirectory = URL(filePath: CommandLine.arguments[1])
for url in try FileManager.default.contentsOfDirectory(at: schemesDirectory, includingPropertiesForKeys: nil)
where url.pathExtension == "xcscheme" {
var scheme = try String(contentsOf: url, encoding: .utf8)
scheme.replace(#/"\s*(runPostActionsOnFailure|onlyGenerateCoverageForSpecifiedTargets) = "NO">/#, with: "\">")
scheme.replace(#/\s*<CommandLineArguments>\s*<\/CommandLineArguments>/#, with: "")
scheme.replace(#/default = (?<value>"[^"]*")(?<indent>\s*)reference = (?<reference>"[^"]*")/#) { match in
"reference = \(match.reference)\(match.indent)default = \(match.value)"
}
// Xcode writes test plans last and omits the then redundant empty testables.
if let testPlans = scheme.firstMatch(of: #/\s*<TestPlans>.*?<\/TestPlans>/#.dotMatchesNewlines()) {
scheme.removeSubrange(testPlans.range)
scheme.replace(#/\s*<Testables>\s*<\/Testables>/#, with: "")
scheme.replace(#/\s*<\/TestAction>/#) { testPlans.output + $0.output }
}
try scheme.write(to: url, atomically: true, encoding: .utf8)
}