Context
I tried writing a macOS Swift app that launches an executable in /usr/local/bin (e.g. Homebrew-installed /usr/local/bin/cowsay).
However, this does not work because /usr/local/bin is not in the PATH environment variable.
Within the app, I tried adding /usr/local/bin to PATH via setenv, which also doesn't seem to work.
Code
Here is a code reproducer with Xcode 12.5 beta 3 (12E5244e):
import Foundation
import SwiftUI
func addPathComponentIfNotSet(_ pathComponent: String) {
// If path component already exists in PATH, return.
let path = ProcessInfo.processInfo.environment["PATH"]!
let pathComponents = path.split(separator: ":")
if pathComponents.contains(Substring(pathComponent)) {
return
}
// Otherwise, prepend path component to PATH via setenv.
let newPath = "\(pathComponent):\(path)"
setenv("PATH", newPath, 1)
// Print new PATH.
print("Actual new PATH:", ProcessInfo.processInfo.environment["PATH"]!)
}
@discardableResult
func shell(_ args: String...) -> Int32 {
let task = Process()
task.launchPath = "/usr/bin/env"
task.arguments = args
task.launch()
task.waitUntilExit()
return task.terminationStatus
}
struct DummyContentView: View {
var body: some View {
Button("Press me") {
// Add "/usr/local/bin" to PATH if it doesn't exist.
// Prints: "Actual new PATH: /usr/local/bin:/Applications/Xcode-beta.app/Contents/Developer/usr/bin:/usr/bin:/bin:/usr/sbin:/sbin"
addPathComponentIfNotSet("/usr/local/bin")
// Test: running "ls" works.
shell("ls")
// Error 1: running "cowsay" here fails with "env: cowsay: Operation not permitted".
shell("cowsay", "Test this")
// Error 2: running "cowsay" here fails with "The file “cowsay” doesn’t exist".
let executableURL = URL(fileURLWithPath: "/usr/local/bin/cowsay")
try! Process.run(
executableURL,
arguments: ["Test this too"],
terminationHandler: nil)
}
}
}
Questions
- Why do attempts to invoke
/usr/local/bin/cowsayfail?- I suspect it is related to security sandboxing.
- In a Swift macOS app, is it possible to invoke executables within
/usr/local/bin?- Perhaps it's possible if security measures like System Integrity Protection are disabled – other approaches would be ideal.
- How can I adapt the app to achieve my goal of invoking a local Homebrew-installed binary?
- One idea is to (a) create a server that can invoke the binary and (b) change the app to be a client sending requests to the server. I wonder if there are more lightweight solutions.
Accepted solution notes
Bill's solution worked for me. Here's a screenshot showing the fix – making Build Settings > Signing > Code Signing Entitlements empty:
