Skip to content

Commit 753e5cc

Browse files
committed
Re-architect format.py in swift
Make a couple of changes to `format.py` and use the opportunity to re-write it in Swift. 1. Always build a local copy of swift-format in `swift-syntax/.swift-format-build` in release mode. This eliminates issues that occur if you had `swift-format` installed on your system and `format.py` would pick it up 2. Since the local swift-format build is persistent (doesn’t live in `/tmp`), we can build it in release. This increases the first run but decreases the time for any subsequent runs from ~15s to ~1s, which should pay usually pay off for frequent contributors because of faster turnaround times. 3. Add a little tip to add `swift-format` as a git hook to Contributing.md And as an added bonus, the rewrite reduced the runtime of `format.(py|swift)` from 1.2s to 900ms.
1 parent feee0ef commit 753e5cc

File tree

5 files changed

+272
-140
lines changed

5 files changed

+272
-140
lines changed

.gitignore

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -26,3 +26,6 @@ Package.resolved
2626
*.pyc
2727

2828
Tests/PerformanceTest/baselines.json
29+
30+
# The local build of swift-format to format swift-syntax
31+
.swift-format-build

CONTRIBUTING.md

Lines changed: 14 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -21,12 +21,23 @@ swift-syntax is a SwiftPM package, so you can build and test it using anything t
2121

2222
swift-syntax is formatted using [swift-format](http://github.com/apple/swift-format) to ensure a consistent style.
2323

24-
To format your changes run `format.py` at the root of this repository. If you have a `swift-format` executable ready, you can pass it to `format.py`. If you do not, `format.py` will build its own copy of `swift-format` in `/tmp/swift-format`.
25-
26-
If you are seeing surprising formatting results, you most likely have a `swift-format` installed on your system that’s not the most recent version built from the `main` branch. To fix this, clone [swift-format](http://github.com/apple/swift-format), build it using `swift build` and pass the freshly built executable to `format.py` as `--swift-format path/to/swift-format/.build/debug/swift-format`. Alternatively, you can uninstall `swift-format` on your system and `format.py` will build it from scratch.
24+
To format your changes run `format.swift` at the root of this repository from the terminal as `./format.swift`. It will build a local copy of swift-format from the `main` branch and format the repository. Since it is building a release version of `swift-format`, the first run will take few minutes. Subsequent runs take less than 2 seconds.
2725

2826
Generated source code is not formatted to make it easier to spot changes when re-running code generation.
2927

28+
> [!NOTE]
29+
> You can add a git hook to ensure all commits to the swift-syntax repository are correctly formatted.
30+
> 1. Save the following contents to `swift-syntax/.git/hooks/pre-commit`
31+
> ```bash
32+
> #!/usr/bin/env bash
33+
> set -e
34+
> SOURCE_DIR=$(realpath "$(dirname $0)/../..")
35+
> swift run --package-path "$SOURCE_DIR/SwiftSyntaxDevUtils" swift-syntax-dev-utils format --lint
36+
> ```
37+
> 2. Mark the file as executable: `chmod a+x swift-syntax/.git/hooks/pre-commit`
38+
> 3. If you have global git hooks installed, be sure to call them at the end of the script with `path/to/global/hooks/pre-commit "$@"`
39+
40+
3041
## Generating Source Code
3142
3243
If you want to modify the generated files, open the [CodeGeneration](CodeGeneration) package and run the `generate-swift-syntax` executable.

SwiftSyntaxDevUtils/Sources/swift-syntax-dev-utils/SwiftSyntaxDevUtils.swift

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,7 @@ struct SwiftSyntaxDevUtils: ParsableCommand {
2626
""",
2727
subcommands: [
2828
Build.self,
29+
Format.self,
2930
GenerateSourceCode.self,
3031
Test.self,
3132
VerifySourceCode.self,
Lines changed: 254 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,254 @@
1+
//===----------------------------------------------------------------------===//
2+
//
3+
// This source file is part of the Swift.org open source project
4+
//
5+
// Copyright (c) 2014 - 2023 Apple Inc. and the Swift project authors
6+
// Licensed under Apache License v2.0 with Runtime Library Exception
7+
//
8+
// See https://swift.org/LICENSE.txt for license information
9+
// See https://swift.org/CONTRIBUTORS.txt for the list of Swift project authors
10+
//
11+
//===----------------------------------------------------------------------===//
12+
13+
import ArgumentParser
14+
import Foundation
15+
16+
struct Format: ParsableCommand {
17+
static var configuration: CommandConfiguration {
18+
return CommandConfiguration(
19+
abstract: "Format files in SwiftSyntax using swift-format.",
20+
discussion: """
21+
This command automatically builds the '\(Self.swiftFormatBranch)' branch \
22+
of swift-format in the '\(Paths.swiftFormatBuildDir.lastPathComponent)' \
23+
directory of this repository and uses the build to format the swift-syntax \
24+
sources.
25+
"""
26+
)
27+
}
28+
29+
@Flag(help: "Update the sources of swift-format and rebuild swift-format")
30+
var update: Bool = false
31+
32+
@Flag(help: "Instead of formatting in-place, verify that the files are correctly formatted. Exit with 1 if files are not correctly formatted.")
33+
var lint: Bool = false
34+
35+
@Option(
36+
help: """
37+
Instead of building a local swift-format, use this swift-format executable. \
38+
Should primarily be used for CI, which has already built swift-format.
39+
"""
40+
)
41+
var swiftFormat: String? = nil
42+
43+
@Flag(help: "Enable verbose logging.")
44+
var verbose: Bool = false
45+
46+
/// The configuration to build swift-format in.
47+
private static let swiftFormatBuildConfiguration: String = "release"
48+
49+
/// The branch of swift-format to build.
50+
private static let swiftFormatBranch: String = "main"
51+
52+
private enum Error: Swift.Error, CustomStringConvertible {
53+
case swiftFormatNotFound
54+
case lintFailed
55+
56+
var description: String {
57+
switch self {
58+
case .swiftFormatNotFound:
59+
return "The locally built swift-format could not be found"
60+
case .lintFailed:
61+
return """
62+
The swift-syntax repo is not formatted according to the style guides.
63+
64+
Run the following command to format swift-syntax
65+
66+
swift run --package-path SwiftSyntaxDevUtils/ swift-syntax-dev-utils format
67+
68+
If the issue persists, try updating swift-format by running
69+
70+
swift run --package-path SwiftSyntaxDevUtils/ swift-syntax-dev-utils format --update
71+
72+
Should that still fail, fix any remaining issues manually and verify they match the swift-format style using
73+
74+
swift run --package-path SwiftSyntaxDevUtils/ swift-syntax-dev-utils format --lint
75+
"""
76+
}
77+
}
78+
}
79+
80+
/// Run `git` in the .swift-format-build directory with the provided arguments.
81+
private func runGitCommand(_ arguments: String...) throws {
82+
try ProcessRunner(
83+
executableURL: Paths.gitExec,
84+
arguments: ["-C", Paths.swiftFormatBuildDir.path] + arguments
85+
).run(
86+
captureStdout: false,
87+
captureStderr: false,
88+
verbose: verbose
89+
)
90+
}
91+
92+
/// Run `swift` for the `.swift-format-build` package with the provided arguments.
93+
private func runSwiftCommand(_ action: String, _ arguments: String...) throws {
94+
try ProcessRunner(
95+
executableURL: Paths.swiftExec,
96+
arguments: [action, "--package-path", Paths.swiftFormatBuildDir.path] + arguments
97+
).run(
98+
captureStdout: false,
99+
captureStderr: false,
100+
verbose: verbose
101+
)
102+
}
103+
104+
/// Ensure that we have an up-to-date checkout of swift-format in `.swift-format-build`.
105+
private func cloneOrUpdateSwiftFormat() throws {
106+
if FileManager.default.fileExists(atPath: Paths.swiftFormatBuildDir.appendingPathComponent(".git").path) {
107+
try runGitCommand("checkout", Self.swiftFormatBranch)
108+
try runGitCommand("pull")
109+
} else {
110+
try FileManager.default.createDirectory(atPath: Paths.swiftFormatBuildDir.path, withIntermediateDirectories: true)
111+
try runGitCommand("clone", "https://github.com/apple/swift-format.git", ".")
112+
try runGitCommand("checkout", Self.swiftFormatBranch)
113+
}
114+
try runSwiftCommand("package", "update")
115+
}
116+
117+
/// Build the swift-format executable.
118+
private func buildSwiftFormat() throws {
119+
try runSwiftCommand("build", "--product", "swift-format", "--configuration", Self.swiftFormatBuildConfiguration)
120+
}
121+
122+
/// Get the URL of the locally-built swift-format executable.
123+
private func findSwiftFormatExecutable() throws -> URL {
124+
if let swiftFormat = swiftFormat {
125+
return URL(fileURLWithPath: swiftFormat)
126+
}
127+
128+
// We could run `swift build --show-bin-path` here but that takes 0.4s.
129+
// Since the path seems really stable, let’s build the path ourselves.
130+
let swiftFormatExec = URL(fileURLWithPath: Paths.swiftFormatBuildDir.path)
131+
.appendingPathComponent(".build")
132+
.appendingPathComponent(Self.swiftFormatBuildConfiguration)
133+
.appendingPathComponent("swift-format")
134+
if !swiftFormatExec.isExecutableFile {
135+
throw Error.swiftFormatNotFound
136+
}
137+
return swiftFormatExec
138+
}
139+
140+
/// Get the list of files that should be formatted using swift-format.
141+
///
142+
/// This excludes some files like generated files or test inputs.
143+
private func filesToFormat() -> [URL] {
144+
guard let enumerator = FileManager.default.enumerator(at: Paths.packageDir.resolvingSymlinksInPath(), includingPropertiesForKeys: [], options: []) else {
145+
return []
146+
}
147+
148+
var result: [URL] = []
149+
for case let url as URL in enumerator {
150+
switch url.lastPathComponent {
151+
case "lit_tests",
152+
"generated",
153+
"build",
154+
"Inputs",
155+
".build",
156+
".swift-format-build":
157+
enumerator.skipDescendants()
158+
default:
159+
break
160+
}
161+
if url.pathExtension == "swift" {
162+
result.append(url)
163+
}
164+
}
165+
166+
return result
167+
}
168+
169+
/// Format all files in the repo using the locally-built swift-format.
170+
private func formatFilesInRepo() throws {
171+
let swiftFormatExecutable = try findSwiftFormatExecutable()
172+
173+
let filesToFormat = self.filesToFormat()
174+
175+
try ProcessRunner(
176+
executableURL: swiftFormatExecutable,
177+
arguments: [
178+
"format",
179+
"--in-place",
180+
"--parallel",
181+
] + filesToFormat.map { $0.path }
182+
)
183+
.run(
184+
captureStdout: false,
185+
captureStderr: false,
186+
verbose: verbose
187+
)
188+
}
189+
190+
/// Lint all files in the repo using the locally-built swift-format.
191+
private func lintFilesInRepo() throws {
192+
let swiftFormatExecutable = try findSwiftFormatExecutable()
193+
194+
let filesToFormat = self.filesToFormat()
195+
196+
do {
197+
try ProcessRunner(
198+
executableURL: swiftFormatExecutable,
199+
arguments: [
200+
"lint",
201+
"--strict",
202+
"--parallel",
203+
] + filesToFormat.map { $0.path }
204+
)
205+
.run(
206+
captureStdout: false,
207+
captureStderr: false,
208+
verbose: verbose
209+
)
210+
} catch is NonZeroExitCodeError {
211+
throw Error.lintFailed
212+
}
213+
}
214+
215+
func run() throws {
216+
#if compiler(<5.10)
217+
print("💡 You are building running the format script with Swift 5.9 or lower. Running it with SwiftPM 5.10 is about 10s faster.")
218+
#endif
219+
220+
try run(updateAndBuild: self.update)
221+
}
222+
223+
/// - Parameter updateAndBuild: Whether to update the locally checked out
224+
/// swift-format sources and rebuild swift-format.
225+
func run(updateAndBuild: Bool) throws {
226+
if updateAndBuild {
227+
try cloneOrUpdateSwiftFormat()
228+
try buildSwiftFormat()
229+
}
230+
do {
231+
if lint {
232+
try lintFilesInRepo()
233+
} else {
234+
try formatFilesInRepo()
235+
}
236+
} catch Error.swiftFormatNotFound {
237+
if !updateAndBuild {
238+
print(
239+
"""
240+
No build of swift-format was found in '\(Paths.swiftFormatBuildDir.lastPathComponent)'.
241+
Building swift-format now. This may take a couple of minutes.
242+
Future invocations of 'format.swift' will re-use the build and are much faster.
243+
244+
"""
245+
)
246+
247+
// If swift-format cannot be found, try again, updating (aka cloning + building) swift-format this time
248+
try run(updateAndBuild: true)
249+
} else {
250+
throw Error.swiftFormatNotFound
251+
}
252+
}
253+
}
254+
}

0 commit comments

Comments
 (0)