Repository navigation
asdf-macos-app — build a signed, notarisable macOS .app bundle from an SBCL image #2615
Description
asdf-macos-app is an ASDF extension that turns a system into a macOS .app
bundle. You add a class and a build operation to the defsystem and asdf:make
does the rest — Info.plist, icon conversion, dylib relocation, code signing,
verification, and notarisation:
(defsystem "editor"
:defsystem-depends-on ("asdf-macos-app")
:class :macos-app-system
:build-operation "macos-app-op"
:entry-point "editor:main"
:bundle-identifier "com.example.editor"
:bundle-name "Editor"
:bundle-icon "res/icon.png"
:code-signing-identity "Developer ID Application: Jane Doe (ABCDE12345)"
:components ((:file "main")))Two problems shape the design, and both are the sort you only find by hitting
them.
The image dump kills the process. save-lisp-and-die never returns, so
nothing that must happen after the executable exists — signing, dylib
relocation, verification — can run in the same image. The operation therefore
builds the skeleton, spawns a child SBCL to dump into it, and finishes in the
parent. The child gets an explicit source registry naming every system in the
resolved dependency closure, because inheriting CL_SOURCE_REGISTRY is not
enough when the parent found systems through asdf:*central-registry*.
An SBCL image dumped with :executable t cannot be codesigned at all.
save-lisp-and-die appends the core to the runtime's Mach-O — past the end of
__LINKEDIT and past the code signature — and codesign refuses the trailing
data with main executable failed strict validation, for a Developer ID as much
as for ad hoc. So a bundle here holds the SBCL runtime as its executable, the
core beside it in Contents/Resources as a sealed resource, and a relative
symlink between them: the runtime finds a core named sbcl.core beside itself
with no arguments, which is what LaunchServices gives a double-clicked app, while
codesign seals the link as an ordinary resource rather than refusing it as
unsigned nested code. No C launcher, so no C toolchain in the build.
The result signs with the hardened runtime, passes codesign --verify --deep --strict, launches through LaunchServices, detects a tampered core, and
notarises. There is a notarize entry point that submits and staples.
- Repository: https://github.com/lispnik/asdf-macos-app
- Source: git, main branch (tagged
v0.2.0) - License: MIT
- Systems:
asdf-macos-app,asdf-macos-app/tests - Dependencies: none beyond
sb-posixon SBCL
SBCL only, and the bundles it builds are macOS only — though the library
itself loads and its layout/plist half runs anywhere, which is what the Linux CI
leg covers. macos-app:*allow-non-macos-build* assembles a bundle off macOS for
smoke tests; the result is marked Contents/INCOMPLETE-BUILD, and that marker is
acted on rather than advisory — such a bundle refuses to replace a complete one
and cannot be notarised.
:description, :author and :license are set on both systems, the README
documents the options and the known limits, and CI runs the suite on Linux and
macOS on every push: 203 checks. The macOS leg is the one that matters — it
runs plutil, real codesign, and codesign --verify against an actual SBCL
image, which is the riskiest part of the design.
It is in use: https://github.com/lispnik/utc-status-app ships a signed and
notarised disk image built with it.
Activity