macOS integration
Launch a command-line tool explicitly
Resolve an executable deliberately and give Process an explicit environment instead of relying on a Finder-launched app’s PATH.
A Mac app can lean on command-line tools for heavy lifting. Imagine the notes app shelling out to git to version a notes folder.
The API is Process. The first thing to learn about it: your app does not live in your shell.
Your app has no PATH
When you type git in Terminal, the shell searches your PATH, shaped by .zshrc and Homebrew’s setup. An app launched from Finder inherits none of that.
Its environment is minimal. Its PATH is a short system default. A tool that works in Terminal can be impossible to find from the app.
So be explicit about everything:
let process = Process()
process.executableURL = URL(filePath: "/usr/bin/git")
process.arguments = ["--version"]
process.environment = [
"PATH": "/opt/homebrew/bin:/usr/local/bin:/usr/bin:/bin"
]
The absolute executableURL removes the PATH question for system tools. For user-installed tools you have two honest options. Check the known locations, /opt/homebrew/bin on Apple silicon and /usr/local/bin on Intel. Or let the user pick the executable with the file importer from the previous module.
Read the output
Attach a pipe before launching:
let stdout = Pipe()
process.standardOutput = stdout
try process.run()
let data = try stdout.fileHandleForReading.readToEnd() ?? Data()
process.waitUntilExit()
let output = String(decoding: data, as: UTF8.self)
let succeeded = process.terminationStatus == 0
For git --version the output is a single line like git version 2.39.5 (Apple Git-154), and succeeded is true.
Read before you wait
Order matters here more than it looks. Read the pipe before waitUntilExit, not after.
A pipe buffer holds around 64 KB. If the child prints more while nobody reads, it blocks on its own write call. Your app blocks in waitUntilExit waiting for a child that is waiting for you. Both sides sit there forever.
This deadlock is the most common Process bug. It only appears when output grows past the buffer, which means it appears in production, not in your quick test.
Check the exit status
Check terminationStatus every time. Zero means success by convention. Anything else means the tool failed, and stderr usually says why. Capture it with a second pipe on standardError.
Treat the child as a dependency
A child process is an external dependency. It can be missing, be the wrong version, hang, or print unexpected output.
Record the resolved path and version at startup. git --version is cheap. And put a timeout on every run, so a hung child cannot hang your app with it.
Verify from the right place
Don’t verify from Xcode. It launches your app with its own environment, which hides the problem.
Build the app, double-click the .app in Finder, and run the feature. If your tool resolution is wrong, this is where it shows.
Lesson completed