Skip to content

Shrink release DMG 102MB -> 61MB: strip binaries, LZMA DMG, dead-code stripping - #7422

Closed
lawrencecchen wants to merge 3 commits into
mainfrom
feat-shrink-dmg
Closed

lawrencecchen wants to merge 3 commits into
mainfrom
feat-shrink-dmg

Conversation

@lawrencecchen

@lawrencecchen lawrencecchen commented Jul 6, 2026 •

Copy link
Copy Markdown
Contributor

The download grew from 12.7MB (v0.60.0) to 102.1MB (v0.64.17). Two structural causes, both fixed here with no feature or architecture change.

Binaries were never stripped. The release workflows use xcodebuild's build action, which skips the strip phase even in Release, so every shipped Mach-O carried its full symbol table: the main binary's arm64 slice has a 59.5MB __LINKEDIT (360k symbols), larger than its 57.8MB __TEXT. dSYMs are built separately (dwarf-with-dsym) and uploaded to Sentry, so server-side crash symbolication is unaffected. New scripts/strip-app-binaries.sh strips the app binary and Resources/bin/cmux with -rSTx, and the nucleo FFI dylib and DockTile plugin with -x (exports preserved; verified all 5 cmux_nucleo_* exports survive). It hard-fails on missing paths so bundle-layout drift is caught at release time. Wired into release.yml, nightly.yml, and build-sign-upload.sh after the final app payload is assembled (ghostty helper installed) and before codesigning. The zig-built ghostty helper, Sparkle, and Sentry are already stripped and untouched.

DMG used lzfse. create-dmg emits ULFO; converting to ULMO (LZMA) via hdiutil convert before DMG codesign/notarize/staple cuts another 29MB. Appcast edSignature/length and the Homebrew cask sha256 all derive from the final converted bytes. ULMO needs macOS 10.15+ to mount; the app requires Ventura.

Also sets DEAD_CODE_STRIPPING = YES in both project-level configs (previously unset, so the linker default NO kept unreferenced GhosttyKit/static-lib code). It applies to Debug too, so this PR's CI exercises dead-stripped links before any release does. A dlsym audit found only FFI-dylib and Security.framework lookups, no self-dlsym into the main executable.

Measured on the real v0.64.17 artifact (strip script + ULFO->ULMO conversion run locally, DMG rebuilt): app 357MB -> 221MB installed, lipo -archs intact on both binaries, DMG 102.1MB -> 61.1MB (-40%), converted image mounts via hdiutil attach. Intel support (universal binaries, #2287) is deliberately unchanged.

🤖 Generated with Claude Code


View with Codesmith Autofix with Codesmith
Need help on this PR? Tag /codesmith with what you need. Autofix is disabled.


Note

Medium Risk
Changes affect every shipped macOS binary and DMG signing order; stripping is mitigated by separate dSYMs for Sentry, but aggressive strip/dead-code settings could surface link or runtime issues if exports or dynamic lookups were missed.

Overview
Shrinks macOS release and nightly downloads by stripping symbol tables from key bundled Mach-Os, enabling linker dead-code stripping, and recompressing disk images with LZMA.

Adds scripts/strip-app-binaries.sh and runs it in release.yml, nightly.yml, and scripts/build-sign-upload.sh after the app payload is assembled (including the Ghostty helper) and before codesign/notarize. It strips the main app and CLI with -rSTx and lighter -x on the nucleo FFI dylib and DockTile plugin, failing fast if expected paths are missing.

Adds scripts/convert-dmg-ulmo.sh to convert DMGs to ULMO via hdiutil; CI and the manual release script call it after create-dmg and codesign the DMG once afterward (conversion invalidates any prior DMG signature). build-sign-upload.sh stops signing at DMG creation time for the same reason.

Sets DEAD_CODE_STRIPPING = YES on project-level Debug and Release in cmux.xcodeproj so the linker drops unreferenced code at link time.

Reviewed by Cursor Bugbot for commit d09834f. Bugbot is set up for automated code reviews on this repo. Configure here.


Summary by cubic

Shrink macOS release size by ~40%: DMG 102MB → 61MB and installed app 357MB → 221MB. Achieved by stripping binaries, enabling dead-code stripping, and converting the DMG to LZMA; no feature or architecture changes.

  • Refactors
    • Added scripts/strip-app-binaries.sh; strips app and CLI (-rSTx), FFI dylib and DockTile plugin (-x); run before codesign in release.yml, nightly.yml, and scripts/build-sign-upload.sh.
    • Introduced scripts/convert-dmg-ulmo.sh; workflows and manual script now convert to ULMO (LZMA) and sign the DMG once after conversion; appcast/Homebrew hashes use the final image; requires macOS 10.15+; clarifies Homebrew vs npm create-dmg signing behavior, so the manual path omits --codesign before conversion on purpose.
    • Set DEAD_CODE_STRIPPING=YES for Debug/Release to drop unused code.
    • Safety: dSYMs still uploaded; required exports preserved; universal binaries unchanged.

Written for commit d09834f. Summary will update on new commits.

Review in cubic

Summary by CodeRabbit

  • New Features
    • macOS release/nightly pipelines now strip targeted embedded binaries during packaging to produce smaller app bundles.
    • DMG artifacts are converted to the more space-efficient ULMO format before codesign and notarization.
  • Bug Fixes
    • Packaging now fails early with clear errors if expected app components are missing during stripping.
    • Signing/notarization now follows the converted DMG to improve artifact consistency.
  • Chores
    • macOS build settings enable explicit dead code stripping; shellcheck noise is reduced for secret sourcing.

… stripping

The shipped app was never stripped: xcodebuild's `build` action skips strip
even in Release, so the main binary carried a 59.5MB symbol table per arch
(larger than its own __TEXT) and the CLI another 15MB per arch. dSYMs are
built separately (dwarf-with-dsym) and uploaded to Sentry, so these symbols
were pure download weight. New scripts/strip-app-binaries.sh strips the app
binary and CLI with -rSTx and the nucleo FFI dylib and DockTile plugin with
-x (exports preserved), wired into release.yml, nightly.yml, and
build-sign-upload.sh after the final app payload is assembled and before
codesigning.

The DMG also moves from create-dmg's ULFO (lzfse) default to ULMO (LZMA)
via hdiutil convert before DMG codesign/notarize/staple; appcast signature,
length, and Homebrew sha256 all still derive from the final bytes.

DEAD_CODE_STRIPPING=YES is set in both project-level configs (previously
unset, so linker default NO retained unreferenced GhosttyKit/static code).
Debug too, so PR CI exercises dead-stripped links before a release does.

Measured on the real v0.64.17 artifact: strip 357MB->221MB installed,
lipo archs intact, all 5 FFI dylib exports intact; DMG 102.1MB -> 61.1MB
(-40%). ULMO verified mounting via hdiutil attach.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
@vercel

vercel Bot commented Jul 6, 2026 •

Copy link
Copy Markdown

The latest updates on your projects. Learn more about Vercel for GitHub.

Project Deployment Actions Updated (UTC)
cmux Canceled Canceled Jul 6, 2026 4:53am
cmux-staging Building Building Preview, Comment Jul 6, 2026 4:53am

@coderabbitai

coderabbitai Bot commented Jul 6, 2026 •

Copy link
Copy Markdown

Review Change Stack

📝 Walkthrough

Walkthrough

This PR adds a macOS app-binary stripping script, wires it into local build and CI packaging flows, converts generated DMGs to ULMO format before signing, and sets DEAD_CODE_STRIPPING = YES; in the Xcode project.

Changes

Binary stripping and DMG packaging

Layer / File(s) Summary
Strip binaries script
scripts/strip-app-binaries.sh
New script validates the app bundle argument, strips embedded binaries with per-target flags via /usr/bin/strip, and logs size before/after, failing if a target binary is missing.
Local build integration
scripts/build-sign-upload.sh
Adds a shellcheck suppression, invokes the strip script before codesigning, and replaces DMG creation with a pipeline that converts to ULMO format, re-codesigns, and lists the result.
Nightly workflow integration
.github/workflows/nightly.yml
Adds a strip-binaries step for the nightly app build and a DMG ULMO conversion step during notarization/packaging.
Release workflow integration
.github/workflows/release.yml
Adds a gated strip-binaries step before architecture verification and a DMG ULMO conversion step in the notarization flow.
Xcode build setting
cmux.xcodeproj/project.pbxproj
Sets DEAD_CODE_STRIPPING = YES; in two build configuration blocks.

Estimated code review effort: 3 (Moderate) | ~20 minutes

Sequence Diagram(s)

sequenceDiagram
  participant BuildScript as build-sign-upload.sh
  participant StripScript as strip-app-binaries.sh
  participant hdiutil as hdiutil
  participant codesign as codesign

  BuildScript->>StripScript: strip-app-binaries.sh "$APP_PATH"
  StripScript-->>BuildScript: stripped app bundle
  BuildScript->>hdiutil: create DMG without codesign
  BuildScript->>hdiutil: convert DMG to ULMO
  hdiutil-->>BuildScript: cmux-macos.dmg
  BuildScript->>codesign: force sign final DMG
Loading

Related issues: None found.

Related PRs: None found.

Suggested labels: build, ci, macos

Suggested reviewers: None identified from the provided context.

🐰 A binary trimmed, a DMG refined,
ULMO shapes the disk we bind,
Symbols stripped, size grows lean,
Signed and sealed, the build is clean.

🚥 Pre-merge checks | ✅ 24 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Description check ⚠️ Warning The description covers the change well, but it omits the required Testing, Review Trigger, and Checklist sections from the template. Add the missing template sections: Testing details, the review-trigger block, and the checklist; mark Demo Video N/A if not applicable.
✅ Passed checks (24 passed)
Check name Status Explanation
Docstring Coverage ✅ Passed No functions found in the changed files to evaluate docstring coverage. Skipping docstring coverage check.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
Cmux Swift Actor Isolation ✅ Passed The diff touches only scripts/build-sign-upload.sh; no .swift production files changed, so no Swift actor-isolation risk was introduced.
Cmux Swift Blocking Runtime ✅ Passed No Swift files are changed in the PR range; only workflows/scripts were modified, so the Swift blocking-runtime rule is not triggered.
Cmux Browser Automation Off-Main ✅ Passed Packaging-only diff: no browser.* routing, mainActor/processV2Command changes, or policy-test files were touched.
Cmux Expensive Synchronous Load ✅ Passed PASS: The commit only touches scripts/build-sign-upload.sh; no Swift source files or agent-history loaders were added or moved, so the rule doesn’t apply.
Cmux Cache Substitution Correctness ✅ Passed PASS: The touched code is a shell script only; there are no Swift/TypeScript/JavaScript cache/persistence paths for this rule to apply to.
Cmux No Hacky Sleeps ✅ Passed No fixed sleeps, timers, or polling were added in the changed shell/runtime scripts; workflow waits are CI orchestration and out of scope.
Cmux Algorithmic Complexity ✅ Passed New shell steps only process fixed, tiny target lists (4 binaries, 2 release assets) and add no scalable nested scans or hot-path rescans.
Cmux Swift Concurrency ✅ Passed The diff only changes workflows, shell scripts, and Xcode settings; no Swift source files were modified, so the Swift concurrency rule isn’t triggered.
Cmux Swift @Concurrent ✅ Passed No Swift files changed in the PR diff; only scripts/build-sign-upload.sh was modified, so the Swift @concurrent rule is not applicable.
Cmux Swift File And Package Boundaries ✅ Passed No production Swift files changed; the diff only updates scripts/build-sign-upload.sh, so the Swift boundary rule doesn’t apply.
Cmux Swiftpm Lockfiles ✅ Passed The PR changes workflows, scripts, and dead-code stripping only; no .gitignore or SwiftPM package-reference change appears, and no Package.resolved diff is required.
Cmux Swift Logging ✅ Passed No Swift files changed in the PR diff; only workflows, scripts, and the Xcode project changed, so the Swift logging rule isn’t implicated.
Cmux User-Facing Error Privacy ✅ Passed New user-facing copy is generic (usage/not found/stripping) and doesn’t expose vendor names, env vars, tokens, raw upstream errors, or payload dumps.
Cmux Full Internationalization ✅ Passed Branch only changes build scripts/workflows and project settings; no web/messages, .xcstrings, or localized Swift text were modified.
Cmux Swiftui State Layout ✅ Passed The PR only changes workflows, project settings, and shell scripts; no Swift files or SwiftUI state/layout patterns were introduced.
Cmux Architecture Rethink ✅ Passed PASS: The PR only changes workflows, shell scripts, and an Xcode setting; no Swift source or UI/lifecycle wiring is added, so the Swift architectural rethink rule doesn’t apply.
Cmux Swift Auxiliary Window Close Shortcuts ✅ Passed PR changes only workflows, a pbxproj setting, and shell scripts; no Swift window code or cmuxAuxiliaryWindowIdentifiers changes are present.
Cmux Source Artifacts ✅ Passed Changed paths are workflows, project config, and hand-written scripts; no artifact/cache/tmp paths or generated outputs appear in the diff.
Cmux No Test Or Debug Seam In Production Source ✅ Passed Only scripts/build-sign-upload.sh changed; no production Sources Swift files were touched, so the seam rule is not applicable.
Cmux No Ambient Global State ✅ Passed PASS: the commit touches only scripts/build-sign-upload.sh; no Swift files or new global-state constructs were added.
Title check ✅ Passed The title clearly summarizes the main change: shrinking the macOS release DMG via stripping binaries, LZMA conversion, and dead-code stripping.
✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch feat-shrink-dmg

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.

@lawrencecchen

Copy link
Copy Markdown
Contributor Author

@codex review

@coderabbitai coderabbitai 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.

Actionable comments posted: 1

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@scripts/build-sign-upload.sh`:
- Around line 119-124: The DMG to ULMO conversion sequence is duplicated across
multiple release paths and should be centralized. Extract the create-dmg,
hdiutil convert, mv, and re-sign flow from the build-sign-upload.sh logic into a
shared script such as scripts/convert-dmg-ulmo.sh, and update the
build-sign-upload.sh, nightly workflow, and release workflow call sites to use
it. Keep the behavior identical, including the post-conversion codesign step, so
all paths stay in sync.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: ASSERTIVE

Plan: Pro

Run ID: fd49cee8-6fda-412b-91fa-678c8edf1cc0

📥 Commits

Reviewing files that changed from the base of the PR and between 3bce97e and 94a3408.

📒 Files selected for processing (5)
  • .github/workflows/nightly.yml
  • .github/workflows/release.yml
  • cmux.xcodeproj/project.pbxproj
  • scripts/build-sign-upload.sh
  • scripts/strip-app-binaries.sh

Comment thread scripts/build-sign-upload.sh Outdated
@greptile-apps

greptile-apps Bot commented Jul 6, 2026 •

Copy link
Copy Markdown
Contributor

Greptile Summary

This PR reduces the macOS release DMG from 102MB to ~61MB by stripping Mach-O symbols, re-compressing the DMG with LZMA (ULMO), and enabling linker dead-code stripping—no feature or architecture changes.

  • scripts/strip-app-binaries.sh strips the main executable and bundled CLI with -rSTx and the nucleo FFI dylib and DockTile plugin with -x (preserving exports); the script hard-fails on any missing path and runs before codesigning in all three packaging flows.
  • scripts/convert-dmg-ulmo.sh converts a ULFO DMG to ULMO in-place via hdiutil convert; callers in release.yml, nightly.yml, and build-sign-upload.sh all codesign the DMG exactly once after conversion, correctly replacing the previous signature.
  • cmux.xcodeproj/project.pbxproj enables DEAD_CODE_STRIPPING = YES in both project-level Debug and Release configurations.

Confidence Score: 5/5

Safe to merge — packaging-only changes with no feature or architecture modifications; operation ordering (strip → codesign app → create DMG → ULMO convert → codesign DMG) is correct in all three flows.

All three packaging paths (release.yml, nightly.yml, build-sign-upload.sh) apply strip before codesigning the app bundle and ULMO conversion before codesigning the DMG, which is the required order. dSYMs are unaffected so Sentry symbolication continues to work. The -x flag on the dylib and plugin preserves exported symbols, and the PR documents a dlsym audit confirming no self-lookup into the main executable. Dead-code stripping is a standard linker flag with no runtime risk for an app that isn't dynamically looked up by outside callers.

No files require special attention.

Important Files Changed

Filename Overview
scripts/strip-app-binaries.sh New script; strips four known bundle paths with appropriate flags (-rSTx for executables, -x for dylib/plugin); hard-fails on missing paths; correct macOS-only stat usage.
scripts/convert-dmg-ulmo.sh New script; converts DMG to ULMO in-place via hdiutil convert; temp-file naming is correct, callers handle codesigning after this returns.
scripts/build-sign-upload.sh Drops --codesign from create-dmg call (Homebrew variant has no --no-code-sign flag), adds strip step before bundle codesign, and signs DMG exactly once after ULMO conversion; ordering is correct.
.github/workflows/release.yml Adds strip step guarded by the same skip_all condition as surrounding steps, after Ghostty helper install and before arch verification; adds ULMO conversion inline before DMG codesign.
.github/workflows/nightly.yml Adds strip step with the same should_publish/still_current guard used by surrounding build steps; ULMO conversion inserted inline before DMG codesign in the packaging block.
cmux.xcodeproj/project.pbxproj Adds DEAD_CODE_STRIPPING = YES to both project-level Debug and Release build configurations; straightforward linker flag change.

Flowchart

%%{init: {'theme': 'neutral'}}%%
flowchart TD
    A[xcodebuild universal build] --> B[Install Ghostty CLI helper]
    B --> C[strip-app-binaries.sh\n-rSTx main exe + CLI\n-x dylib + plugin]
    C --> D[sign-cmux-bundle.sh\nCodesign app bundle]
    D --> E[xcrun notarytool\nNotarize app]
    E --> F[create-dmg\nCreate ULFO DMG]
    F --> G[convert-dmg-ulmo.sh\nhdiutil convert ULMO/LZMA]
    G --> H[codesign DMG\nSign once after conversion]
    H --> I[xcrun notarytool\nNotarize DMG]
    I --> J[xcrun stapler\nStaple ticket]
    J --> K[Upload release artifact\n102MB to 61MB]
Loading
%%{init: {'theme': 'base', 'themeVariables': {"darkMode": true, "background": "#0d1117", "primaryColor": "#21262d", "primaryTextColor": "#e6edf3", "primaryBorderColor": "#8b949e", "lineColor": "#8b949e", "textColor": "#e6edf3", "edgeLabelBackground": "#161b22", "actorBkg": "#21262d", "actorBorder": "#8b949e", "actorTextColor": "#e6edf3", "actorLineColor": "#8b949e", "signalColor": "#8b949e", "signalTextColor": "#e6edf3", "noteBkgColor": "#373320", "noteBorderColor": "#d4a72c", "noteTextColor": "#f0e6c0", "labelBoxBkgColor": "#21262d", "labelBoxBorderColor": "#8b949e", "labelTextColor": "#e6edf3", "loopTextColor": "#e6edf3", "activationBkgColor": "#30363d", "activationBorderColor": "#8b949e"}}}%%
flowchart TD
    A[xcodebuild universal build] --> B[Install Ghostty CLI helper]
    B --> C[strip-app-binaries.sh\n-rSTx main exe + CLI\n-x dylib + plugin]
    C --> D[sign-cmux-bundle.sh\nCodesign app bundle]
    D --> E[xcrun notarytool\nNotarize app]
    E --> F[create-dmg\nCreate ULFO DMG]
    F --> G[convert-dmg-ulmo.sh\nhdiutil convert ULMO/LZMA]
    G --> H[codesign DMG\nSign once after conversion]
    H --> I[xcrun notarytool\nNotarize DMG]
    I --> J[xcrun stapler\nStaple ticket]
    J --> K[Upload release artifact\n102MB to 61MB]
Loading

Reviews (3): Last reviewed commit: "Document create-dmg variant difference a..." | Re-trigger Greptile

Comment thread scripts/build-sign-upload.sh Outdated
echo "Creating DMG..."
rm -f cmux-macos.dmg
rm -f cmux-macos.dmg cmux-macos-ulmo.dmg
create-dmg --codesign "$SIGN_HASH" cmux-macos.dmg "$APP_PATH"

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

P2 create-dmg --codesign now signs a ULFO DMG that is immediately discarded by hdiutil convert, so the initial signature is never used. The re-sign after mv is what actually matters. The CI workflows already use --no-code-sign for this reason. Switching the local script to match avoids a wasted signing round-trip and keeps the intent clear.

Suggested change
create-dmg --codesign "$SIGN_HASH" cmux-macos.dmg "$APP_PATH"
create-dmg --no-code-sign cmux-macos.dmg "$APP_PATH"

Note: If this suggestion doesn't match your team's coding style, reply to this and let me know. I'll remember it for next time!

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Fixed in 39f2ac4, with one correction: build-sign-upload.sh uses the Homebrew (andreyvit) create-dmg, which has no --no-code-sign flag (that flag belongs to the npm create-dmg the workflows use), so the literal suggestion would have errored. Omitting --codesign is the equivalent there; the DMG is signed once, after the ULMO conversion.

— Claude Code

@chatgpt-codex-connector

Copy link
Copy Markdown

Codex Review: Didn't find any major issues. Nice work!

Reviewed commit: 94a34088ef

ℹ️ 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".

lawrencecchen and others added 2 commits July 5, 2026 20:19
Review feedback (CodeRabbit, Greptile): the DMG->ULMO convert/mv sequence was
duplicated in three release-critical paths, and build-sign-upload.sh signed a
DMG via create-dmg --codesign that hdiutil convert immediately discarded.
Centralize the conversion in scripts/convert-dmg-ulmo.sh and sign the DMG once,
after conversion. Note the Homebrew create-dmg used by build-sign-upload.sh has
no --no-code-sign flag (that is the npm create-dmg in the workflows); omitting
--codesign is the equivalent.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
The Homebrew create-dmg used by this manual release path signs opt-in via
--codesign (no --no-code-sign flag exists), unlike the npm create-dmg in the
CI workflows which signs by default. Three reviewers flagged the omitted
signing flag based on the npm tool's semantics; name the invariant inline.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
@lawrencecchen lawrencecchen added the stale-revisit Closed after 30+ days without activity; preserved for possible revisit or reopening. label Sep 23, 2026

This branch was successfully deployed

1 active deployment
Preview – cmux — d09834f7 Deployed Jul 6, 2026 by vercel[bot]
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

stale-revisit Closed after 30+ days without activity; preserved for possible revisit or reopening.

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants