Skip to content

fix(ci): prune Qt's mangled nested .app bundles before signing - #25

Merged
pacnpal merged 3 commits into
mainfrom
claude/macos-signing-injection-rvlnbq
Aug 20, 2026
Merged

pacnpal merged 3 commits into
mainfrom
claude/macos-signing-injection-rvlnbq

Conversation

@pacnpal

@pacnpal pacnpal commented Aug 20, 2026

Copy link
Copy Markdown
Owner

Follow-up to #24. macOS release builds still fail at the signing step — but further in. The dangling symlinks are gone and ~60 binaries sign successfully before this:

.../PySide6/Assistant__dot__app/Contents/MacOS/Assistant: replacing existing signature
.../Assistant__dot__app/Contents/MacOS/Assistant: the main executable or Info.plist
    must be a regular file (no symlinks, etc.)

Cause

PyInstaller splits the .app across Contents/Frameworks (binaries) and Contents/Resources (data), cross-linking each side — and relocates Qt's nested tool bundles into Frameworks under a mangled <name>__dot__app name. The directory is real, but its Contents/Info.plist is a symlink into Resources.

Handed a Mach-O at <X>/Contents/MacOS/<n>, codesign treats <X> as a bundle and signs it as one — then refuses when that bundle's Info.plist is a symlink. There is no flag to sign the file without the enclosing bundle, so the layout is simply unsignable as shipped.

Fix

Prune the mangled nested bundles on macOS. Assistant is Qt's help viewer; this app links QtCore/QtGui/QtWidgets only, and QtHelp — the module that would drive Assistant — is already --exclude-module'd, so nothing in the bundle can launch it. Removing them fixes signing and drops dead weight from every macOS artifact.

QtWebEngineProcess is spared by name: it's the one nested helper Qt genuinely needs at runtime, should WebEngine ever return to the build.

macos-sign.sh also now names the failing file and explains this specific case instead of dying on a bare codesign error, so a future occurrence diagnoses itself.

Testing

Against a bundle reproducing the real Frameworks/Resources split, with a codesign stub that refuses a symlinked Info.plist the way Apple's does:

Case Result
Pre-fix fails with CI's exact message, exit 1
Pre-fix, new diagnostic names the file and points at the prune block
Post-fix prune Assistant__dot__app removed from both roots
Post-fix prune QtWebEngineProcess__dot__app kept, signed
Post-fix sign bundle signs clean, exit 0

Still unverified without a Mac: that the resulting bundle notarizes and launches.


Generated by Claude Code

macOS release builds still fail at the signing step, now further in —
past the dangling symlinks fixed in #24, after ~60 binaries sign
successfully:

    .../PySide6/Assistant__dot__app/Contents/MacOS/Assistant: replacing existing signature
    .../Assistant__dot__app/Contents/MacOS/Assistant: the main executable
        or Info.plist must be a regular file (no symlinks, etc.)

PyInstaller splits the bundle across Contents/Frameworks (binaries) and
Contents/Resources (data), cross-linking each side, and relocates Qt's
nested tool bundles into Frameworks under a mangled "<name>__dot__app"
name. The directory is real but its Contents/Info.plist is a symlink
into Resources. codesign, handed a Mach-O at <X>/Contents/MacOS/<n>,
signs <X> as a bundle — and refuses outright when that bundle's
Info.plist is a symlink.

Assistant is Qt's help viewer. This app links QtCore/QtGui/QtWidgets
only, and QtHelp — the module that would drive Assistant — is already
excluded from the build, so nothing can launch it. Pruning the mangled
nested bundles fixes signing and drops dead weight from every macOS
artifact. QtWebEngineProcess is spared by name: it is the one nested
helper Qt needs at runtime, if WebEngine ever returns to the build.

macos-sign.sh now also names the file and explains this specific failure
instead of dying on a bare codesign error, so the next occurrence is
self-diagnosing.

Verified against a bundle reproducing the real Frameworks/Resources
split, with a codesign stub that refuses a symlinked Info.plist the way
Apple's does: the pre-fix script fails with CI's exact message, and with
the prune the Assistant bundle is removed from both roots, the WebEngine
helper survives and signs, and the bundle signs clean.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01JDuaD9Tgz6PT1yUypGzAmU
@chatgpt-codex-connector

Copy link
Copy Markdown

You have reached your Codex usage limits for security reviews. Please try again later.

@coderabbitai

coderabbitai Bot commented Aug 20, 2026 •

Copy link
Copy Markdown

Warning

Review limit reached

@pacnpal, you've reached your PR review limit, so we couldn't start this review.

Next review available in: 29 minutes

Limit details: You’ve used the included review currently available.

You've used all free OSS reviews for now. Wait for the free limit to reset to keep reviewing this public repository.

How can I continue?

Wait for the limit to reset, then comment @coderabbitai review or push new commits to the PR.

An organization admin can change what happens after included review limits in Billing.

How do review limits work?

CodeRabbit enforces per-developer PR review limits within each organization.

For paid Pro and Pro+ reviews, CodeRabbit uses a developer's included PR review attempts over the past 7 days to set the current hourly allowance. At typical activity levels, the full plan allowance applies. Higher sustained activity can lower the allowance until earlier attempts leave the 7-day window.

Please refer docs for additional details.

Review details
⚙️ Run configuration

Configuration used: Organization UI

Review profile: ASSERTIVE

Plan: Pro Plus

Run ID: 81090f9a-ce20-4d8b-a65a-5ddbbe029907

📥 Commits

Reviewing files that changed from the base of the PR and between 2e2f655 and 84882ba.

📒 Files selected for processing (2)
  • .github/signing/macos-sign.sh
  • .github/workflows/release.yml

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: cecc877a8a

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment thread .github/workflows/release.yml
Review finding, and a fair one: sparing QtWebEngineProcess__dot__app
protected nothing. That helper has the same PyInstaller-split layout and
symlinked Contents/Info.plist as Assistant, so if it were ever present
the signing pass would walk into it and fail with the identical codesign
error this change exists to fix — the carve-out only guaranteed the
failure it claimed to prevent. My own test fixture hid this by giving
the helper a real Info.plist, which is not how PyInstaller lays it out.

Not pruning it either, though: Qt needs it at runtime, so pruning trades
a loud build failure for a silently broken WebEngine at the user's end.
It cannot appear today (every WebEngine module is excluded), so the
build now stops with an explanation of what re-enabling WebEngine
actually requires — rebuilding the helper into a signable layout — which
is the one path that neither ships something broken nor fails later
inside codesign.

Tested both branches with a fixture that reproduces the real layout:
Assistant-only prunes and signs clean, and a re-enabled WebEngine helper
fails in the prune step with the explanation rather than in codesign.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01JDuaD9Tgz6PT1yUypGzAmU

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: cde5dcb5b0

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment thread .github/workflows/release.yml Outdated
Partly adopting a review finding. The finding's premise — that
`-maxdepth` is GNU-only and unsupported by Apple's find(1) — is not
right: it is a BSD primary, documented in macOS's own find(1) man page,
inherited from FreeBSD, and this workflow already relies on it a few
lines up. No change needed there.

The second half of the finding is correct and worth fixing, though: a
find(1) failure inside a process substitution does not propagate under
set -e. Whatever the cause, a scan that failed would sweep nothing, the
step would report success, and the build would die later inside codesign
with nothing pointing back here. Capturing via command substitution puts
find's exit status back in reach, so a failed scan stops the build where
the problem is.

Branches covered by the fixture: Assistant-only prunes and signs clean;
a WebEngine helper stops the build with the explanation and is left in
place; a bundle with no nested apps is a clean no-op; and a find that
fails now exits 1 with a diagnostic where it previously reported success
with the bundle untouched.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01JDuaD9Tgz6PT1yUypGzAmU
@pacnpal
pacnpal merged commit 71446a5 into main Aug 20, 2026
11 checks passed
@pacnpal
pacnpal deleted the claude/macos-signing-injection-rvlnbq branch August 20, 2026 17:07
pacnpal added a commit that referenced this pull request Aug 28, 2026
* release: v1.3.2 — signed and notarized macOS builds

The Developer ID signing + notarization pipeline landed in #23 (with
follow-up fixes in #24 and #25) after v1.3.1 was cut, so every published
macOS zip is still the old ad-hoc-signed build. Nothing ships it until a
`v*` tag is pushed: release.yml only publishes assets on a tag push, and
the 2026-08-20 dispatch smoke-test on this commit confirmed sign +
notarize + staple succeed on macos-arm64, macos-x86_64, and
macos-universal2.

- Bump the package version to 1.3.2. It had been left at 1.2.6 through
  the v1.3.0 and v1.3.1 tags, and it is not cosmetic: `__version__` is
  read from installed package metadata and stamped into every disc's
  boot menu (`MENU TITLE FloppyBootCD vX.Y.Z`), so discs built from
  v1.3.1 identify themselves as v1.2.6. Also surfaced by `--version`
  and the About dialog.
- Rewrite the macOS install docs now that a signed build exists to point
  at. README, the Pages site, and `.postbeep/docs.md` all told every
  macOS user to strip the quarantine flag; that step is unnecessary from
  v1.3.2 on, and `.postbeep/docs.md` (rendered on POSTBEEP.NET) still
  described the app as flatly "unsigned". Each now leads with the plain
  unzip-and-open path plus the `spctl` check, and keeps the `xattr`
  workaround scoped to v1.3.1 and older.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_016NNtwTQvf8G9hEuEsHwQBH

* fix(ci): fail a tag build closed when signing credentials are missing

Review caught a real gap: the docs added in the previous commit promise
that every macOS artifact from v1.3.2 on is signed and notarized, but
release.yml could publish an ad-hoc-signed one. macos-import-cert.sh and
macos-notarize.sh both exit 0 when their secrets are absent, and the
"Locate signing tooling" step only warns when the tooling is missing —
so a release cut with a lapsed or misconfigured secret would ship a
bundle Gatekeeper blocks, under a promise that it wouldn't.

That graceful degradation is right for fork PRs and dispatch smoke-tests
and is kept. It is wrong for a release, which is the one trigger that
publishes. Introduce a single MACOS_SIGNING_REQUIRED contract, set at
the workflow level for pushed v* tags only, and honour it everywhere the
fallback lives:

- macos-import-cert.sh, macos-sign.sh, macos-notarize.sh: each turns its
  clean no-op into a hard failure with a message naming the missing
  secret.
- "Locate signing tooling" (both the matrix job and universal2): missing
  tooling is fatal on a tag build, since every signing step below is
  gated on its output and would otherwise silently skip.

A release now either carries a real Developer ID signature and a stapled
ticket, or it doesn't happen — which is what makes the docs' claim
unconditional rather than best-effort.

Also from review:

- docs/index.html: the spctl example checked the relative path, but the
  block moves the app to /Applications first, so following it in order
  made the check fail. Point it at the installed path.
- README: the legacy xattr command sat in the same copy-pasteable block
  as the current-release steps, so a v1.3.2 user working top to bottom
  still stripped quarantine. Move it into a collapsed
  "Downloading v1.3.1 or older?" section.
- .github/signing/README.md: document the new contract.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_016NNtwTQvf8G9hEuEsHwQBH

---------

Co-authored-by: Claude <thewesker@gmail.com>
Co-authored-by: Claude Opus 5 <noreply@anthropic.com>
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.

2 participants