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.
12 minute lesson
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, and the first thing to learn about it is that your app does not live in your shell.
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, and a tool that works in Terminal can be unfindable 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 the honest options are checking the known locations — /opt/homebrew/bin on Apple silicon, /usr/local/bin on Intel — or letting the user pick the executable with the file importer from the previous module.
To read 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
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, and 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, and it only appears when output grows past the buffer — which means it appears in production, not in your quick test.
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.
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 impose a timeout so a hung child cannot hang your app with it.
Verify from the right context: not from Xcode, which launches your app with its own environment, but from Finder. Build the app, double-click the .app, and run the feature. If your tool resolution is wrong, this is where it shows.
Lesson completed