-
Notifications
You must be signed in to change notification settings - Fork 0
How It Works
This page is the order of events in a single BootstrapMate run, with the trigger and the security context of each step. Read it when a run did something you did not expect and you need to know which step you are looking at in the log.
Everything below happens in one process, running as root, from start to exit. There is no second process, no LaunchAgent and no user-context worker.
Three things start the CLI. The package's postinstall boots out and then loads
/Library/LaunchDaemons/com.github.bootstrapmate.plist, whose RunAtLoad starts a run
immediately as root — this is the Setup Assistant path, and the daemon passes no
arguments, so the configuration must already be in the com.github.bootstrapmate
managed-preferences domain. An administrator can run the binary by hand with flags. The
GUI can start one via the root XPC helper, which adds --verbose to the arguments it was
given.
The CLI creates /Library/Managed Bootstrap/logs if it is missing. If that fails it
writes one line to stderr and carries on with no log file — there is no fallback log
location. It then opens a new log file named for the current time, deletes any .log in
that directory older than 30 days, installs a SIGTERM handler (which kills SwiftDialog and
exits 1, the path the GUI's Stop button uses), and logs the session header and the full
command line.
Three waits follow, in this order, and none of them is fatal:
-
Network. An
NWPathMonitorwaits up to--network-timeoutseconds (default 120) for a satisfied path. On timeout it does one synchronous DNS resolution ofapple.comas a last check, then logs eitherNetwork is availableorNetwork check timed out - proceeding anywayand continues either way. A run that proceeds without a network will fail its downloads. Note that thenetworkTimeoutmanaged preference is read but never applied — only the CLI option takes effect. -
Data volume. If
/Library/Managed Bootstrapis not writable — which happens early in Setup Assistant — it polls/Libraryonce a second for up to 30 seconds before proceeding regardless. -
Management profile. Only when no
--jsonurlwas passed and the configuration is not yet valid, it reloads managed preferences once a second for up to 300 seconds, logging progress every 30. On expiry it logsManagement configuration not received within 300sand continues, which then producesNo manifest URL configured…and exit 1.
CLI arguments are applied over the managed preferences, which are applied over the
embedded defaults; with --verbose the effective configuration is printed. The
authorization header, if configured, is set globally, so it is sent both on the manifest
fetch and on every subsequent item download.
The manifest is then downloaded, with a 60-second ceiling, and decoded — YAML for a
.yaml/.yml path, JSON for .json, and JSON-then-YAML for anything else. A download or
decode failure ends the run with exit 1 before any stage begins.
If --userscript was passed, the run short-circuits here: only userland items of type
userscript are executed, a session summary is written, and the process exits 0
regardless of what those scripts returned.
Stages always run preflight → setupassistant → userland. There is no way to reorder
them and no dependency graph inside a stage; items run in the order you wrote them.
Preflight uses only the first item whose type is rootscript; any others in the
array are ignored. Its exit code decides the run: 0 means the Mac is already provisioned,
so the whole bootstrap is skipped, cleanup runs and the CLI exits 0; greater than 0 means
continue; a negative code means the script could not be downloaded or launched, which
fails the phase and skips both remaining stages.
Setup Assistant runs every item in setupassistant. A failing item is recorded and
the loop continues — a bad package must not strand provisioning — but the run is marked
failed.
Userland first sets its status to Starting, then blocks in a two-second poll loop until
the console user is a real user, ignoring loginwindow, _mbsetupuser, root and any
name starting with an underscore. This wait has no timeout. A Mac that never reaches a
login, or that sits at a login window nobody uses, stays in this loop indefinitely with the
dialog reading "Waiting for user to log in...". Once a user appears, the items run.
The distinction is entirely about when in the same run an item executes, not about who
executes it. preflight and setupassistant items run while Setup Assistant is still up
and no user session exists. userland items run in the same root daemon process after a
console user appears. userscript items are executed the same way rootscript items are —
as root — because the helper that would run a script as the console user is present in
the source but never called. If an item genuinely needs the user's context, it has to
establish that itself.
For each item, in order: the skipIf architecture test can skip it outright; then the file
is downloaded to the absolute path in file, retries times (default 3) with retrywait
seconds between attempts (default 5), re-checking the SHA-256 after each; an existing file
whose hash already matches is not downloaded again. Downloads are read into memory and
written non-atomically, because the system temp directory is read-only during Setup
Assistant, so a very large payload is fully resident in RAM. HTTP status codes are not
inspected, so an error page reaches the destination and then fails the hash check.
A package with both packageid and version set is skipped if pkgutil already reports
that receipt at that version or newer. Otherwise its signature is checked with
pkgutil --check-signature before installer runs, and it is refused if untrusted (unless
allowUnsigned) or if its Team ID does not match expectedTeamID (which no setting
overrides). Scripts get no signature check — only the hash. A script with donotwait is
launched and reported as success immediately, without waiting for it.
There is no retry on an installer failure or a script failure; retries apply to downloads only.
Throughout the run, per-phase status (stage, start and completion time, exit code, last
error, run id) is written to /Library/Managed Bootstrap/status.json and to
/Library/Preferences/com.github.bootstrapmate.plist, and, when SwiftDialog is present at
/usr/local/bin/dialog and neither --no-dialog nor --silent was given, each item's
state is pushed to the on-screen progress list.
At the end, the dialog closes and — only if a reporting URL is configured — a single JSON
run summary is POSTed, with a 15-second request timeout. A reporting failure is logged and
never fails the run. Be aware that a successful run overwrites the status plist with just
LastRunVersion, LastUpdated and Architecture before the summary is built, so a
successful run reports an empty phases block while a failing one reports full detail.
Cleanup runs at the end of every run — success, failure, or preflight skip. It deletes
/Library/LaunchDaemons/com.github.bootstrapmate.plist and then runs
launchctl bootout system/com.github.bootstrapmate, falling back to launchctl remove if
that fails. Downloaded payloads are not removed; they stay wherever each item's file
put them.
If --reboot (or the reboot preference) is set and the run succeeded, a reboot is
triggered five seconds later. The process then exits 0 if every stage succeeded — or if
preflight chose to skip — and 1 if the manifest could not be loaded, no manifest URL was
configured, a stage failed, or SIGTERM was received.
There is no state machine and no resume. Every invocation starts from scratch with a fresh
run id. The only things that make a repeat run cheaper are the SHA-256 check that skips a
re-download, the pkgutil receipt check that skips a reinstall, and whatever logic you
put in your own preflight script. Because the daemon deletes itself during cleanup, a
second unattended run does not happen on its own.