citruspi
citruspi

Reputation: 6891

Embed Executable inside Mac Application

If you go to /usr/bin you will see hundreds of executables or links to executables.

My application (Mac app written in Obj-C in Xcode) relies on some of these executables. Unfortunately, the executables must be installed manually - I have to check for them and then prompt the user to install them.

My code is:

NSTask *task = [[NSTask alloc] init];
[task setLaunchPath:@"/usr/bin/executable"];

NSPipe *pipe = [NSPipe pipe];
[task setStandardOutput:pipe];
NSFileHandle *file = [pipe fileHandleForReading];

[task setArguments:[NSArray arrayWithObjects:sender, target, nil]];
[task launch];

I was wondering if it possible to copy the executable inside my app somewhere and then call it from there. That way, users wouldn't have to go through getting it for themselves.

And would it be allowed by the Mac App Store?

Upvotes: 4

Views: 3042

Answers (5)

PruitIgoe
PruitIgoe

Reputation: 6384

I updated hbowie's answer to return the value received from the shell as a string:

func safeShell(_ strExecutable:String, _ arrArguments:Array<String>) throws -> String {
        
        let task = Process()
        let pipe = Pipe()
        let bundle = Bundle.main
        let execURL = bundle.url(forResource: strExecutable, withExtension: nil)
        
        //no point in going on if we can't find the program
        guard execURL != nil else {
            print("\(strExecutable) executable could not be found!")
            return ""
        }
        
        task.executableURL = execURL!
        task.arguments = arrArguments
        task.standardOutput = pipe
        task.standardError = pipe
        //task.arguments = ["-c", command]
        //task.executableURL = URL(fileURLWithPath: "/bin/zsh") //<--updated

        do {
            try task.run() //<--updated
        }
        catch{ throw error }
        
        let data = pipe.fileHandleForReading.readDataToEndOfFile()
        let output = String(data: data, encoding: .utf8)!
        
        return output
    }
    
}

Use example:

let strOutput = try myAppManager.safeShell("pandoc", ["--version"])
print(strOutput)

Returns:

pandoc 2.16.2
Compiled with pandoc-types 1.22.1, texmath 0.12.3.2, skylighting 0.12.1,
citeproc 0.6, ipynb 0.1.0.2
User data directory: /Users/stevesuranie/Library/Containers/MartianTribe.MD2HTML/Data/.local/share/pandoc
Copyright (C) 2006-2021 John MacFarlane. Web:  https://pandoc.org
This is free software; see the source for copying conditions. There is no
warranty, not even for merchantability or fitness for a particular purpose.

Upvotes: 0

hbowie
hbowie

Reputation: 21

Just to update and clarify, I found the following to work in Xcode 11 and Swift 5.

First, dragged and dropped the desired executable (pandoc, in my case) into my Xcode project. I placed this in a group/folder named 'bin'.

Made sure to select the desired Targets in which I wished to have the executable included!

Then used the following code:

let task = Process()
let bundle = Bundle.main
let execURL = bundle.url(forResource: "pandoc", withExtension: nil)
guard execURL != nil else {
    print("Pandoc executable could not be found!")
    return 
}
task.executableURL = execURL!
task.arguments = ["--version"]
do {
    try task.run()
    print("Pandoc executed successfully!")
} catch {
    print("Error running Pandoc: \(error)")
}

Upvotes: 1

Maiaux
Maiaux

Reputation: 975

In case you wonder, in Swift 3 you can do this:

let bundle = Bundle.main
let task = Process()
let path = bundle.path(forResource: "executableName", ofType: "")
task.launchPath = path

Upvotes: 1

user43633
user43633

Reputation: 319

In Swift, you can do this:

let bundle = NSbundle.mainBundle()
let task = NSTask()

task.launchPath = bundle.pathForResource("executableName", ofType: "")

Make sure the bundled executable is in the top level of the directory tree, not in a sub-directory like /lib, or it might not work.

Upvotes: 0

citruspi
citruspi

Reputation: 6891

Ok, so I set out to find the answer for myself by playing around. And, I'm happy to say that I figured it out!

So, just copy the executable into the Xcode project wherever you want. Then, change

[task setLaunchPath:@"/usr/bin/executable"];

to

[task setLaunchPath:[[NSBundle mainBundle] pathForResource:@"executable" ofType:@""]];

And ta da!

Upvotes: 0

Related Questions