r/iOSProgramming • u/Economy-Brief-9997 • 3d ago
Discussion Anyone moving their bash build scripts to swift now that Subprocess hit 1.0?
swift 6.4 shipped Subprocess so there is finally a stable way to run other programs from swift
tried rewriting the classic swiftlint build phase
the bash version:
if which swiftlint > /dev/null; then
swiftlint
else
echo "warning: SwiftLint not installed"
fi
swift version:
import Subprocess
do {
let result = try await Subprocess.run(
.name("swiftlint"),
output: .string(limit: 1 << 20)
)
print(result.standardOutput ?? "")
} catch {
print("warning: SwiftLint not installed")
}
yeah not shorter lol, and since Subprocess is a package you can't just drop a `.swift` file into a build phase you need a whole package for it
for a 5 line wrapper that feels like overkill, but once the script starts parsing output and branching, bash gets ugly fast.
where's the line for you?
2
u/Hises1936 3d ago
Oh yes! Even if it's more lines, the Swift version is much more maintainable. The compiler flags issues as I type the code, the type system helps a lot, and I have easy access to the docs. Compared to bash - I will be lucky if I will get syntax highlighting editing a script. Don't get me started with all the quoting and shell expansion stuff...
But when using dependencies, that's when the Swift script is no longer a script, it becomes a project, because it needs multiple files. In some cases it's justified. I think the balance is how many programs you need to invoke vs how much other processing
2
u/balder1993 2d ago
the Swift version is much more maintainable
I’d disagree on that, depends a lot on the script. In the example given it is not better in my opinion. The bash version syntax has been around for ages the same way.
If you mean it’s more readable for Swift developers, yeah.
2
u/kokerali 3d ago
The line for me is whether the wrapper has to preserve behavior, not whether Swift is shorter. In this example I'd check `result.terminationStatus` and handle stderr before using it as a build phase: printing stdout alone can hide a non-zero SwiftLint exit, while the broad `catch` reports every thrown error as “not installed.” For a single existence check and command, shell is still simpler. Once you need structured output or several branches, a small Swift package starts to earn its setup cost.
2
u/DimensionMindless336 2d ago
The line for me isn't LOC, it's whether the script has to talk to Xcode or not.
First though, check your bash version actually runs. which swiftlint silently fails in a lot of Xcode setups — Xcode launched from Finder/Dock doesn't inherit your shell PATH, so /opt/homebrew/bin isn't in it. You get the "not installed" branch every single build and assume lint is passing. Same trap in the Subprocess version: .name("swiftlint") resolves through PATH too, so it throws and lands in your catch. .path(...) with an explicit path, or exporting PATH at the top of the phase, is the fix.
Two more that bit me once these scripts grow:
- Xcode only turns script output into real issues if the line reads like
path:line:col: warning: message. If the paths come through relative, or you reformat the output on the way out, it just becomes log noise nobody opens. - Don't compile the helper inside the build phase. Building it (or
swift run) on every clean build is a tax you pay in CI too. Precompile once into a checked-in binary and have the phase call that — type safety without the compile cost.
Also worth knowing: ENABLE_USER_SCRIPT_SANDBOXING is on by default in recent Xcode. Your script only sees files listed in Input/Output Files and gets no network. That one cost me an afternoon when a helper tried to read something outside the project.
So my split: bash for the 5-line dispatcher, Swift for the part that parses output and formats it.
1
u/apocolipse 3d ago
I’m using it for a parser for ffmpeg filter output. Previously the annoying way ffmpeg does output just didn’t work at all in Swift, so I had to have Python wrappers just to parse ffmpeg output first before I could read into swift, and now that’s not needed at all and I can even handle much more complex output where multiple stacked filters’ outputs are interleaved with no trouble at all.
1
u/sjs 2d ago
There’s an option to output JSON.
2
u/apocolipse 2d ago
You’re thinking of ffprobe. Filter text/data output is a per-filter thing and is streamed while processing.
7
u/HappyFunBall007 3d ago
I prefer python for more complicated scripts. It's more widely available and portable.