Skip to content

DMG silently drops files when packaging large apps on HFS+ (dmgbuild size calc + unchecked ditto exit code) #9706

@medha

Description

@medha

Summary

dmg-builder@26.8.1 produces DMGs that are missing files for apps in the ~1.9 GB+ range, with no error reported during the build. The resulting DMG installs cleanly but the app crashes on launch because critical Mach-O binaries (in our case the Electron Framework binary itself) were silently dropped.

The bug is reproducible with raw shell commands and traces to two specific issues in the vendored dmgbuild Python tool.

Versions

  • electron-builder@26.8.1
  • dmg-builder@26.8.1
  • app-builder-bin@5.0.0-alpha.12
  • Bundled dmgbuild from electron-userland/electron-builder-binaries release dmg-builder@1.2.0 (commit 75c8a6c), running on Python 3.14
  • macOS 26.2 (build 25C56), Apple Silicon

Symptom

A universal Electron app at ~2.1 GB unpacked is built into a DMG that compresses to ~1 GB. Running hdiutil verify on the DMG reports valid. Mounting the DMG and listing the .app contents shows 234 files vs 235 in the source — exactly one missing, the 178 MB / 375 MB Electron Framework Mach-O binary at:

Keel.app/Contents/Frameworks/Electron Framework.framework/Versions/A/Electron Framework

The directory and symlinks exist; only the binary is gone. App crashes on launch with:

Library not loaded: @rpath/Electron Framework.framework/Electron Framework
Reason: tried: '...Electron Framework.framework/Electron Framework' (no such file)

Root cause

Two interacting bugs in the bundled dmgbuild (al45tair/dmgbuild):

1. Size calculation is too small for HFS+

dmgbuild/core.py:474:

total_size = str(max(total_size / 1000, 1024)) + "K"

total_size is in bytes; dividing by 1000 then formatting as K (which hdiutil interprets as 1024 bytes) gives a requested image size of bytes × 1.024 — only ~2.4% headroom over the raw payload. HFS+ catalog overhead for a deeply nested app bundle (Electron .app has 200+ small lproj files plus large frameworks) easily exceeds that, leaving no room.

2. ditto exit code is not checked

dmgbuild/core.py:693:

subprocess.call(["/usr/bin/ditto", f, f_in_image])

When ditto fails partway through with No space left on device (which happens reliably given #1), the script proceeds to the next stages — finalizing the DMG, signing, etc. — without any error. Files that didn't make it into the RW image are silently absent from the final DMG.

Reproduction without electron-builder

Confirms it's the dmgbuild path, not raw hdiutil:

# This succeeds — raw hdiutil is fine
hdiutil create -volname "Test" -srcfolder /path/to/Keel.app -ov -format UDZO /tmp/raw.dmg

# This reproduces the silent ditto failure on HFS+
hdiutil create -size 2g -fs HFS+ -volname "Test HFS" -ov /tmp/test.dmg
hdiutil attach /tmp/test.dmg -mountpoint /tmp/mount -nobrowse
/usr/bin/ditto /path/to/Keel.app /tmp/mount/Keel.app
echo "exit: $?"  # 1, with dozens of "No space left on device" messages
ls -la "/tmp/mount/Keel.app/Contents/Frameworks/Electron Framework.framework/Versions/A/" 
# Framework binary missing or truncated

Same image as -fs APFS: ditto exits 0, copy is complete.

Why this isn't always visible

  • Apps under ~1.5 GB unpacked have enough headroom that the 2.4% margin doesn't get eaten by HFS+ overhead. They build fine.
  • The bug ships silent; we discovered it because the resulting DMG crashed on launch with a missing dyld error. Anyone whose app grows past ~1.5–2 GB unpacked will trip this without warning.

Suggested fixes

In rough order of effort:

  1. Check ditto's exit code in core.py:693. Switch subprocess.call to subprocess.check_call, or check the return value and raise. This alone would surface the failure loudly instead of producing a silently broken DMG.
  2. Fix the size formula in core.py:474. bytes / 1000 should be bytes / 1024, or better, multiply by a real safety factor (e.g. 1.10) for HFS+ catalog overhead. Or default to -fs APFS when unspecified — APFS has dramatically lower overhead and the same image size succeeds where HFS+ fails.

Workaround for downstream users

Set dmg.size explicitly in electron-builder config to force a larger image:

dmg: {
  size: '3g',  // or larger; 'shrink: true' (default) shrinks the final DMG anyway
  // ...
}

This buys headroom but doesn't fix the underlying unchecked-exit-code or size-formula bugs — a future growth spurt would trip the same silent failure.

Related

While investigating, I also noticed dmg-builder/out/dmgUtil.js:122 has the line that would expose the filesystem option to user config commented out:

// filesystem: specification.filesystem || "HFS+",

If that's intentional, fine; if accidental, it would also be a useful workaround (set filesystem: 'APFS' and the size issue largely goes away).

Happy to send a PR for #1 (check_call) if useful — that one is uncontroversial and would catch silent failures from any source, not just this size mismatch.

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type
    No fields configured for issues without a type.

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions