macOS & iOS Build Automation

Shipping Mac & iOS Apps Without Ever Opening Xcode

Shipping Mac & iOS Apps Without Ever Opening Xcode

You’ve probably seen the same scene a dozen times: someone says “Xcode is awful,” then immediately shows a screenshot of Xcode’s UI… and you realize they were opening the toolbox drawer instead of using the tools inside the drawer.

Here’s the trick: Xcode is mostly a collection of command-line utilities plus a build system. You do need Xcode installed, but you don’t need to open Xcode every time you build, sign, notarize, or deploy. Why keep staring at an IDE when the real work can run headless in your terminal?

This post walks through a practical workflow for building and shipping Mac and iOS apps without opening Xcode—starting with the one-time “human” setup steps, then moving into a repeatable command/script chain.


The mental model: Xcode is a bundle of CLI tools

When people say “Xcode,” they often mean the big GUI app. But for shipping software, the important part is that Xcode includes the command-line tools you can run from Terminal—most notably xcodebuild and utilities invoked via xcrun.

A few key ideas to lock in:

  • xcodebuild is the build command that compiles your project, creates archives, and performs exports. (An “archive” is a packaged build product meant for later export/signing.)
  • xcrun is a launcher that finds the right Xcode tool in the currently selected Xcode installation.
  • Xcode projects are just files. On macOS, Xcode presents a project as a “file,” but the underlying .xcodeproj is a folder with many settings and references.

Once you accept that, the rest becomes pipeline design.


Step 0: Install Xcode (you won’t escape it)

You need Xcode installed because the full iOS/macOS toolchain lives inside Xcode.app. Then make sure your system points at that Xcode.

Check where the active developer directory is:

xcode-select -p

If you see something like:

/Applications/Xcode.app/Contents/Developer

you’re good.

If you accidentally have only the standalone Command Line Tools selected (location typically under /Library/Developer/CommandLineTools), switch to the full Xcode toolchain:

sudo xcode-select -s /Applications/Xcode.app/Contents/Developer

That “Command Line Tools” package is handy (clang, git, a few basics), but it’s not the complete iOS/macOS build environment.


Step 1: Use XcodeGen so you never edit .xcodeproj again

Xcode can generate a .xcodeproj folder that macOS pretends is a single file. That folder contains build settings and references, and Xcode likes to modify it during workflows.

The result: merge conflicts, mysterious diffs, and the feeling that the project is haunted.

XcodeGen is a tool that treats project generation as a repeatable step:

  • You write a project.yml (a YAML spec file).
  • XcodeGen regenerates the .xcodeproj from that spec.
  • Only the YAML lives in git; the generated .xcodeproj can be ignored.

So your day-to-day “project changes” are actually “spec changes.” The .xcodeproj becomes build output.


Step 2: Do the only GUI steps once (certs + passwords)

There’s a set of setup tasks that are intentionally interactive. After you do them, builds and deployments can be fully headless.

2a) Accept the Xcode license and finish first-launch setup

You can kick these off from Terminal:

sudo xcodebuild -license accept
sudo xcodebuild -runFirstLaunch

This is the part that replaces “open Xcode and click a few dialogs.”

2b) Add your Apple Developer account

Eventually, signing and distribution require an Apple Developer Program identity. For shipping outside the Mac App Store, you specifically need Developer ID.

Developer ID is Apple’s system for verifying that a downloaded app comes from a trusted developer; it also ties into notarization.

2c) Create a “Developer ID Application” certificate

Create a Developer ID Application certificate. This certificate is used to sign the shipped .app bundle so Gatekeeper can trust it.

Important distinction (people mix these up all the time):

  • Apple Development identities are for development and local testing.
  • Developer ID Application identities are for distributing notarized apps that should survive Gatekeeper checks on other Macs.

When you create the certificate in Xcode, Apple installs the private key into your login keychain. That private key is what xcodebuild uses when signing.

Keychain is macOS’s secure credential store. A keychain profile is a named configuration the notarization tool can use later.

Because this private key is not easily re-downloaded, treat it like you’d treat your only copy of a signing key:
- don’t delete it
- back up your keychain (or at least export the certificate + key securely)

2d) Store a notarization credential once in Terminal

Notarization is Apple’s scanning process for your signed software. The output is a ticket that tells Gatekeeper the app passed Apple’s automated checks.

Apple’s modern CLI flow is centered on notarytool invoked via xcrun.

You create an interactive keychain-backed credential profile:

xcrun notarytool store-credentials App-Name \

  --apple-id "[email protected]" --team-id YOUR-TEAM-ID

When prompted, enter an app-specific password (not your Apple ID password). These passwords can go stale if you change your Apple ID password.

Confirm the stored profile:

xcrun notarytool history --keychain-profile App-Name

2e) Keep signing configuration out of git

Create a Local.xcconfig (not committed) that stores things like:

  • Bundle prefix / bundle identifier components
  • DEVELOPMENT_TEAM (your Team ID)

A .xcconfig file is an Xcode configuration file. XcodeGen and xcodebuild read settings from these files, but you keep the secrets local.


Step 3: The day-to-day pipeline runs headless

Here’s the chain for a typical Mac release outside the Mac App Store:

  1. Build an archive with xcodebuild
  2. Export the distributable package with -exportArchive and an ExportOptions.plist
  3. Sign using Developer ID (handled during export/signing configuration)
  4. Submit to notarization with xcrun notarytool --wait
  5. Attach the notarization ticket using xcrun stapler
  6. Verify with spctl
  7. Copy/install into /Applications (optional for your own dev machine)

You can wrap the whole thing in one script, like scripts/release.sh.

A key principle: the script should fail loudly. That means using set -euo pipefail at the top so errors don’t get swallowed.


A release script skeleton (Mac)

This is not a drop-in “magic script,” but it shows the moving parts and where you plug in your project-specific values.

#!/usr/bin/env bash
set -euo pipefail

APP_NAME="MyApp"
SCHEME="MyApp"
CONFIGURATION="Release"

# 1) Archive
xcodebuild \
  -scheme "$SCHEME" \
  -configuration "$CONFIGURATION" \
  -destination 'generic/platform=macOS' \
  -archivePath "build/${APP_NAME}.xcarchive" \
  archive

# 2) Export
xcodebuild \
  -exportArchive \
  -archivePath "build/${APP_NAME}.xcarchive" \
  -exportPath "build/dist" \
  -exportOptionsPlist "export/ExportOptions.plist"

# At this point you’ll have something like build/dist/${APP_NAME}.app

# 3) Notarize
# (notarytool reads credentials from the keychain profile you stored earlier)
xcrun notarytool submit "build/dist/${APP_NAME}.app" \
  --keychain-profile "${APP_NAME}" \
  --wait

# 4) Staple ticket
# stapler attaches the notarization ticket to the app bundle
xcrun stapler staple "build/dist/${APP_NAME}.app"

# 5) Verify Gatekeeper assessment
spctl --assess --type exec "build/dist/${APP_NAME}.app"

# 6) Install locally (optional)
cp -R "build/dist/${APP_NAME}.app" "/Applications/${APP_NAME}.app"

Where the signing decisions live

The heavy lifting for the “right identity” and “right signing mode” is usually controlled via ExportOptions.plist.

That plist is an export configuration file—passed to xcodebuild -exportArchive—that tells Xcode how to package and sign the output (including Developer ID signing style).

If the export/sign phase is wrong, notarization will fail in ways that feel unrelated. So treat ExportOptions.plist as part of your build system, not a one-off.


iOS without opening Xcode: build + install from the command line

For iOS, “shipping” often means producing an .ipa (iOS App Store Package file) or an .app for device installation.

The pattern stays consistent:

  • Use xcodebuild to archive and export
  • Install onto a connected device with the Xcode-provided device management tool

Apple ships devicectl inside Xcode. devicectl is a command-line tool for interacting with physical devices.

Because command details can evolve across Xcode releases, the reliable move is:

xcrun devicectl help

From there, your release flow for a debug-to-device loop is typically:

  1. xcodebuild export to an installable artifact
  2. xcrun devicectl to install to a specific device
  3. optionally launch the app

For local iteration, you can also use simulators with tools like simctl, but the “no Xcode GUI” principle is the same.


Keeping secrets out of the repo (and out of scripts)

One reason Xcode workflows feel “manual” is that they encourage you to park credentials in places that don’t belong in git.

A cleaner approach:

  • Notarization secrets live in your keychain via notarytool store-credentials.
  • Signing identity selection is discovered by xcodebuild using your login keychain.
  • Team IDs / bundle prefixes live in Local.xcconfig (ignored by git).
  • Export rules live in ExportOptions.plist (committed), but keep account-specific values in local config.

The result is a repo that can be shared without “credential archaeology.”


The “vibe coding” payoff

So where does this leave the original complaint—why would anyone open Xcode at all?

Because for a long time, the most visible tutorials were UI-driven. But the real work for shipping lives in:

  • a build archive/export chain (xcodebuild)
  • a notarization chain (xcrun notarytool)
  • a ticket attachment chain (xcrun stapler)
  • an optional device install chain (xcrun devicectl)

The GUI becomes a one-time setup environment for certificates and passwords. After that, your pipeline does what it should: build, sign, notarize, staple, and install without drama.


Conclusion

Opening Xcode isn’t required to ship Mac and iOS apps. Xcode is the installer and the toolbox; your real “release process” is a script-driven pipeline.

Once the initial certificate and notarization credential work is done, building and deploying can become fully headless. And that’s the best kind of progress: the boring parts stop stealing attention, and the product work gets the screen time.

ahsan

ahsan

Hello! I am Mr Ahsan, the writer of the Website. I am from Netherland. I like to write about technology and the news around it.

Comments (0)

No comments yet. Be the first to respond!

Leave a Comment

Your comment will be visible after review.