CI: UPX-compress Linux release binaries (150 MB → 49 MB)#1701
Draft
CI: UPX-compress Linux release binaries (150 MB → 49 MB)#1701
Conversation
Two layered size-reduction changes for the non-Windows release binaries. Windows is intentionally excluded from UPX because the AV false-positive surface (Defender, CrowdStrike, corporate proxies, SmartScreen) would disrupt distribution at our scale. 1. Enable split-sections on macOS. Linux and Windows cabal project files already set split-sections: True; macOS was the outlier. With split-sections the compiler emits each top-level binding into its own ELF/Mach-O section, which lets the linker GC unused code at function granularity instead of per-module. Expected savings: 10–20 MB on macOS. 2. UPX compression on macOS and Linux (amd64). Install upx via brew on macOS and apt on ubuntu-latest. Run upx --best --lzma against fossa, diagnose, and millhone after strip and before codesign (so the signature covers the compressed binary). rendergraph is skipped because UPX's self-modification can conflict with its stdin-reading main. LinuxARM is skipped because the runner doesn't have apt; revisit if this ships. Post-UPX smoke-launch verifies the compressed binary boots before any signing attempt. 3. Temporary signing trigger on this branch. The Sign and Notarize step previously ran only on tag pushes. To validate that codesign + Apple notarization survive UPX-packed Mach-O, this branch is added as a secondary trigger. MUST be reverted before merge — tag pushes should be the only production signing trigger. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Linux job runs inside fossa/haskell-static-alpine via container: at job level; every step runs in Alpine where apt-get does not exist and the user is root. First CI run failed with apt-get: command not found at this step and cancelled the whole matrix. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
UPX 5.x refuses to pack Mach-O without --force-macos. Stacking UPX's own experimental macOS support on top of the hardened-runtime notarization question makes failures ambiguous — if the end-to-end chain breaks we can't tell whether UPX, the runtime, or the notary is the culprit. Keep the macOS size reduction via split-sections only (already in cabal.project.ci.macos on this branch). Drop the temporary signing-on-this-branch trigger since there is no UPX on macOS to sign-test. Revisit macOS UPX as its own spike if/when Linux UPX is proven. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
GHC-74335: -fsplit-sections is not useful on this platform since it uses subsections-via-symbols. Ignoring. macOS's Mach-O linker already does function-granular dead-code elimination via ld64's subsections-via-symbols feature, so GHC emits -Winconsistent-flags when -fsplit-sections is requested. Our CI has -Werror, so that warning kills the build. The original assumption that macOS was 'missing' split-sections was wrong — macOS doesn't need it. The ~10-20 MB savings I'd projected for the macOS binary were already baked in by default. Replace the setting with a comment recording the reason so the next person looking at this doesn't repeat the mistake. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Two CI-time optimizations for the Linux UPX step. 1. Cache the packed output keyed on the pre-UPX content hash. When a build produces the same uncompressed fossa/diagnose/millhone triple as a prior build — same deps, same source, same strip output — we skip re-running UPX and restore the packed copies straight from the cache. actions/cache saves at end-of-job on a cache miss, so future matching builds hit. On cache hit, Install UPX and the compression step are both skipped; the packed binaries are ready after the restore step. Key is `sha256(sha256(fossa) || sha256(diagnose) || sha256(millhone))` of the uncompressed inputs; any one of them changing invalidates the triple. Single combined key keeps the yaml simple and the three-binary UPX step is coupled anyway. 2. Drop --lzma in favour of UPX's default NRV compression. Cache misses now pay ~20s instead of ~60s per job for ~2.5% larger packed output. On a 150 MB fossa that is ~4 MB — cheap insurance given LZMA's 3× slowdown on every cache miss. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Prefer the best compression ratio over CI time on cache miss. With the packed-binary cache in place, the ~60s LZMA cost is paid only when source actually changes; cache hits pay zero regardless. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Overview
Enables UPX compression on the Linux release binaries. macOS and Windows intentionally skipped (reasons below).
Still draft pending a decision on whether Linux-only is worth shipping on its own; the spike answer is "yes UPX works on Linux, no macOS is not worth the risk surface."
What ships
UPX on Linux amd64 (and Linux-arm via the same gate)
upxviaapk add --no-cache upxinside the existing Alpine build container (Linux runs infossa/haskell-static-alpine:ghc-9.8.4).upx --best --lzmaagainstfossa,diagnose, andmillhoneafter strip.rendergraphskipped — its main reads from stdin, and UPX's self-modifying stub is more brittle for piped tools.Real compression (measured on this branch's green CI run):
UPX compression itself takes ~60 seconds in CI.
What doesn't ship (and why)
macOS UPX — dropped after first CI run
UPX 5.x (brew's current version) refuses Mach-O with
CantPackException: macOS is currently not supported (try --force-macos). Stacking UPX's own experimental macOS path on top of the hardened-runtime notarization question made failures diagnostically ambiguous — if--force-macosproduced a binary Apple's notary rejected, we couldn't tell if UPX, the runtime, or the notary was the culprit. Separate spike if macOS matters enough.macOS split-sections — tried, didn't work
Original plan was to also enable
split-sections: Trueincabal.project.ci.macos, matching Linux/Windows. CI showed why this was never set:Mach-O's
ld64already does function-granular dead-code elimination viasubsections-via-symbolsby default. The ~10–20 MB I projected for macOS was imaginary — that saving was already baked in. Replaced with an explanatory comment in the cabal project file so nobody repeats the mistake.Windows UPX — excluded on principle
AV false-positive surface at our distribution scale: Defender ML (
Trojan:Win32/Wacatac.H!ml), corporate EDR (CrowdStrike/SentinelOne), Zscaler-class proxies, and SmartScreen all flag UPX-packed Windows binaries regardless of contents. Across thousands of CI environments the support burden would outweigh the ~100 MB saving.LinuxARM UPX — deferred
Linux-arm passed the gate (
matrix.os-name == 'Linux') doesn't include it, so the current workflow compresses only Linux amd64. ARM would need the equivalent apk install inside its own container step; boring but real work.Acceptance criteria
fossaartifact is noticeably smaller (observed 49 MB vs 150 MB baseline)../fossa --versionlaunches successfully against the UPX-packed binary (smoke-launch step).Testing plan
CI
Manual (before un-drafting)
Linux-binariesartifact from the latest build on this branch.file fossashould report a UPX-packed executable.docker run --rm -v $(pwd):/w -w /w alpine:3.19 ./fossa --versionor equivalent).Risks
fossa --versionin a hot loop (unusual), invisible in normal CI use.ldd,strings, and antivirus heuristics see the UPX stub, not the inner binary. Debug workflow changes: needupx -dto inspect.Out of scope
--force-macosUPX spike (separate issue if pursued).References
__text81 MB +__const75 MB +__data8.5 MB as the macOS 177 MB dominant contributors.Checklist
.fossa.yml/fossa-deps/ subcommand changes.