Building an app with both GUI and Command Line versions

I use CMake for my builds not the XCode GUI.

I want to be able to build a single .app that contains both the GUI version and the command line version. I have seen products that ship both as part of the same .app (e.g. CMake) so this is clearly entirely possible, the 64k question is HOW?

Right now I am building them as separate apps - lets call them DSS and DSSCL (*).

The code base for each is in its own directory - each with its own CMakelists.txt file.

My initial thoughts are to change the build for DSSCL so it doesn't create a bundle and then simply copy the DSSCL command and related .qm files into DSS.app/.../MacOS.

However that's likely enough totally wrong, so how should I handle this please.

As much detail as possible please, I am very new to macOS development -please don't assume knowledge of stuff that's second nature to you

Many thanks David

(*) Strictly they are DeepSkyStacker and DeepSkyStackerCL

Answered by DTS Engineer in 837590022
I've placed the CL executable in the MacOS directory I expect that users will place a symlink to it in /usr/local/bin/

OK.

Unlike Etresoft, I’m kinda neutral on that approach. At a technical level it works just fine, and at a UI level I’ve certainly used products that work that we. We even have explicit support for that model in sandboxed apps [1].

Speaking of the sandbox, you didn’t answer my App Sandbox question. I’m gonna presume that your app is not sandboxed and thus you have no plans to distribute it via the Mac App Store. If that’s not right, lemme know because it changes the following answer.

If you want to distribute an app with a command-line tool that users can run from Terminal, this structure is fine:

MyApp.app/
    Contents/
        Info.plist
        MacOS/
            MyApp           -- the app's main executable
            MyTool
        Resources/
            …
        Frameworks/
            …

The only tricky thing about this approach is that, when running in the tool, +[NSBundle mainBundle] won’t return the app’s bundle. Rather, it’ll point to the tool itself. That’s because your tool is not the main executable of the app’s bundled, as recorded in the Info.plist.

There are two standard ways to deal with that:

  • Put all the interesting code and its associated resources in framework in Contents/Frameworks. There’s a standard mechanism for framework code to access framework resources.

  • Have the tool use +[NSBundle mainBundle] to find its own path, then walk up the file system hierarchy and create a bundle for the app by calling bundleWithURL:.

If you do use a framework, use an rpath-relative install name, as explained in Dynamic Library Standard Setup for Apps.

Finally, when it comes to code signing, keep in mind that the tool, any frameworks, and the app itself are all separate code items. Creating distribution-signed code for macOS discusses the signing process for things like this in detail. Part of that process is working out the order in which to sign items, whether an item is bundled code or not, and whether it’s a main executable or not. Here’s a quick summary of that for the structure I’ve outlined above:

#   Item        Bundled?    Main Executable?
-   ----        --------    ----------------
1   framework   yes         no
2   tool        no          yes
3   app         yes         yes

Share and Enjoy

Quinn “The Eskimo!” @ Developer Technical Support @ Apple
let myEmail = "eskimo" + "1" + "@" + "apple.com"

[1] The privileged file operation feature in NSWorkspace.

Can't comment on any CMake issues. I've used CMake enough to consider it worthwhile to burn a whole day just to port the build environment to Xcode. That makes so many problems go away like magic.

However, in this case, CMake seem irrelevant. You can construct an app bundle with as many executables as you want. If you put them in the wrong locations, that could cause problems with code signing, notarization, and/or App Store submission. But otherwise, there's literally no limit.

But why even do that at all? It's not hard to make an app that runs totally on the command line, if you provide the correct commands. I have an app that does that. I just have to provide a "--headless" argument to trigger command-line operation, or "-h" to show command-line help. If I don't have either of those two arguments, then I let the normal app launch process run.

The command line version of the app is structured quite differently from the GUI and is already written and working.

I've just spent weeks porting from Visual Studio/msbuild to CMake so we can build for Win/Linux/Mac using ONE tool. So I really don't wish to convert the build to XCode.

Should I put the command line version into the SharedSupport directory of the app rather than the MacOS one?

The command line version of the app is structured quite differently from the GUI and is already written and working.

The entry point for both is the same - the "main" function. It's trivial to check the command line arguments at that point and do either one or the other. Here's my main() function:

#import <Cocoa/Cocoa.h>
#import "HeadlessController.h"

int main(int argc, char * argv[])
  {
  if([HeadlessController isHeadless: argc args: argv])
    if(NSApplicationLoad())
      {
      @autoreleasepool
        {
        HeadlessController * controller = [HeadlessController new];
        
        [controller run: argc args: argv];
        
        [controller release];
        }
      
      return 0;
      }
    
  return NSApplicationMain(argc, (const char **) argv);
  }

The only problem would be if both sets of source code share some symbol names. Ideally, most of the code would be UI-agnostic. The command line version would call it directly, maybe with a text-based progress. The GUI version would call the same code with a Cocoa UI.

I've just spent weeks porting from Visual Studio/msbuild to CMake so we can build for Win/Linux/Mac using ONE tool. So I really don't wish to convert the build to XCode.

That's unfortunate. Just remember that Xcode people don't speak CMake and vice-versa. When you have problems with CMake, you'll be on your own. These two camps really don't get along.

I understand you're not willing to switch at this point. But do keep it in mind if you have problems in the future. I've ported a lot of open source projects into Xcode from autotools, CMake, and some even more complex Google build systems. When targeting Apple platforms, Xcode make so many problems go away entirely. Don't spend days or weeks on a build problem when you can just make it go away in a single day.

Should I put the command line version into the SharedSupport directory of the app rather than the MacOS one?

I think it would be more appropriate for the "Helpers" directory. Here is Apple's current documentation on the topic. I do see a "SharedSupport" directory referenced in Apple's archived documentation but the description of ("non-critical resources that do not impact the ability of the application to run") seems to preclude executables.

It's trivial to check the command line arguments at that point and do either one or the other.

True, but there are potential drawbacks. For example, if you’re expecting the user to run your command-line tool from a non-GUI context then the act of linking to GUI framework can get you into trouble.


perdrix52, There are two parts to your question:

  • What should your bundle structure look like?

  • How do you get there with CMake?

I can’t help you with the second bit; I don’t use that build system. My advice is to first nail down what you want, and then ask your CMake question via the support channel for that tool.

As to the structure you should be building, I have two questions up front:

  • Does you app have the App Sandbox enabled? Typically folks only enable App Sandbox to ship on the Mac App Store. It’s optional in other contexts, but there are good reasons to enable it [1].

  • Do you expect your users to always run your command-line tool from Terminal? Or do you want them to be able to run it in a non-GUI context, for example, when logged in over SSH?

Share and Enjoy

Quinn “The Eskimo!” @ Developer Technical Support @ Apple
let myEmail = "eskimo" + "1" + "@" + "apple.com"

[1] See The Case for Sandboxing a Directly Distributed App

I've placed the CL executable in the MacOS directory I expect that users will place a symlink to it in /usr/local/bin/

The CL program doesn't have any GUI stuff...

I hope/expect users will be able to run from either Terminal or an SSH session. I realise they'll need to create an .zshenv looking a bit like this:

# remove duplicate entries from $PATH
# zsh uses $path array along with $PATH
typeset -U PATH path

pathadd () {
       if [ "$2" = "after" ] ; then
          PATH=$PATH:$1
       else
          PATH=$1:$PATH
       fi
}
pathadd /usr/local/bin
pathadd /home/amonra/bin

Cheers, David

Maybe take a step back. Perhaps the answer is social rather than technical. Mac users aren't like Windows or Linux users. It's unlikely they would ever discover a command line tool regardless of whether it's integrated or where it lies in the bundle.

There are Mac command-line tool users. If you want to find them, go to where they live. On the Mac in 2025, for better or worse, that's Homebrew. I don't know if your code is open source or not. But Homebrew does have a system for installing binaries. Otherwise, your market pool for Mac command line users who aren't running Homebrew is essentially just me. 😄

This way, users don't have to worry about creating a .zshenv file. Homebrew handles all of that.

I've placed the CL executable in the MacOS directory I expect that users will place a symlink to it in /usr/local/bin/

OK.

Unlike Etresoft, I’m kinda neutral on that approach. At a technical level it works just fine, and at a UI level I’ve certainly used products that work that we. We even have explicit support for that model in sandboxed apps [1].

Speaking of the sandbox, you didn’t answer my App Sandbox question. I’m gonna presume that your app is not sandboxed and thus you have no plans to distribute it via the Mac App Store. If that’s not right, lemme know because it changes the following answer.

If you want to distribute an app with a command-line tool that users can run from Terminal, this structure is fine:

MyApp.app/
    Contents/
        Info.plist
        MacOS/
            MyApp           -- the app's main executable
            MyTool
        Resources/
            …
        Frameworks/
            …

The only tricky thing about this approach is that, when running in the tool, +[NSBundle mainBundle] won’t return the app’s bundle. Rather, it’ll point to the tool itself. That’s because your tool is not the main executable of the app’s bundled, as recorded in the Info.plist.

There are two standard ways to deal with that:

  • Put all the interesting code and its associated resources in framework in Contents/Frameworks. There’s a standard mechanism for framework code to access framework resources.

  • Have the tool use +[NSBundle mainBundle] to find its own path, then walk up the file system hierarchy and create a bundle for the app by calling bundleWithURL:.

If you do use a framework, use an rpath-relative install name, as explained in Dynamic Library Standard Setup for Apps.

Finally, when it comes to code signing, keep in mind that the tool, any frameworks, and the app itself are all separate code items. Creating distribution-signed code for macOS discusses the signing process for things like this in detail. Part of that process is working out the order in which to sign items, whether an item is bundled code or not, and whether it’s a main executable or not. Here’s a quick summary of that for the structure I’ve outlined above:

#   Item        Bundled?    Main Executable?
-   ----        --------    ----------------
1   framework   yes         no
2   tool        no          yes
3   app         yes         yes

Share and Enjoy

Quinn “The Eskimo!” @ Developer Technical Support @ Apple
let myEmail = "eskimo" + "1" + "@" + "apple.com"

[1] The privileged file operation feature in NSWorkspace.

Building an app with both GUI and Command Line versions
 
 
Q