Skip to main content

macOS App Bundles

Create macOS .app bundles from your compiled binaries

Anodizer can package your macOS binaries into .app bundles with a proper directory structure, Info.plist, and optional icon.

Classification

Packager — creates macOS .app bundle directories from Darwin binaries. Required: not a publisher; runs only on macOS targets.

Minimal config

crates:
  - name: myapp
    app_bundles:
      - bundle: com.myorg.myapp

Full config reference

crates:
  - name: myapp
    app_bundles:
      - bundle: com.example.myapp    # required; reverse-DNS bundle identifier
        id: ""                        # optional; unique identifier
        ids: []                       # optional; filter by build IDs
        name: ""                      # optional; output .app name template
        icon: ""                      # optional; path to .icns icon file (template)
        min_os_version: "10.13"       # optional; LSMinimumSystemVersion in Info.plist
        extra_files: []               # optional; additional files to include in Resources
        templated_extra_files: []     # optional; template-rendered extra files
        mod_timestamp: ""             # optional; override mtime for reproducible builds
        replace: false                # optional; remove source archives, keep app bundle
        disable: false                # optional; bool or template string

Authentication

Not applicable — app bundle creation is a local build step with no external service calls.

Common gotchas

  • macOS only: the stage only processes Darwin binary artifacts. Non-macOS targets are ignored.
  • .app extension: auto-appended if not present in name. The directory must end in .app for macOS to treat it as an application bundle, so unlike .pkg / .msi / .nsis / .dmg (where the user controls the extension) anodizer always ensures the suffix.
  • icon must be .icns: other formats (.png, .svg) are not accepted by macOS for the CFBundleIconFile key.
  • bundle: has no default: a missing bundle: is a hard error. Anodizer does not invent an identifier under com.anodizer.* because that namespace is not owned by you — using it would conflict with App Store submission and notarization.

Republish / update behavior

Not applicable — this is a local packaging stage, not a publisher.

Signing and Notarization

An .app bundle produced by this stage is unsigned and un-notarized. macOS Gatekeeper will reject it with the "cannot be opened because the developer cannot be verified" dialog unless you also run the notarize.macos_native stage downstream.

Wire it up like this:

notarize:
  macos_native:
    sign:
      certificate_name: "Developer ID Application: My Org (TEAMID)"
      entitlements: "macos/entitlements.plist"
      options: ["runtime"]   # enables hardened runtime
    notarize:
      apple_id: "you@example.com"
      team_id: "TEAMID"
      key: "{{ Env.AC_API_KEY }}"
      key_id: "{{ Env.AC_API_KEY_ID }}"
      issuer: "{{ Env.AC_API_ISSUER }}"

entitlements and options: [runtime] (hardened runtime) are not knobs on app_bundles: itself — they live on the notarize stage, which signs the generated .app and then notarizes it through Apple. See the notarize docs for the full set of options.

App bundle config fields

FieldTypeDefaultDescription
idstringnoneUnique identifier for this config
idslistall buildsFilter by build IDs
namestring{{ ProjectName }}_{{ Arch }}.appOutput .app bundle name (template)
iconstringnonePath to .icns icon file (template)
bundlestringrequiredBundle identifier in reverse-DNS notation
min_os_versionstring10.13Written to LSMinimumSystemVersion in Info.plist
extra_fileslistnoneAdditional files to include in Resources
templated_extra_fileslistnoneTemplate-rendered extra files
mod_timestampstringnoneOverride mtime for reproducible builds (template)
replaceboolfalseRemove source archives, keeping only the app bundle (anodizer-additive — not in GoReleaser Pro's app_bundles:)
disablestring/boolnoneDisable this config (bool or template)

extra_files shape

Each entry is either a glob string or a detailed object:

extra_files:
  - LICENSE                          # glob, copies into Contents/Resources/
  - src: "docs/*.txt"                # glob with explicit destination
    dst: "Contents/SharedSupport"
  - src: "assets/manual.pdf"         # per-file mtime for reproducibility
    info:
      mtime: "{{ CommitDate }}"

The info block supports mtime (RFC3339 or SOURCE_DATE_EPOCH integer seconds). When both extra_files[].info.mtime and the bundle-level mod_timestamp are set, the per-file value wins because it is the more specific knob.

The info.mode, info.owner, and info.group fields are accepted by the schema but not used for app bundles (macOS reads file metadata from the filesystem, not from a manifest). Anodizer emits a warning when any of them are set so the no-op is visible.

Glob + dst rename caveat: when dst is set, the src glob must match exactly one file. A multi-match glob paired with a constant dst would silently overwrite every earlier copy with the last one — anodizer fails pre-flight instead. Use a directory dst (no rename) or narrow the glob.

Overriding the generated Info.plist

The auto-generated Info.plist is written first, then extra_files are copied in. A user-supplied entry whose dst resolves to Contents/Info.plist therefore overwrites the generated file:

crates:
  - name: myapp
    app_bundles:
      - bundle: com.example.myapp
        extra_files:
          - src: macos/Info.plist
            dst: Contents/Info.plist

Use this when you need keys anodizer does not emit (NSCameraUsageDescription, CFBundleURLTypes, UIBackgroundModes, custom document types, etc.).

Generated structure

The stage creates a standard macOS .app directory:

MyApp.app/
  Contents/
    Info.plist
    MacOS/
      myapp          (binary, chmod 755)
    Resources/
      icon.icns      (if configured)
      ...            (extra files)

Info.plist

Anodizer auto-generates Info.plist with:

KeyValue
CFBundleExecutableBinary name
CFBundleIdentifierbundle field value
CFBundleNameProject name
CFBundleVersionVersion
CFBundleShortVersionStringVersion
CFBundlePackageTypeAPPL
CFBundleInfoDictionaryVersion6.0
NSHighResolutionCapabletrue
LSMinimumSystemVersionmin_os_version (default 10.13)
CFBundleIconFileIcon filename (when icon is set)

If you need a key not in this list, supply your own Info.plist via the override pattern.

Defaults vs GoReleaser

KnobGoReleaser ProAnodizer
name default{{ ProjectName }}{{ ProjectName }}_{{ Arch }}.app
bundle defaultnone (user-supplied)none (hard error if unset)
replace on app_bundles:not exposedexposed (anodizer-additive)
min_os_versionnot exposedexposed (anodizer-additive)

The _{{ Arch }} suffix on the default name is deliberate: a multi-arch build (arm64 + amd64) under GoReleaser's bare {{ ProjectName }} would produce two MyApp.app directories whose second write overwrites the first. Anodizer's default keeps both artifacts addressable. If you are importing a GoReleaser config and want the original behavior, set name explicitly.

Behavior

  • Only processes macOS (darwin) binary artifacts
  • The .app extension is auto-appended if not present in name
  • Output is placed in dist/macos/
  • mod_timestamp is applied recursively to the entire .app tree
  • Per-file extra_files[].info.mtime overrides the bundle-level mod_timestamp for the file it points at
  • Skippable with --skip appbundle

Full example

crates:
  - name: myapp
    app_bundles:
      - name: "MyApp_{{ Arch }}.app"
        bundle: com.myorg.myapp
        icon: assets/icon.icns
        min_os_version: "12.0"
        extra_files:
          - LICENSE
          - README.md
          - src: macos/Info.plist
            dst: Contents/Info.plist
          - src: assets/manual.pdf
            info:
              mtime: "{{ CommitDate }}"
        mod_timestamp: "{{ CommitTimestamp }}"
        replace: true