0028. Host: macOS command line tools ship as plain executables
Date: 2026-10-13
Amends what 0024
and 0025 sign and notarize:
the tools in tag_tools/bin as well as the bundles. Everything else in both
still stands.
Context
Up to v3.2 every command line tool was built MACOSX_BUNDLE, so the DMG held
tag-info.app, tag-dwnld.app and so on. A user ran one as
tag_tools/tag-info.app/Contents/MacOS/tag-info, which nothing in the user
documentation said; the build tree had grown a post-build symlink
(host_cli_bundle_link) so that developers did not have to.
The bundles carried nothing. In the v3.2 image each tag-* bundle held its
Info.plist, its signature and the executable, and the executable linked only
/usr/lib and /System libraries: vcpkg links protobuf, libusb, SQLite and
Abseil statically. A bundle earns its place by carrying frameworks, plugins
and resources; these had none.
dataprocessing is different. It links QtCore and QtGui through
sensoranalysis, Qt is dynamic on macOS, and macdeployqt deploys Qt only
into a bundle. Its v3.2 bundle is 35 MB, almost all Qt.
Decision
- The seven shipped
tag-*tools, and the developer tools built beside them, are plain executables on macOS. The package installs the shipped ones totag_tools/bin, inside the folder the user drags out, so the DMG keeps one top-level folder and the tools one directory to put onPATH. dataprocessingstays a bundle intag_tools, deployed as before, and the package addstag_tools/bin/dataprocessing, a relative link to the executable inside it. dyld resolves@executable_paththrough the link, so the bundle's frameworks are found.- Signing stays one implementation.
cmake/MacosCodesignBundle.cmakegainsmacos_codesign_executable(), with the same hardened-runtime and timestamp rules as bundles;install_macos_codesign_executable()calls it at install time andhost/tools/sign-ci-macos.shcalls it in script mode on CI's tools. host/tools/verify-macos-dmg.shchecks every tool inbin: signature, authority, notarization (codesign -R=notarized --check-notarization, sincespctlrejects any bare executable as "not an app"), that the link resolves, and that a tool links nothing outside/usr/liband/System.
Verification
On 2026-10-13, from a local Developer ID build of a66ec44e plus this change:
- The DMG held six bundles and
tag_tools/binwith seven tools and thedataprocessinglink. Copied out,tag-info --helpanddataprocessing --helpran, the latter through the link and fromPATH. - Apple accepted the image (submission
5f0a31cf), the ticket stapled, andverify-macos-dmg.sh --notarizedpassed every bundle and every tool. - Given an image with a tool linking Qt and a link into a missing bundle,
verify-macos-dmg.shfailed both and exited 1.
The CI path was then exercised on release.yml run 38489181249, a manual
build of 667b8d8f: sign-ci-macos.sh --run 38489181249 --no-upload
re-signed it, Apple accepted it (submission a046b27f), and the same checks
passed every bundle, tool and link.
Consequences
- A tool that gains a dynamic dependency fails the release check rather than
failing on a user's Mac. Making it work then means linking the dependency
statically or making that tool a bundle like
dataprocessing. - A bare executable cannot hold a stapled ticket. Notarizing the image registers each tool's code hash, and Gatekeeper looks it up online the first time a quarantined copy runs, so the first run of each tool needs the network. The apps are unaffected; their tickets are stapled.
- Anyone who scripted the v3.2-and-earlier bundle paths has to change them to
tag_tools/bin/<tool>. The README says so. - Existing build trees keep stale
<tool>.appdirectories beside the new executables in<build>/<config>/bin. They are inert; the linker replaces the oldbin/<tool>link with the real executable. - Windows and Linux are unchanged:
MACOSX_BUNDLEwas ignored there.