Skip to content
C.W.K.
Stream
Lesson 03 of 06 · published

An App Is a Folder

~14 min · platform-map, bundle, info-plist, identity

Level 0Bundle Opener
0 XP0/81 lessons0/17 achievements
0/100 XP to next level100 XP to go0% complete
"Finder shows you an icon. The system sees a directory with a contract inside."

Open One Up

Every .app is a directory the Finder draws as a single icon. On the Mac the layout is nested under Contents/: an Info.plist describing the app, a MacOS/ folder holding the executable, Resources/ for icons and assets, and _CodeSignature/ sealing all of it. Helpers and extensions nest further inside (Frameworks/, PlugIns/, Library/LaunchAgents/). An iOS app bundle is flat: the executable, Info.plist and Assets.car sit at the top, with a share extension under PlugIns/, a watch app under Watch/, and an embedded.mobileprovision on a development install. An archive exported for TestFlight carries a distribution profile as well.

You can open any bundle with ls, and you should — once, on a system app, before you write your own build script. Nothing in this quest will make more sense than seeing that the thing you ship is a folder with rules.

Info.plist Is the Contract

  • CFBundleIdentifier — the app's name to the system, in reverse-DNS form. TCC permissions, Keychain items, LaunchServices registrations, App Groups and the App Store Connect record all hang off it.
  • CFBundleExecutable — which file in MacOS/ actually runs. It does not have to match the bundle's display name, and scripts that pkill by the wrong one match nothing.
  • CFBundleShortVersionString — the marketing version people see (1.2.0).
  • CFBundleVersion — the build number. TestFlight refuses to accept the same one twice.
  • LSMinimumSystemVersion (macOS) / MinimumOSVersion (iOS) — the deployment target, written into the artifact.
  • LSUIElement — true for a menu-bar utility with no Dock icon.

The Signature Seals the Folder

_CodeSignature/CodeResources lists a hash for every file in the bundle. Edit the Info.plist after signing, drop in an extra image, or let Finder attach extended attributes, and codesign --verify reports that a sealed resource is missing or invalid. That is why every build script in this family finishes the bundle completely — version stamped, icon copied, attributes cleared — and then signs, and then verifies.

A Bare Executable Is Not a Bundle

swift build produces a plain binary under .build/. It runs, and Bundle.main even answers — but it answers with the directory containing the binary. The family measured this after a code comment claimed Bundle.main.resourceURL is nil for a SwiftPM executable: it is not nil, it points at .build/arm64-apple-macosx/debug. So a non-nil resource URL proves nothing about whether you are packaged. Check for the file you actually need.

Code

Look inside a real app before you build your own·bash
APP=/System/Applications/Calculator.app
ls "$APP/Contents"                                   # Info.plist  MacOS  Resources  _CodeSignature ...
plutil -p "$APP/Contents/Info.plist" | head -20
/usr/libexec/PlistBuddy -c 'Print :CFBundleIdentifier' "$APP/Contents/Info.plist"
/usr/libexec/PlistBuddy -c 'Print :CFBundleExecutable' "$APP/Contents/Info.plist"
codesign --verify --deep --strict --verbose=2 "$APP"  # valid on disk, satisfies its designated requirement
What Bundle.main says from swift run versus from a packaged .app·swift
// Sources/Spark/Spark.swift — run with `swift run`, then again from inside a built .app
import Foundation

@main
struct Spark {
    static func main() {
        print("bundlePath   :", Bundle.main.bundlePath)
        print("resourceURL  :", Bundle.main.resourceURL?.path ?? "nil")
        print("identifier   :", Bundle.main.bundleIdentifier ?? "nil")

        // swift run    -> bundlePath and resourceURL are .build/arm64-apple-macosx/debug, identifier nil
        // packaged app -> Spark.app and Spark.app/Contents/Resources, identifier com.example.spark

        // So never branch on resourceURL != nil to mean "packaged". Look for the content:
        let packaged = Bundle.main.url(forResource: "AppIcon", withExtension: "icns") != nil
        print("packaged     :", packaged)
    }
}

External links

Exercise

Create an empty Spark package with swift package init --type executable --name Spark, replace the contents of Sources/Spark/Spark.swift with the Bundle.main program above, including its import Foundation, which the template lacks, and run it with swift run. Record the three values. Then inspect one menu-bar app installed on your Mac: find its CFBundleIdentifier, CFBundleExecutable, CFBundleVersion and whether LSUIElement is set. Write down which of those values you would never be allowed to change after shipping, and why.
Hint
plutil -p <App>/Contents/Info.plist prints every key. The identifier is the one whose change silently resets permissions and Keychain access; the build number is the one that must only ever go up.

Progress

Progress is local-only — sign in to sync across devices.
Spotted a bug or have feedback on this page?Report an Issue

Comments 0

🔔 Reply notifications (sign in)
Sign inPlease sign in to comment.

No comments yet — be the first.