Skip to content

feat(self-packaging #48): macOS notarised via reserved Mach-O segment#58

Merged
jrosskopf merged 2 commits into
mainfrom
feature/gh-48-macos-notarised
May 22, 2026
Merged

feat(self-packaging #48): macOS notarised via reserved Mach-O segment#58
jrosskopf merged 2 commits into
mainfrom
feature/gh-48-macos-notarised

Conversation

@jrosskopf
Copy link
Copy Markdown
Contributor

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 __LINKEDIT invalidates
it. This PR introduces the reserved-segment approach used by AppImage,
PyInstaller, et al.:

  1. macOS build links a placeholder __FLAPI/__bundle Mach-O section
    at link time (16 MiB default, knob FLAPI_RESERVED_BUNDLE_MIB).
  2. flapi pack overwrites the segment in place instead of
    appending.
  3. pack re-invokes codesign after writing, so the bundled
    binary has a fresh valid signature.
  4. The runtime locator probes the reserved segment first; Linux /
    Windows binaries don't have one, behaviour there is unchanged.

What's added

File Purpose
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 Linux/Windows too. CodesignBinary() is a benign no-op off Darwin.
CMakeLists.txt (APPLE branch only) Generates the 16-MiB placeholder via dd + add_custom_command, links flapi with -Wl,-sectcreate,__FLAPI,__bundle,....
src/bundle_locator.{hpp,cpp} Refactored. Extracted the EOCD reverse-scan into ScanBufferForEocd, added LocateBundleInRange() for section-mode, LocateBundleInSelf() now tries section first then falls back to EOF tail.
src/include/pack.hpp New MacOSPackMode enum + PackOptions::macos_mode and PackOptions::codesign.
src/pack.cpp Probes for the section in the host; section-overwrite path on hit, append path on miss. Re-signs on Darwin.
src/main.cpp New --macos-append flag on the pack subcommand.

Tests

C++ unit tests (run on Linux against synthetic fixtures)

test/cpp/macho_bundle_test.cpp -- 7 cases / 13 assertions:

  • magic recognition (Mach-O 64, fat, ELF rejected)
  • finds __FLAPI/__bundle in synthetic Mach-O
  • returns nullopt when section is absent
  • rejects non-Mach-O input
  • rejects same-name section in wrong segment
  • short / truncated buffer handled without crash
  • CodesignBinary is a benign no-op on non-Darwin
[macho_bundle] All tests passed (13 assertions in 7 test cases)

macOS integration tests (skipped on non-Darwin)

test/integration/test_self_packaging_macos.py -- 4 cases, all
pytest.mark.skipif(platform.system() != "Darwin"):

  1. unbundled flapi has the reserved __FLAPI/__bundle segment
    (verified via otool -l)
  2. default pack passes codesign --verify --strict
  3. --macos-append produces a runnable artifact with a discoverable
    bundle (legacy path still works)
  4. 32-MiB payload into 16-MiB segment -- rejected with an error
    mentioning FLAPI_RESERVED_BUNDLE_MIB

These will run on the macOS leg in #49 (CI) and verify the
end-to-end signing pipeline.

Existing test coverage

Out of scope (deferred follow-ups, called out in the parser comments)

  • Fat (universal) binary support. The parser handles thin
    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.
  • 32-bit Mach-O. Same reasoning.
  • Notarisation upload pipeline. This PR makes the binary
    signature stable so notarisation can succeed; the actual
    xcrun notarytool submit flow is release-tooling, not part of
    the self-packaging code path.

Test plan

Closes #48. Part of #40. Stacked on #57.

@jrosskopf jrosskopf changed the base branch from feature/gh-47-env-config-loglevel to main May 22, 2026 16:10
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.
@jrosskopf jrosskopf force-pushed the feature/gh-48-macos-notarised branch from 4235df5 to 86e96e5 Compare May 22, 2026 16:58
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.
@jrosskopf jrosskopf marked this pull request as ready for review May 22, 2026 18:44
@jrosskopf jrosskopf merged commit 52af76c into main May 22, 2026
17 checks passed
@jrosskopf jrosskopf deleted the feature/gh-48-macos-notarised branch May 22, 2026 18:44
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Self-packaging #8: macOS notarised — reserved-segment variant + re-sign

1 participant