appbundle

package module
v0.4.0 Latest Latest
Warning

This package is not in the latest version of its module.

Go to latest
Published: Sep 4, 2026 License: BSD-3-Clause Imports: 11 Imported by: 0

README

go-macos/appbundle

The .app directory a macOS program lives in: whether the running process is inside one, and how to assemble one around an executable. Pure Go, CGO_ENABLED=0, no AppKit — path and file work only, so it builds and is tested on every platform, which is what makes a bundle assembler useful in a cross-compiling build.

Assembling on Windows. The executable is written with mode 0o755, but Windows keeps no execute bit: os.Chmod there uses only the 0o200 (owner-writable) bit to set or clear the read-only attribute, and Stat synthesises the mode back as 0444 or 0666. A bundle assembled on a Windows host therefore arrives on macOS without its executable bit, and whatever carries it across — a tar, a zip with Unix extras, an scp — has to restore it. Assembling on macOS or Linux is unaffected.

if b, ok := appbundle.Running(); ok {
    // Launched from the Finder: no arguments, and a menu bar to live in.
}

_, err := appbundle.Build(appbundle.Spec{
    Dir: "dist", Name: "godl", Identifier: "io.github.go-downloader.godl",
    Version: "0.1.0", Executable: "build/godl",
    Accessory: true,          // LSUIElement: a menu-bar item, no dock tile
    MinimumSystem: "11.0",
    Icon: icns,               // Contents/Resources/godl.icns + CFBundleIconFile
})

ICNS packs PNGs into the icon file macOS reads, in pure Go — replacing a shell out to iconutil and sips:

icns, err := appbundle.ICNS(png16, png32, png512)

Each image goes in whole, under the code for its own size, so the caller chooses which sizes to ship: the system picks the nearest it has, and one good large image beats seven resampled from it.

Why a bundle at all

A bare executable is not an application on this system. AppKit reads what a program is from the bundle around it, so a program that wants a menu-bar item, a dock tile, a name in the menu bar, notification permission or a place in Login Items has to be in one.

Asked for from outside a bundle, a status item is asked for by nobody: it never appears, and the process ends without complaining. That silence is the whole reason this package exists — it is not an error anybody sees, it is a menu bar that stays empty.

Accessory sets LSUIElement, which is what separates a status-item program from an ordinary one: a menu-bar item, no dock tile, no menu of its own.

Notes

  • Of asks about a path, not about the machine, so it answers the same way everywhere. A build on another platform can reason about the bundle it is assembling.
  • Build replaces what was there. A bundle assembled over an older one keeps files nothing refers to any more, and those are the ones that go stale without anybody noticing.
  • PkgInfo goes in unasked. Eight bytes the Finder has read since long before Info.plist existed: nothing fails without it and everything is very slightly wrong, which surfaces years later as an application that will not associate with its own documents.
  • A program that cannot locate itself is treated as not bundled: better a command-line program than an application acting on a guess.

BSD-3-Clause.

Documentation

Overview

Package appbundle is the .app directory a macOS program lives in: whether the running process is inside one, and how to assemble one around an executable.

A bare executable is not an application on this system. AppKit reads what a program is from the bundle around it, so a program that wants a menu-bar item, a dock tile, a name in the menu bar, notification permission or a place in Login Items has to be in one. Asked for from outside a bundle, a status item is asked for by nobody: it never appears, and the process ends without complaining.

Everything here is path and file work — no AppKit, no cgo — so it builds and is tested on every platform, which is what makes a bundle assembler useful in a cross-compiling build.

Index

Constants

This section is empty.

Variables

This section is empty.

Functions

func ICNS

func ICNS(images ...[]byte) ([]byte, error)

ICNS packs PNGs into the icon file macOS reads, replacing a shell out to iconutil and sips.

Each image goes in whole, under the code for its own size, so the caller chooses which sizes to ship: the system picks the nearest it has, and one good large image beats seven resampled from it.

Types

type Bundle

type Bundle struct {
	// Path is the .app directory itself.
	Path string
	// Name is what it is called, without the .app.
	Name string
}

Bundle is a .app directory.

func Build

func Build(s Spec) (Bundle, error)

Build assembles the bundle and reports where it put it.

It replaces whatever was there: a bundle assembled over the top of an older one keeps files nothing refers to any more, and those are the ones that go stale without anybody noticing.

func Of

func Of(exePath string) (Bundle, bool)

Of reports the bundle an executable path sits in, if it sits in one.

It is a question about a path, not about the machine, so it answers the same way everywhere: a build on another platform can reason about a bundle it is assembling.

func Running

func Running() (Bundle, bool)

Running reports the bundle this process is in, if it is in one.

type Spec

type Spec struct {
	// Dir is where the .app is written.
	Dir string
	// Name is the application's name, and the .app directory's.
	Name string
	// Identifier is the bundle identifier, in reverse-DNS form.
	Identifier string
	// Version is what the application reports as its version.
	Version string
	// Executable is the built program to put inside. It is copied, so the
	// caller keeps whatever it built.
	Executable string
	// Accessory asks for LSUIElement: a program with a menu-bar item and no
	// dock tile and no menu of its own, which is what a status-item
	// application is.
	Accessory bool
	// MinimumSystem is the oldest macOS this claims to run on. Empty leaves
	// the key out rather than inventing a floor.
	MinimumSystem string
	// UsageDescriptions are the NS...UsageDescription strings this program
	// needs, keyed by the plist key -- "NSCameraUsageDescription" and the like.
	//
	// ⛔ WITHOUT THE RIGHT ONE, macOS DOES NOT DENY THE PROGRAM, IT ENDS IT.
	// Touching a camera, a microphone, the Photos library or a dozen other
	// things from a program with no usage description for it is not a refusal a
	// caller can handle: TCC terminates the process with "This app has crashed
	// because it attempted to access privacy-sensitive data without a usage
	// description". A bare binary has no Info.plist at all, which is why a
	// program that needs any of these has to be a bundle.
	//
	// The value is shown to the person in the prompt, so it is a SENTENCE about
	// what this program wants it for -- "XR desk shows what the glasses see" --
	// and not the name of an API.
	UsageDescriptions map[string]string

	// Icon is a .icns file's bytes. Empty leaves the bundle without one,
	// which is an application drawn as a blank page everywhere it appears.
	//
	// It is bytes rather than a path because assembling a bundle is the last
	// step of a build, and by then the icon is as likely to be embedded in
	// the builder as sitting on disk beside it.
	Icon []byte
}

Spec is what to assemble.

Jump to

Keyboard shortcuts

? : This menu
/ : Search site
f or F : Jump to
y or Y : Canonical URL