Our Windows installer jumped from 277 MB in one release to 640 MB in the next. The issue we opened for it had a reasonable guess: the bundled Python runtime had grown, and maybe some model files had slipped in. Please find the biggest chunks and trim them.
That guess pointed at the right area, but the framing was wrong. The installer had not grown by about 360 MB. The archive inside it had grown to roughly 4.9 GB, and most of it had been cut off on the way into the .exe, with no error from the build. This post walks through how we found that out, what we changed, and what we still can't prove.
The symptom
We build a desktop browser on Electron and package the Windows build with electron-builder, which produces an NSIS installer. Inside the installer sits a 7z archive (app-64.7z) holding the actual app: the Electron binary, app.asar, and extra resources such as a Python runtime and local AI binaries.
The numbers from our GitHub releases:
- 0.1.56: setup 277 MB
- 0.1.58: setup 640 MB
- 0.1.60: setup 337 MB (after the fix)
Between 0.1.56 and 0.1.58 a few things had landed at once, including a local AI runtime folder shipped as an extra resource. So "something got bigger, find it" sounded like a sizing exercise: list the archive, sort by size, delete the fat.
Wrong lead: just list the archive
The first step was the obvious one. Extract app-64.7z from the installer and list its contents.
7-Zip extracted the archive from the installer without complaint: 639,394,190 bytes. Opening that archive was another matter:
Open ERROR: Cannot open the file as [7z] archive
ERRORS:
Unexpected end of archive
At this point it would have been easy to blame a bad download, or to assume the installer was fine and only our extraction tool was confused. We had a second wrong lead too: the release workflow run for that day had failed before the build step (missing signing secrets), yet an asset was uploaded anyway. That pointed to a local, manual build. It's a useful clue, but it doesn't explain why the archive is unreadable.
Root cause: a size that wrapped at 4 GiB
A 7z file starts with a 32-byte signature header. Among other things, it stores NextHeaderOffset and NextHeaderSize: where the archive's index (file names, sizes, folder structure) lives. That index sits at the very end of the file. The start header also carries its own CRC, so you can check whether those fields are genuine.
For the 0.1.58 archive:
- start header CRC: valid
-
NextHeaderOffset: 4,934,361,413 -
NextHeaderSize: 41 bytes (small, because 7z usually stores its index compressed) - expected end of archive: 32 + 4,934,361,413 + 41 = 4,934,361,486 bytes
- actual file size: 639,394,190 bytes
The difference between expected and actual size is 4,294,967,296 bytes. That's exactly 2^32.
So the archive electron-builder created was about 4.93 GB. What ended up in the installer was its first 639 MB: the true size modulo 2^32. The index at the end, which lists every file, is gone. That's why nothing could open it, and why we could not simply list what was inside.
A truncation that lands exactly on "size mod 2^32" points to a 32-bit size field somewhere in the packaging chain. (A FAT32 copy, by contrast, would fail or stop just below 4 GiB, not wrap around.) NSIS documents a 2 GB limit for installers and uses 32-bit integers internally, so it's the most likely place. We did not trace which exact step did the cut. What we can say is that the build finished, produced a 640 MB .exe, and printed no error.
The 640 MB number was therefore misleading in a specific way: the payload hadn't doubled, it had grown by more than an order of magnitude, and the visible size was what was left after the cut.
What was in those 4.9 GB?
This is the part we can't prove, and we want to be upfront about it. With the index gone, we can't list the 0.1.58 contents.
What we do know comes from the config. In 0.1.58 the local AI folder was copied into the app like this:
{
"from": "runtime/local-ai",
"to": "apps/desktop/runtime/local-ai",
"filter": ["**/*", "!**/*.partial"]
}
That's "everything in that folder except partial downloads". On a developer machine, that folder is also where model weights end up when you test local inference. Model weights are several gigabytes and barely compress. Combined with a manual build, this is the most plausible explanation for a 4.9 GB archive. It is plausible, not proven.
The fix
The change (PR #355, superseding #354) did two things in apps/desktop/package.json.
It excluded model weights from the copied local AI resources:
{
"from": "runtime/local-ai",
"to": "apps/desktop/runtime/local-ai",
- "filter": ["**/*", "!**/*.partial"]
+ "filter": ["**/*", "!**/*.partial", "!**/*.gguf", "!**/*.safetensors", "!**/models/**"]
},
And it excluded Python build inputs and sources from app.asar:
+ "!runtime/python/**",
+ "!castlabs-venv/**",
+ "!installer-asset-tools/**",
+ "!wheelhouse/**",
+ "!src/**",
Our files list already whitelists what goes into app.asar, so the second block is likely defensive rather than the cause. The weights filter is the change that matters.
A test now pins both filter groups, so a later config edit can't drop them quietly:
expect(localAiResource.filter).toEqual(expect.arrayContaining(['!**/*.gguf', '!**/*.safetensors', '!**/models/**']));
The result: the next beta came out at 300 MB, and 0.1.60 at 337 MB. In 0.1.60 the archive opens normally (13,398 files, about 1.1 GB unpacked, 336 MB packed). The local AI folder now holds only the llama.cpp and stable-diffusion runtime binaries, about 290 MB unpacked, and no .gguf or .safetensors files.
We're still about 60 MB above 0.1.56, and the original size issue is still open for the remaining trimming. The broken-archive part is done.
What we took away from this
Treat a size jump as a correctness signal first. We started out treating 640 MB as an optimisation problem. It was really a broken artifact. If an installer changes size a lot between releases, open the payload and check it before you start trimming.
A catch-all glob in a packaging filter is a promise about what's on the build machine. A **/* filter is fine on a clean CI runner. On a laptop where you've been testing local models, it copies whatever is lying around. If a folder can hold user-sized or test data, either whitelist what you ship or explicitly exclude the heavy file types.
Check the archive, not just the build exit code. Our build succeeded. If we had run 7z t on app-64.7z, or compared the archive size against a budget, the failure would have been obvious before release. Our fix added a config test, not an artifact check. A size or integrity check on the final payload is the stronger guard, and we'd recommend it to anyone shipping large Electron apps through NSIS.
When a number lands exactly on a power of two, look for an overflow. "Expected size minus actual size equals exactly 2^32" took a few lines of arithmetic to spot, and it reframed the whole problem. The 7z start header made that possible because it records where the archive is supposed to end, and its CRC told us we could trust that field.
We ran into this while building LastBrowser, an Electron-based desktop browser with a built-in AI assistant. Code is at github.com/Loggableim/lastbrowser, more at lastbrowser.com.
Top comments (0)