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:
- 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.
- 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.
Summary
dmg-builder@26.8.1produces 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 theElectron Frameworkbinary itself) were silently dropped.The bug is reproducible with raw shell commands and traces to two specific issues in the vendored
dmgbuildPython tool.Versions
electron-builder@26.8.1dmg-builder@26.8.1app-builder-bin@5.0.0-alpha.12dmgbuildfromelectron-userland/electron-builder-binariesreleasedmg-builder@1.2.0(commit75c8a6c), running on Python 3.14Symptom
A universal Electron app at ~2.1 GB unpacked is built into a DMG that compresses to ~1 GB. Running
hdiutil verifyon 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 MBElectron FrameworkMach-O binary at:The directory and symlinks exist; only the binary is gone. App crashes on launch with:
Root cause
Two interacting bugs in the bundled
dmgbuild(al45tair/dmgbuild):1. Size calculation is too small for HFS+
dmgbuild/core.py:474:total_sizeis in bytes; dividing by 1000 then formatting asK(whichhdiutilinterprets as 1024 bytes) gives a requested image size ofbytes × 1.024— only ~2.4% headroom over the raw payload. HFS+ catalog overhead for a deeply nested app bundle (Electron .app has 200+ smalllprojfiles plus large frameworks) easily exceeds that, leaving no room.2.
dittoexit code is not checkeddmgbuild/core.py:693:When
dittofails partway through withNo 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:Same image as
-fs APFS:dittoexits 0, copy is complete.Why this isn't always visible
Suggested fixes
In rough order of effort:
ditto's exit code incore.py:693. Switchsubprocess.calltosubprocess.check_call, or check the return value and raise. This alone would surface the failure loudly instead of producing a silently broken DMG.core.py:474.bytes / 1000should bebytes / 1024, or better, multiply by a real safety factor (e.g. 1.10) for HFS+ catalog overhead. Or default to-fs APFSwhen unspecified — APFS has dramatically lower overhead and the same image size succeeds where HFS+ fails.Workaround for downstream users
Set
dmg.sizeexplicitly inelectron-builderconfig to force a larger image: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:122has the line that would expose thefilesystemoption 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.