feat(self-packaging #48): macOS notarised via reserved Mach-O segment#58
Merged
Conversation
This was referenced May 22, 2026
Part of #40. Closes #48. macOS-notarised distribution requires a stable binary signature. The Linux/Windows "append a ZIP after EOF" trick invalidates the signature because trailing bytes after __LINKEDIT aren't covered. This PR introduces the reserved-segment approach used by AppImage, PyInstaller, and friends: 1. The macOS build allocates a placeholder __FLAPI/__bundle Mach-O section at link time (default 16 MiB, knob FLAPI_RESERVED_BUNDLE_MIB). 2. `flapi pack` overwrites the segment in place rather than appending after EOF. 3. `pack` re-invokes `codesign` after writing, so the freshly bundled binary has a fresh, valid signature. 4. The runtime locator looks for the reserved segment first; Linux/Windows binaries don't have one, so behaviour there is unchanged. Implementation: - src/include/macho_bundle.hpp + src/macho_bundle.cpp -- 64-bit Mach-O parser (header + LC_SEGMENT_64 + section_64), no external deps, compiles on all platforms. CodesignBinary() wraps popen for the macOS path and is a benign no-op elsewhere. - CMakeLists.txt (APPLE branch only) -- adds the placeholder file via add_custom_command + dd, and links `flapi` with -Wl,-sectcreate,__FLAPI,__bundle,<placeholder>. Linux/Windows builds skip this entirely. - src/bundle_locator.{hpp,cpp} -- refactored. Extracted the EOCD reverse-scan into `ScanBufferForEocd`, shared by: * `LocateBundle(path)` -- existing EOF-tail scan * `LocateBundleInRange(path, off, size)` -- NEW. Scans a sub-range of the file. Used by section-mode lookup. * `LocateBundleInSelf()` -- NEW behaviour: try the macOS section first, fall back to EOF tail. - src/include/pack.hpp -- new enum `MacOSPackMode` + PackOptions fields `macos_mode` (defaults to kReservedSegment) and `codesign` (defaults to true; tests turn it off to skip the codesign call). - src/pack.cpp -- Pack() probes for the Mach-O section first; if present, copies the host whole and `OverwriteFlapiSection` writes the archive in place. If the section is absent (Linux/Windows) OR `--macos-append` was passed, falls through to the existing append-after-EOF code path. Re-signs on Darwin afterwards. - src/main.cpp -- new `--macos-append` flag on the `pack` subcommand. Threads through to PackOptions. Tests: - test/cpp/macho_bundle_test.cpp -- 7 cases / 13 assertions. Builds synthetic Mach-O fixtures byte-by-byte and feeds them to `LocateFlapiSectionInBuffer`. Covers: magic recognition, present section, absent section, wrong-segment name match attempt, short buffer, non-Mach-O input, CodesignBinary no-op on non-Darwin. - test/integration/test_self_packaging_macos.py -- 4 cases, all marked `pytest.mark.skipif(platform.system() != "Darwin")`: 1. unbundled flapi has the reserved __FLAPI/__bundle segment (otool -l check) 2. default pack passes `codesign --verify --strict` 3. `--macos-append` produces a runnable artifact with a discoverable bundle 4. oversized payload (32 MiB into a 16 MiB segment) is rejected with an error mentioning FLAPI_RESERVED_BUNDLE_MIB - All existing 14 Linux integration tests still pass; the new code is fully backward-compatible (section probe returns nullopt on Linux, code falls through to the existing append path). - 23/23 C++ unit tests in the [pack],[macho_bundle],[bundle_locator] tag set pass (62 assertions). Out of scope (deferred to follow-ups): - Fat (universal) binary support -- the parser handles thin 64-bit Mach-O only. macOS releases produced by this repo are per-architecture thin so the gap is acceptable for now. - 32-bit Mach-O. Same reasoning. Closes #48. Part of #40.
4235df5 to
86e96e5
Compare
The previous `cmake -E env bash -c "dd ... >/dev/null 2>&1"` form broke under ninja-on-macos -- the redirect operators were consumed by the outer shell that cmake exec'd, not the bash -c subshell, so the build failed with: /bin/sh: /dev/null 2: Permission denied Drop the shell wrapper entirely. dd's argv form (`if=...`, `of=...`, `bs=1m`, `count=N`) goes straight to execve via CMake's command runner, no shell involved. dd's 2-3 line summary stays in CI logs, which is fine. Found by CI on PR #58 (#48 macOS notarised) at osx-universal-build.
This was referenced May 24, 2026
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Part of epic #40. Stacked on #57 -> #56 -> #55 -> #54 -> #53 -> #52 -> #51.
Summary
macOS-notarised distribution requires a stable signature. The
Linux/Windows trick of appending a ZIP after
__LINKEDITinvalidatesit. This PR introduces the reserved-segment approach used by AppImage,
PyInstaller, et al.:
__FLAPI/__bundleMach-O sectionat link time (16 MiB default, knob
FLAPI_RESERVED_BUNDLE_MIB).flapi packoverwrites the segment in place instead ofappending.
packre-invokescodesignafter writing, so the bundledbinary has a fresh valid signature.
Windows binaries don't have one, behaviour there is unchanged.
What's added
src/include/macho_bundle.hpp+src/macho_bundle.cppCodesignBinary()is a benign no-op off Darwin.CMakeLists.txt(APPLE branch only)dd+add_custom_command, linksflapiwith-Wl,-sectcreate,__FLAPI,__bundle,....src/bundle_locator.{hpp,cpp}ScanBufferForEocd, addedLocateBundleInRange()for section-mode,LocateBundleInSelf()now tries section first then falls back to EOF tail.src/include/pack.hppMacOSPackModeenum +PackOptions::macos_modeandPackOptions::codesign.src/pack.cppsrc/main.cpp--macos-appendflag on thepacksubcommand.Tests
C++ unit tests (run on Linux against synthetic fixtures)
test/cpp/macho_bundle_test.cpp-- 7 cases / 13 assertions:__FLAPI/__bundlein synthetic Mach-OCodesignBinaryis a benign no-op on non-DarwinmacOS integration tests (skipped on non-Darwin)
test/integration/test_self_packaging_macos.py-- 4 cases, allpytest.mark.skipif(platform.system() != "Darwin"):flapihas the reserved__FLAPI/__bundlesegment(verified via
otool -l)codesign --verify --strict--macos-appendproduces a runnable artifact with a discoverablebundle (legacy path still works)
mentioning
FLAPI_RESERVED_BUNDLE_MIBThese will run on the macOS leg in #49 (CI) and verify the
end-to-end signing pipeline.
Existing test coverage
[pack],[macho_bundle],[bundle_locator]pass.Linux backward-compatible (section probe returns nullopt, code
falls through to the existing append path).
Out of scope (deferred follow-ups, called out in the parser comments)
64-bit Mach-O only. macOS releases produced by this repo are
per-architecture thin (arm64-osx, x64-osx triplets), so the gap
is acceptable for now.
signature stable so notarisation can succeed; the actual
xcrun notarytool submitflow is release-tooling, not part ofthe self-packaging code path.
Test plan
4 skipped in 0.07s).otool -lshows the segment;codesign --verify --strictpasses on packed output. Will run in CI (Self-packaging #9: CI cross-platform verification (Linux x86/ARM, macOS, Windows) #49).
Closes #48. Part of #40. Stacked on #57.