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/cowsay fail?
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:
The main problem is sandboxing. If this is just for your own use, you could disable sandbox for this application. To do so, go to target / build settings / signing. Remove the name of the entitlements file from code signing entitlements.
Related
I have a single project but with 4 different environments (Dev,Stagging,QA,Production). I have given their (environment's webservice url) paths from mobile's setting. Now I want to use different GoogleService-info.plist for all these different environments. Like when I select Dev from backend the project should take GoogleService-Info.plist of Dev project only. These GoogleService-Info.plists are created on 4 different accounts. Project should take the path of GoogleService-info.plist programmatically. I have tried the following code
1] By taking reference from this url , I have created two folders Dev and QA (for now) and tried to given their paths by programmaically
#if DEV
print("[FIREBASE] Development mode.")
filePath = Bundle.main.path(forResource: "GoogleService-Info",
ofType: "plist", inDirectory: "Dev")
#elseif QA
print("[FIREBASE] QA mode.")
filePath = Bundle.main.path(forResource: "GoogleService-Info",
ofType: "plist", inDirectory: "QA")
#endif
let options = FirebaseOptions.init(contentsOfFile: filePath)!
FirebaseApp.configure(options: options)
But it throws an error
Thread 1: Fatal error: Unexpectedly found nil while unwrapping an Optional value
on
let options = FirebaseOptions.init(contentsOfFile: filePath)!
this line
2] Second I changed name of GoogleService-Info.plist by GoogleService-Info-QA.plist and tried to access this file programmatically
private func configureFirebase() {
guard let plistPath = Bundle.main.path(forResource:
"GoogleService-Info-QA", ofType: "plist"),
let options = FirebaseOptions(contentsOfFile: plistPath)
else { return }
FirebaseApp.configure(options: options)
}
But it throws an error
Terminating app due to uncaught exception 'FIRAppNotConfigured',
reason: 'Failed to get default Firebase Database instance. Must
call `[FIRApp configure]` (`FirebaseApp.configure()` in Swift)
before using Firebase Database.
For this confirmation you have to follow following steps:
Goto Project Setting
Select you Dev Target
Goto Build Phase
Click on icon and create new run script with name GOOGLESERVICE_INFO_PLIST
use following script
//Name of the resource we're selectively copying
GOOGLESERVICE_INFO_PLIST=GoogleService-Info.plist
//Get references to dev and prod versions of the GoogleService-Info.plist
//NOTE: These should only live on the file system and should NOT be part of the target (since we'll be adding them to the target manually)
GOOGLESERVICE_INFO_DEV=${PROJECT_DIR}/projectFolder/Firebase/Dev/${GOOGLESERVICE_INFO_PLIST}
GOOGLESERVICE_INFO_PROD=${PROJECT_DIR}/projectFolder/Firebase/Prod/${GOOGLESERVICE_INFO_PLIST}
//Make sure the dev version of GoogleService-Info.plist exists
echo "Looking for ${GOOGLESERVICE_INFO_PLIST} in ${GOOGLESERVICE_INFO_DEV}"
if [ ! -f $GOOGLESERVICE_INFO_DEV ]
then
echo "No Development GoogleService-Info.plist found. Please ensure it's in the proper directory."
exit 1
fi
//Make sure the prod version of GoogleService-Info.plist exists
echo "Looking for ${GOOGLESERVICE_INFO_PLIST} in ${GOOGLESERVICE_INFO_PROD}"
if [ ! -f $GOOGLESERVICE_INFO_PROD ]
then
echo "No Production GoogleService-Info.plist found. Please ensure it's in the proper directory."
exit 1
fi
//Get a reference to the destination location for the GoogleService-Info.plist
PLIST_DESTINATION=${BUILT_PRODUCTS_DIR}/${PRODUCT_NAME}.app
echo "Will copy ${GOOGLESERVICE_INFO_PLIST} to final destination: ${PLIST_DESTINATION}"
//Copy over the prod GoogleService-Info.plist for Release builds
if [ "${CONFIGURATION}" == "Release" ]
then
echo "Using ${GOOGLESERVICE_INFO_PROD}"
cp "${GOOGLESERVICE_INFO_PROD}" "${PLIST_DESTINATION}"
else
echo "Using ${GOOGLESERVICE_INFO_DEV}"
cp "${GOOGLESERVICE_INFO_DEV}" "${PLIST_DESTINATION}"
fi
Final Step: Make sure you have placed GoogleService-Info.plist at correct location path in the project, You can find the attached image for reference.
GOOGLESERVICE_INFO_DEV=${PROJECT_DIR}/projectFolder/Firebase/Dev/${GOOGLESERVICE_INFO_PLIST}
GOOGLESERVICE_INFO_PROD=${PROJECT_DIR}/projectFolder/Firebase/Prod/${GOOGLESERVICE_INFO_PLIST}
projectFolder is your current project folder
Place this code snippet in your AppDelegate.swift in the application's didFinishLaunchingWithOptions delegate function, somewhere before the return true
//Configure Firebase based on the app's environment
#if DEV
guard let filePath = Bundle.main.path(forResource: "GoogleService-Info-DEV", ofType: "plist") else { return }
let options = FIROptions(contentsOfFile: filePath)
FIRApp.configure(with: options!)
#elseif QA
guard let filePath = Bundle.main.path(forResource: "GoogleService-Info-QA", ofType: "plist") else { return }
let options = FIROptions(contentsOfFile: filePath)
FIRApp.configure(with: options!)
#endif
You need to make sure your plist files are named accordingly, and also make sure they are part of your target:
Select the GoogleService-Info-DEV file and on the right, in the FileInspector make sure the checkbox is ticked for your app's target. Do the same with the GoogleService-Info-QA file.
Your files should be placed inside the main folder, as you would place a normal Google-Info.plist file.
I got the solution. I renamed GoogleService-Info.plists files by its respective environments. And Added those files in Build Phases -> Copy Bundle Resources. Then added the following code as per the environment selected
guard let plistPath = Bundle.main.path(forResource: "nameOfTheGoogleService-Info.plistFileAsPerTheEnvironment", ofType: "plist"),
let options = FirebaseOptions(contentsOfFile: plistPath)
else { return }
if FirebaseApp.app() == nil{
FirebaseApp.configure(options: options)
}
Minor change is that, when user changes the app's environment, then he should remove app from background and open it again. Then the AppDeleagte takes the path of respective GoogleService-Info.plist as per its environment selected
I found article describing how to create plugin using Swift and Cocoa. It uses NSBundle to load plugin, but that, as far as I know, is not available in pure swift (no Cocoa). Is there way how to achieve same result without using Cocoa?
More info:
In case it's relevant, here is what I want to achieve. I create app in swift that runs on linux server. User can connect to it using their browser. I want to be able to have other people write "plugins" that will implement functionality itself (what user can see and do once they connect), from printing out hello world, through chat programs to games without having to worry about low level stuff provided by my app. Some sort of dll, that my server application loads and runs.
Solution to this is not trivial, but it's not impossible to do either. I prefer to use swift package manager to manage dependencies and Xcode as IDE. This combination is not perfect as it needs a lot of tinkering but there is not any other useable free swift IDE as of now.
You will need to set up two projects, let's call them Plugin (3rd party library) and PluginConsumer (app that uses other people plugins). You will also need to decide on API, for now we will use simple
TestPluginFunc()
Create Plugin.swift file with TestPluginFunc implementation in your Plugin project:
public func TestPluginFunc() {
print("Hooray!")
}
Set the project to build framework, not executable and build[1]. You will get Plugin.framework file which contains your plugin.
Now switch to your PluginConsumer project
Copy Plugin.framework from your Plugin project somewhere where you can easily find it. To actually load the framework and use it:
// we need to define how our plugin function looks like
typealias TestPluginFunc = #convention(c) ()->()
// and what is its name
let pluginFuncName = "TestPluginFunc"
func loadPlugin() {
let pluginName = "Plugin"
let openRes = dlopen("./\(pluginName).framework/\(pluginName)", RTLD_NOW|RTLD_LOCAL)
if openRes != nil {
// this is fragile
let symbolName = "_TF\(pluginName.utf8.count)\(pluginName)\(initFuncName.utf8.count)\(initFuncName)FT_T_"
let sym = dlsym(openRes, symbolName)
if sym != nil {
// here we load func from framework based on the name we constructed in "symbolName" variable
let f: TestPluginFunc = unsafeBitCast(sym, to: TestPluginFunc.self)
// and now all we need to do is execute our plugin function
f()
} else {
print("Error loading \(realPath). Symbol \(symbolName) not found.")
dlclose(openRes)
}
} else {
print("error opening lib")
}
}
If done correctly, you should see "Hooray!" being printed to your log.
There is a lot of room for improvement, first thing you should do is replace Plugin.framework string with parameter, preferably using some file library (I am using PerfectLib). Another thing to look at is defining plugin API in your PluginConsumer project as a protocol or base class, creating framework out of that, importing that framework in your plugin project and basing your implementation on that protocol/base class. I am trying to figure out exactly how to do that. I will update this post if I mange to do it properly.
[1]: I usually do this by creating Package.swift file and creating xcode project out of it using swift package generate-xcodeproj. If your project doesn't contain main.swift, xcode will create framework instead of executable
What you will want to do is create a folder your program will look in. Let's say it's called 'plugins'. It should make a list of names from the files in there, and then iterate through using them, passing parameters to the files and getting the output and making use of that in some way.
Activating a program and getting output:
func runCommand(cmd : String, args : String...) -> (output: [String], error: [String], exitCode: Int32) {
var output : [String] = []
var error : [String] = []
let task = Process()
task.launchPath = cmd
task.arguments = args
let outpipe = Pipe()
task.standardOutput = outpipe
let errpipe = Pipe()
task.standardError = errpipe
task.launch()
let outdata = outpipe.fileHandleForReading.readDataToEndOfFile()
if var string = String(data: outdata, encoding: .utf8) {
string = string.trimmingCharacters(in: .newlines)
output = string.components(separatedBy: "\n")
}
let errdata = errpipe.fileHandleForReading.readDataToEndOfFile()
if var string = String(data: errdata, encoding: .utf8) {
string = string.trimmingCharacters(in: .newlines)
error = string.components(separatedBy: "\n")
}
task.waitUntilExit()
let status = task.terminationStatus
return (output, error, status)
}
`
Here is how a swift plugin would accept arguments:
for i in 1..C_ARGC {
let index = Int(i);
let arg = String.fromCString(C_ARGV[index])
switch arg {
case 1:
println("1");
case 2:
println("2")
default:
println("3)
}
}
So once you have the program and plugin communicating you just have to add handling in your program based on the output so the plugins output can do something meaningful. Without cocoa libraries this seems the way to go, though if you use C there are a couple of other options available there as well. Hope this helps.
I have a command-line app A, and in A I execute an executable script B, in B I'm expecting an input from stdin.
I wrote a demo, implementing A in Swift, using Foundation's Process api, finding that B, no matter implemented in whatever language, cannot get user input from stdin.
Code:
// `A`'s main.swift
import Foundation
let process = Process()
process.launchPath = PATH_TO_SCRIPT_B
process.launch()
process.waitUntilExit()
// `B`
#!/usr/bin/swift
print("intpu something")
let input = readLine()
print("input: \(input)")
I did not set the process's input since according to the doc:
If this method isn’t used, the standard input is inherited from the process that created the receiver.
UPDATE:
A is an executable package created using Swift Package Manager. I used swift package generate-xcodeproj to generate an Xcode project file. I confirmed that if I run the executable built using swift build or xcodebuild in a shell, the problem with getting input from stdin from B arose. However if I run it directly inside Xcode, by pressing command + R inside Xcode, then it worked. So if I understand the difference between running an executable in a shell and Xcode, I can probably make everything work.
func task() {
print("input here")
let x = input()
print ("inputed:" + x)
}
func input() -> String {
let keyboard = FileHandle.standardInput
let inputData = keyboard.availableData
let strData = String(data: inputData, encoding: .utf8)!
let string = strData.trimmingCharacters(in: .newlines)
return string
}
task()
Hope it helps
For a open source project we want to create a OS X Uninstaller, which should be able to delete user and system domain files. The goal is to write the Uninstaller in Swift only.
During my research I found examples using NSTask to send a rm command,
or others recommend using SMJobBless and a HelperTool for authentication. Both seems for me not the right way. I'm new to the OS X development, so it's hard for me to say which is the right way to achive the task.
I'm looking for an example in Swift 2 which allows user to authenticate and delete user and system domain files. If any one colud help me, I would really appreciate it.
After a couple hours playing around with SecurityFoundation, I found it to be too complex for my taste. Plus, the documentation is outdated. Inspired by this question. You can do it via AppleScript.
You need to divide your uninstaller into 2 parts: a default target
to delete user files and a helper to delete system files (with escalated privileges of course).
Option 1
You can write that helper as a shell script and include that as a resource:
File delete_system_file.sh:
#!/bin/bash
rm -f /usr/local/dummy.txt
In your app:
let helper = NSBundle.mainBundle().pathForResource("delete_system_file", ofType: "sh")!
let script = "do shell script \"\(helper)\" with administrator privileges"
if let appleScript = NSAppleScript(source: script) {
var error: NSDictionary? = nil
appleScript.executeAndReturnError(&error)
if error != nil {
print(error!)
} else {
print("Deleted /usr/local/dummy.txt")
}
} else {
print("Cannot create the AppleScript object")
}
Option 2
You can do this entirely in Swift by adding a new target to your project. Assuming your first target is called MyUninstaller.
Add a new target
Click File > New > Target...
Under OS X, select Application > Command Line Tool
Give your new target a name, say DeleteSystemFile
In the Project Navigator on the left, expand the DeleteSystemFile group and click on the main.swift file. To delete the dummy.txt file:
do {
try NSFileManager.defaultManager().removeItemAtPath("/usr/local/dummy.txt")
} catch {
print(error)
}
Back to the first target
Now go back to the first target (MyUninstaller) and add DeleteSystemFile as a Target Dependancy.
You can run the second target with escalated privileges by calling it through AppleScript:
let helper = NSBundle.mainBundle().pathForAuxiliaryExecutable("DeleteSystemFile")!
let script = "do shell script \"\(helper)\" with administrator privileges"
if let appleScript = NSAppleScript(source: script) {
var error: NSDictionary? = nil
appleScript.executeAndReturnError(&error)
if error != nil {
print(error!)
} else {
print("Deleted /usr/local/dummy.txt")
}
} else {
print("Cannot create the AppleScript object")
}
Option 3
Use SMJobBless, which is the Apple's recommended way of running privileged helper:
import SecurityFoundation
import ServiceManagement
// 1. Obtain an Authorization Reference
// You can do this at the beginning of the app. It has no extra rights until later
var authRef: AuthorizationRef = nil
let status = AuthorizationCreate(nil, nil, [.Defaults], &authRef)
// There's really no reason for this to fail, but we should check or completeness
guard status == errAuthorizationSuccess else {
fatalError("Cannot create AuthorizationRef: \(status)")
}
// 2. Ask user for admin privilege
var authItem = AuthorizationItem(name: kSMRightBlessPrivilegedHelper, valueLength: 0, value: nil, flags: 0)
var authRights = AuthorizationRights(count: 1, items: &authItem)
let flags: AuthorizationFlags = [.Defaults, .InteractionAllowed, .ExtendRights]
let status2 = AuthorizationCopyRights(authRef, &authRights, nil, flags, nil)
if status2 != errAuthorizationSuccess {
// Can't obtain admin privilege, handle error
print("Cannot obtain admin privilege")
}
// 3. Run the privileged helper
// This label must be globally unique and matches the product name of your helper
let label = "com.myCompany.myApp.myAppPrivilgedHelper"
var error: CFError? = nil
let result = withUnsafeMutablePointer(&error) {
SMJobBless(kSMDomainSystemLaunchd, label, authRef, UnsafeMutablePointer($0))
}
if !result {
print(error!)
}
// 4. Release the Authorization Reference
AuthorizationFree(authRef, [.Defaults])
This involves some setup, which you can read about in the documentation for SMJobBless. There's also a sample project in ObjC.
Disclaimer: I could not test this all the way as I don't have a personal signing key. I could do away with section 2 from above, but included it here for completeness--that's how the sample project does it.
In my app I would like to open another app that is installed on the User's Mac (such as iPhoto). I am not sure what I should be looking for in the documentation. What is this called and how should I do it?
Thank you
Swift 5 or later
import Cocoa
func openPhotos() -> Bool {
if let photosApp = FileManager.default.urls(
for: .applicationDirectory,
in: .systemDomainMask
).first?.appendingPathComponent("Photos.app") {
return NSWorkspace.shared.open(photosApp)
}
return false
}
Usage:
if openPhotos() {
print(true)
}
Or using launchApplication with the app name parameter in the method:
import Cocoa
func openApp(_ named: String) -> Bool {
NSWorkspace.shared.launchApplication(named)
}
Usage:
if openApp("Photos") {
print(true)
}
XCode 11 • MacOS Catalina 10.15 • Swift 5
NSWorkspace.shared.launchApplication is deprecated and starting from the MacOS 10.15 the new function NSWorkspace.shared.openApplication shall be used.
Example - open terminal application by its bundle id
guard let url = NSWorkspace.shared.urlForApplication(withBundleIdentifier: "com.apple.Terminal") else { return }
let path = "/bin"
let configuration = NSWorkspace.OpenConfiguration()
configuration.arguments = [path]
NSWorkspace.shared.openApplication(at: url,
configuration: configuration,
completionHandler: nil)
Example - open terminal application by its path
let url = NSURL(fileURLWithPath: "/System/Applications/Utilities/Terminal.app", isDirectory: true) as URL
let path = "/bin"
let configuration = NSWorkspace.OpenConfiguration()
configuration.arguments = [path]
NSWorkspace.shared.openApplication(at: url,
configuration: configuration,
completionHandler: nil)
You can use NSWorkspace class written by Swift/Cocoa.
https://developer.apple.com/library/mac/documentation/Cocoa/Reference/ApplicationKit/Classes/NSWorkspace_Class/index.html#//apple_ref/occ/instm/NSWorkspace/launchApplication:
let task = NSTask.launchedTaskWithLaunchPath(<#path: String#>, arguments: <#[AnyObject]#>) will probably do what you want
I second the answer by vookimedlo - unfortunately, I cannot yet comment (silly reputation limit) so I post this as an extra answer.
This is just one caveat, which might not affect too many: while launchApplication() accepted a path to an executable (e.g. "MyApp.app/Contents/MacOS/MyApp"), this will result in an error (lacking privileges) with openApplication(::). You have to supply the path to the app bundle ("MyApp.app") instead.
Of particular interest when you try to make a helper ("launcher") to add as a login item. See the following and keep my comment in mind:
https://theswiftdev.com/2017/10/27/how-to-launch-a-macos-app-at-login/
(GREAT article by Tibor Bödecs)
BTW, as for vookimedlo's code: in my experience, you don't need to specify the OpenContext.arguments with [path], you can simply pass a default NSWorkspace.OpenContext()...
There are different ways to do that. The most efficient is to use fvork and execve - see man vfork and man execve.
Less efficient but more flexible is to use the system library call. What that actually does is runs a shell - like bash - then passes the string you provide, to bash. So you can set up pipelines, redirection and such.
Or you can send an Apple Event to the Finder: "Tell Finder Open iPhoto".
In the first two cases you want to launch the executable inside the bundle, that is, /Applications/iPhoto.app/Contents/MacOS/iPhoto.
Try the above from the command line, in the Terminal:
$ /Applications/iPhoto.app/Contents/MacOS/iPhoto
You'll see the iPhoto App launch.