Skip to content

ghc: deterministic link order + package.cache (reproducible Haskell) - #277

Merged
bryan-minimal merged 2 commits into
mainfrom
bryan/ghc-determinism
Jun 22, 2026
Merged

ghc: deterministic link order + package.cache (reproducible Haskell)#277
bryan-minimal merged 2 commits into
mainfrom
bryan/ghc-determinism

Conversation

@bryan-minimal

@bryan-minimal bryan-minimal commented Jun 19, 2026

Copy link
Copy Markdown
Member

What

Makes ghc (GHC 9.10.3) reproducible — and, because the fix is in the compiler/linker, it makes every downstream Haskell package (stack, haskell-language-server, …) reproducible too.

Diagnosis

GHC's functional output is already reproducible — .text/.rodata/.data are byte-identical across builds (even the 71 MB libHSghc.so). The entire residual is metadata ordering, from GHC iterating package/UnitId collections in hash-set order (nonDetEltsUFM), in two places:

  1. Link-arg / DT_NEEDED / .dynstr ordering in every linked binary (also what makes stack's identically-sized functions land at different addresses).
  2. ghc-pkg recachepackage.cache serialized in unsorted readdir order.

-fobject-determinism is not the lever here (codegen is already deterministic).

Fix (two source patches, applied before the Hadrian build)

  1. Link order — exact backport of upstream GHC #26838 / MR !15453 (a single sort $ on preload1 in compiler/GHC/Unit/State.hs). Merged upstream for 10.0.1; not in 9.10.x; Debian ships this exact patch for 9.10.3 (Debian #1125305). It restores the sorted-by-UnitId order GHC ≤ 9.6 had — behavior-restoring, low-risk.
  2. package.cache — sort the .conf list before it's read/serialized in utils/ghc-pkg/Main.hs (sort already imported). No upstream fix exists; trivial and additive. The build's existing post-install ghc-pkg recache then emits a byte-identical cache.

Each patch is guarded by a grep that fails the build loudly (in seconds) if a future GHC moves the target, rather than silently regressing.

Status — DRAFT pending verification

The patches are high-confidence (Debian-proven for this exact version, applied behind fail-fast guards) but not yet locally build-twice-verified — a from-scratch GHC build is multi-hour (build_cost_multiple=6), ×2 for a repro check. Marking draft until that verify (or CI / a beefy builder) confirms byte-identical output + ghc --version.

When minimal moves to GHC ≥ 10.0.1, drop patch (1) (it lands upstream); keep (2) (still unfixed upstream).

🤖 Generated with Claude Code

Summary by CodeRabbit

  • Chores
    • Improved build reproducibility by applying patches that ensure deterministic generation of GHC build outputs, with enhanced error checking to validate patch application.

GHC's code output is already reproducible; the residual is metadata
ordering from hash-set iteration of package/UnitId collections:
1. link-arg/DT_NEEDED/.dynstr order — backport of upstream GHC #26838 /
   MR !15453 (sort preload1 in compiler/GHC/Unit/State.hs); Debian ships
   this exact patch for 9.10.3.
2. ghc-pkg package.cache — sort the .conf list before serialize in
   utils/ghc-pkg/Main.hs (no upstream fix; sort already imported).
Both guarded by grep so a future GHC source change fails the build loudly.
Fixing the compiler propagates to all downstream Haskell (stack, HLS).

DRAFT: Debian-proven + guarded, but not yet local build-twice-verified
(GHC is a multi-hour build_cost_multiple=6 build).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
@coderabbitai

coderabbitai Bot commented Jun 19, 2026

Copy link
Copy Markdown
Contributor

Review Change Stack

📝 Walkthrough

Walkthrough

packages/ghc/build.sh adds 28 lines of post-extract source patching. Two guarded sed -i edits enforce deterministic ordering: one sorts the preload1 collection in compiler/GHC/Unit/State.hs, and one sorts .conf file paths in utils/ghc-pkg/Main.hs. Both patches verify expected patterns exist before modifying and call exit 1 on mismatch.

Changes

GHC Build Reproducibility Patches

Layer / File(s) Summary
Deterministic sort patches for Unit.State and ghc-pkg
packages/ghc/build.sh
Adds two guarded sed -i rewrites with pre-checks: sorts preload1 in compiler/GHC/Unit/State.hs for deterministic link ordering, and sorts .conf file paths in utils/ghc-pkg/Main.hs for deterministic package.cache generation. Both patches exit 1 if expected source patterns or imports are not found.

Estimated code review effort

🎯 2 (Simple) | ⏱️ ~10 minutes

Suggested reviewers

  • norrietaylor
  • twitchyliquid64

Poem

🐇 A bunny sorts the Haskell files,
No more non-deterministic wiles!
With sed and sort and guards in place,
Each build now runs at the same pace.
Reproducibility wins the race! 🏁

🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The pull request title directly and specifically describes the main changes: making GHC deterministic through link order and package.cache sorting for reproducible Haskell builds.
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.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ 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 bryan/ghc-determinism

Comment @coderabbitai help to get the list of available commands and usage tips.

@edge-delta edge-delta Bot left a comment

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.

Overall this is a well-structured patch — fail-fast guards before expensive builds are exactly right, the upstream provenance is well-documented, and the scope is appropriately narrow. One gap in the guard logic for patch (2) noted inline.

@edge-delta edge-delta Bot left a comment

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.

Inline suggestion on the ghc-pkg guard completeness.

Comment thread packages/ghc/build.sh
Comment on lines +31 to +33
ghc_pkg_main=utils/ghc-pkg/Main.hs
grep -q 'confs = map (path </>) $ filter (".conf" `isSuffixOf`) fs' "$ghc_pkg_main" \
|| { echo "ERROR: ghc-pkg package.cache patch target not found in $ghc_pkg_main — GHC source changed." >&2; exit 1; }

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.

Guard gap: Unlike patch (1) which explicitly adds sort to the Data.List import via sed, patch (2) relies on the assumption that sort is already in scope in Main.hs. The grep guard here only verifies the target line exists — it doesn't verify sort is importable.

If sort happens NOT to be in scope (e.g., a future GHC version reorganizes imports in ghc-pkg/Main.hs), the build would fail with a Haskell compile error deep into the multi-hour Hadrian build rather than failing early at this guard.

Consider adding a guard like:

grep -q 'import Data.List' "$ghc_pkg_main" \
  || { echo "ERROR: Data.List (sort) not imported in $ghc_pkg_main — add sort import." >&2; exit 1; }

Or, more robustly, mirror the patch (1) pattern: explicitly sed in a sort import if it's not already present, so the patch is self-contained regardless of upstream import state.

@bryan-minimal

Copy link
Copy Markdown
Member Author

Good catch, thanks — agreed, the patch-(2) guard checks the target line but not that sort is importable. For GHC 9.10.3 specifically it's safe: utils/ghc-pkg/Main.hs:78 already has import Data.List ( group, sort, sortBy, nub, partition, find … ), so sort is in scope and the patch compiles (verified against the cached 9.10.3 source). So the current build-twice (running now) is fine.

I'll add the fail-fast import guard to harden against a future GHC reorg — e.g. grep -qE 'import Data.List \(.*\bsort\b' "$ghc_pkg_main" before the sed (or mirror patch (1) and sed the import in if absent). I'm holding the edit until the in-flight overnight build-twice finishes, so the two builds use an identical build.sh; will fold it in when I un-draft.

Per review on #277: patch (2) relies on 'sort' being in scope in
utils/ghc-pkg/Main.hs. 9.10.3 imports it (Main.hs:78), but add a grep
guard so a future GHC import reorg fails fast here rather than deep in
the multi-hour Hadrian build. Output-neutral; verified result unchanged.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
@bryan-minimal
bryan-minimal marked this pull request as ready for review June 19, 2026 16:03
@bryan-minimal

Copy link
Copy Markdown
Member Author

Verified reproducible. Overnight build-twice (aarch64, 8 CPU/28G) came back byte-identical: 6602/6602 files (repro-check diff). Both patches confirmed working. Un-drafted, and folded in the sort-import guard from the review above (output-neutral, so the verified result stands). This also unblocks the rest of the Haskell tail — stack and HLS build on this compiler.

@coderabbitai coderabbitai Bot left a comment

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.

Actionable comments posted: 1

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (2)
packages/ghc/build.sh (2)

5-5: ⚠️ Potential issue | 🟡 Minor | ⚡ Quick win

Use tar -xf as required by the packaging guideline.

The extraction command uses tar -xof; the guideline explicitly requires tar -xf for manual tarball extraction in build.sh.

As per coding guidelines, “When extracting tarballs manually, add 'tar' to build_deps and use 'tar -xf' in build.sh.”

🤖 Prompt for 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.

In `@packages/ghc/build.sh` at line 5, The tar extraction command in the build.sh
file currently uses the tar -xof flags, but according to packaging guidelines,
it should use tar -xf instead. Change the tar command that extracts the
ghc-${MINIMAL_ARG_VERSION}-src.tar.xz archive from tar -xof to tar -xf to comply
with the required packaging standard.

Source: Coding guidelines


65-89: ⚠️ Potential issue | 🟠 Major | ⚡ Quick win

Missing required deterministic C/C++ toolchain flags for this build path.

The script invokes GHC/Hadrian build steps that include C/C++ compilation, but required deterministic flags are not exported in this script (CFLAGS/CXXFLAGS, LDFLAGS, ARFLAGS). Add the mandated exports before configure/build.

As per coding guidelines, “For C/C++ builds, apply compiler flags: CFLAGS/CXXFLAGS with '-ffile-prefix-map=$(pwd)=/builddir -gno-record-gcc-switches', LDFLAGS with '-Wl,--build-id=none', and ARFLAGS='Drc'.”

🤖 Prompt for 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.

In `@packages/ghc/build.sh` around lines 65 - 89, The build script is missing
required deterministic C/C++ toolchain flags that need to be exported before the
configure and hadrian build steps. Add export statements for CFLAGS, CXXFLAGS,
LDFLAGS, and ARFLAGS before the ./configure call (which configures GHC). Set
CFLAGS and CXXFLAGS to include '-ffile-prefix-map=$(pwd)=/builddir
-gno-record-gcc-switches', LDFLAGS to '-Wl,--build-id=none', and ARFLAGS to
'Drc' to ensure deterministic builds and proper toolchain behavior for the
GHC/Hadrian compilation.

Source: Coding guidelines

🤖 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 `@packages/ghc/build.sh`:
- Around line 12-39: The build.sh script applies reproducibility patches to GHC
source code but does not include verification that these patches actually
achieve byte-for-byte reproducibility. After the sed commands that apply patches
(2) to both $ghc_state and $ghc_pkg_main files, add a verification step that
builds the package twice to separate output directories and compares them
byte-for-byte using diff or similar tools to ensure the resulting binaries and
artifacts are identical. If the comparison fails, the script should report an
error indicating the reproducibility goal was not met.

---

Outside diff comments:
In `@packages/ghc/build.sh`:
- Line 5: The tar extraction command in the build.sh file currently uses the tar
-xof flags, but according to packaging guidelines, it should use tar -xf
instead. Change the tar command that extracts the
ghc-${MINIMAL_ARG_VERSION}-src.tar.xz archive from tar -xof to tar -xf to comply
with the required packaging standard.
- Around line 65-89: The build script is missing required deterministic C/C++
toolchain flags that need to be exported before the configure and hadrian build
steps. Add export statements for CFLAGS, CXXFLAGS, LDFLAGS, and ARFLAGS before
the ./configure call (which configures GHC). Set CFLAGS and CXXFLAGS to include
'-ffile-prefix-map=$(pwd)=/builddir -gno-record-gcc-switches', LDFLAGS to
'-Wl,--build-id=none', and ARFLAGS to 'Drc' to ensure deterministic builds and
proper toolchain behavior for the GHC/Hadrian compilation.
🪄 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: CHILL

Plan: Pro

Run ID: 3a7515bd-b817-4f9f-bf39-2ce3aa112d2d

📥 Commits

Reviewing files that changed from the base of the PR and between b265510 and 9555deb.

📒 Files selected for processing (1)
  • packages/ghc/build.sh

Comment thread packages/ghc/build.sh
Comment on lines +12 to +39
# Reproducibility: GHC iterates package/UnitId collections in hash-set order,
# making the linker arg / DT_NEEDED / .dynstr order in every binary AND the
# ghc-pkg package.cache non-deterministic. Code (.text/.rodata) is already
# byte-identical; this is pure ORDERING. Sort the two collections.
#
# (1) Link order — backport of upstream GHC #26838 / MR !15453 (merged for
# 10.0.1; NOT in 9.10.x; Debian ships this exact patch for 9.10.3). Restores
# the sorted-by-UnitId order GHC <= 9.6 had.
ghc_state=compiler/GHC/Unit/State.hs
grep -q 'import Data.List ( intersperse, partition, sortBy, isSuffixOf, sortOn )' "$ghc_state" \
&& grep -q 'let preload1 = nonDetKeysUniqMap (filterUniqMap (isJust . uv_explicit) vis_map)' "$ghc_state" \
|| { echo "ERROR: GHC link-order patch targets not found in $ghc_state — GHC source changed; revisit the #26838 backport." >&2; exit 1; }
sed -i 's/import Data.List ( intersperse, partition, sortBy, isSuffixOf, sortOn )/import Data.List ( intersperse, partition, sortBy, isSuffixOf, sortOn, sort )/' "$ghc_state"
sed -i 's/let preload1 = nonDetKeysUniqMap (filterUniqMap (isJust . uv_explicit) vis_map)/let preload1 = sort $ nonDetKeysUniqMap (filterUniqMap (isJust . uv_explicit) vis_map)/' "$ghc_state"

# (2) ghc-pkg package.cache — sort the .conf list before it is read + serialized
# so the post-build `ghc-pkg recache` emits a byte-identical cache regardless
# of filesystem readdir order. (No upstream fix exists; `sort` already
# imported in Main.hs.)
ghc_pkg_main=utils/ghc-pkg/Main.hs
grep -q 'confs = map (path </>) $ filter (".conf" `isSuffixOf`) fs' "$ghc_pkg_main" \
|| { echo "ERROR: ghc-pkg package.cache patch target not found in $ghc_pkg_main — GHC source changed." >&2; exit 1; }
# The `sort $` below needs `sort` in scope; 9.10.3's Main.hs imports it (line 78),
# but guard it so a future GHC import reorg fails here, not deep in the build.
grep -qE 'import Data.List \(.*\bsort\b' "$ghc_pkg_main" \
|| { echo "ERROR: 'sort' not imported in $ghc_pkg_main — patch (2) requires it (add a sort import)." >&2; exit 1; }
sed -i 's#confs = map (path </>) $ filter (".conf" `isSuffixOf`) fs#confs = map (path </>) $ sort $ filter (".conf" `isSuffixOf`) fs#' "$ghc_pkg_main"

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.

⚠️ Potential issue | 🟠 Major | 🏗️ Heavy lift

Add the required build-twice reproducibility verification step.

This script improves ordering determinism, but it still does not perform the required “build twice and compare two $OUTPUT_DIR trees byte-for-byte” verification step in build.sh. Please add that explicit check before considering this reproducibility work complete.

As per coding guidelines, “Verify reproducibility by building the package twice and comparing the two $OUTPUT_DIR trees for byte-for-byte identity.”

🤖 Prompt for 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.

In `@packages/ghc/build.sh` around lines 12 - 39, The build.sh script applies
reproducibility patches to GHC source code but does not include verification
that these patches actually achieve byte-for-byte reproducibility. After the sed
commands that apply patches (2) to both $ghc_state and $ghc_pkg_main files, add
a verification step that builds the package twice to separate output directories
and compares them byte-for-byte using diff or similar tools to ensure the
resulting binaries and artifacts are identical. If the comparison fails, the
script should report an error indicating the reproducibility goal was not met.

Source: Coding guidelines

@bryan-minimal
bryan-minimal added this pull request to the merge queue Jun 22, 2026
Merged via the queue into main with commit 9101e0f Jun 22, 2026
4 checks passed
@bryan-minimal
bryan-minimal deleted the bryan/ghc-determinism branch June 22, 2026 17:32
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants