Skip to content

Add Guix-based reproducible build pipeline under contrib/guix#1

Closed
kim0 wants to merge 15 commits into
mainfrom
codex/fix-nondeterminism-in-randomx-rs-build
Closed

Add Guix-based reproducible build pipeline under contrib/guix#1
kim0 wants to merge 15 commits into
mainfrom
codex/fix-nondeterminism-in-randomx-rs-build

Conversation

@kim0
Copy link
Copy Markdown
Owner

@kim0 kim0 commented May 17, 2026

Motivation

  • Provide a Guix-first, reproducible release pipeline for building deterministic Linux artifacts for cuprated and pin the Guix environment and toolchain.
  • Build from a deterministic source archive with vendored Cargo dependencies and enforce deterministic build metadata and provenance.
  • Work around current randomx-rs behavior by patching vendored code to allow RANDOMX_ARCH/RANDOMX_DARCH to be honored during builds.

Description

  • Add a new contrib/guix directory including README.md, channels.scm, and manifest.scm to pin Guix channels and the runtime manifest.
  • Add helper scripts: guix-mk-distsrc to create a deterministic source archive, guix-build to run a Guix time-machine build, guix-checksums, guix-attest, and guix-verify for checksums/attestation/verification workflows.
  • Add build library scripts under contrib/guix/libexec: build.sh to drive the deterministic build inside the container, package.sh to create reproducible tar.gz archives, and patch-randomx.sh to patch vendored randomx-rs build scripts to respect RANDOMX_ARCH/RANDOMX_DARCH.
  • Add mk-distsrc to vendor Cargo dependencies, create .cuprate-distsrc.json, and produce a signed SHA256 summary, and add smoke-reproducible.sh as a local reproducibility smoke test.
  • Ensure deterministic build environment via SOURCE_DATE_EPOCH, path remapping (--remap-path-prefix/-ffile-prefix-map), CARGO_NET_OFFLINE, and fixed tar/gzip options to produce deterministic archives and metadata files like build-metadata.json, guix-describe.json, and toolchain versions.

Testing

  • No CI automated tests were executed as part of this change; the PR adds tooling to perform reproducibility checks locally.
  • Added a local reproducibility smoke test at contrib/guix/smoke-reproducible.sh that clones the repository, runs guix-mk-distsrc and guix-build, and compares resulting artifact checksums.
  • Provided verification helpers contrib/guix/guix-checksums, contrib/guix/guix-verify, and contrib/guix/guix-attest for automated checksum generation, verification, and attestation signing.

Ahmed Kamal added 14 commits May 17, 2026 15:32
- channels.scm: pinned commit 2f4f5d74fb... does not exist upstream
  (404 on both git.savannah.gnu.org and codeberg). Repin to the
  v1.5.0 tag commit 230aa373f3..., which actually exists.

- manifest.scm: there is no standalone `rust-cargo` Guix package.
  In Guix v1.5.0 the rust package has outputs ("out" "cargo"), so
  the right spec is `rust:cargo`. `rust-cargo` would fail
  `guix shell -m manifest.scm` with "no such package".

- libexec/patch-randomx.sh:
  * regenerate `.cargo-checksum.json` after rewriting build.rs;
    cargo --frozen verifies vendored-crate file hashes and would
    otherwise refuse with "checksum has changed".
  * collapse the duplicated ARCH/DARCH branches into one replacement
    string (the old DARCH branch wrote `"ARCH"` but the surrounding
    comment said DARCH — confusing; net effect was correct because
    upstream only has DARCH today, but the script would silently
    double-emit `"ARCH"` if upstream ever adds a real ARCH define).
  * treat an empty RANDOMX_ARCH / RANDOMX_DARCH as unset so the
    "default" fallback actually fires (guix-build exports
    RANDOMX_ARCH="" by default; env::var on an empty var returns
    Ok("") which previously skipped the or_else fallback).
  * fail the patch if neither ARCH nor DARCH define is found,
    instead of silently producing an unpatched vendored crate.
guix time-machine v1.5.0 refuses to authenticate a channel that lacks
an (introduction ...) field when the underlying git repo carries a
.guix-authorizations file (the official Guix repo does). Without this,
the canonical error is:

  guix time-machine: error: channel 'guix' lacks an introduction and
  cannot be authenticated

The values added are the canonical introduction commit + OpenPGP
fingerprint for the official Guix channel as documented upstream
(guix/channels.scm: %default-channels).
`guix shell --container --pure` mounts only the manifest's profile bin
on PATH; it does NOT provide /usr/bin/env or /bin/bash, so the
`#!/usr/bin/env bash` shebangs on mk-distsrc / build.sh / package.sh /
patch-randomx.sh make those scripts fail with 'command not found' when
guix-shell tries to execve them as a direct path.

Fix by invoking the inner scripts via the `bash` interpreter (which IS
in PATH via the manifest's bash package), bypassing the shebang. Four
call sites need this:

  guix-mk-distsrc        invokes mk-distsrc
  guix-build             invokes libexec/build.sh
  mk-distsrc             invokes libexec/patch-randomx.sh
  libexec/build.sh       invokes libexec/package.sh

Outside of guix-shell --container the original shebang still works
unchanged on any normal Linux host with /usr/bin/env.
`guix shell --container --pure` mounts the host worktree with stat
metadata that differs from what the index recorded on the host, so
`git diff-index --quiet` falsely reports the tree as dirty even when
content matches and `git status --porcelain` is empty.

Refresh the stat cache (`git update-index -q --refresh`) immediately
before the check, which is the canonical fix. Also dump
`git status --porcelain` and `git diff-index --name-only HEAD` on
genuine failure so a real divergence can be diagnosed without
re-running.
`guix shell --container --pure` runs commands as a mapped user without
privilege to chown into uid 0/gid 0. The unsuffixed `tar -xf - -C` in
mk-distsrc therefore fails on every entry with

  tar: <path>: Cannot change ownership to uid 0, gid 0: Invalid argument

eventually aborting with 'Exiting with failure status due to previous
errors'. Add --no-same-owner --no-same-permissions so tar uses the
extracting user's identity and ignores the archive's stored ownership
metadata (which is what we want here — the distsrc only cares about
file content and mtime, not who owned them at archive time).
build.sh extracts the deterministic source archive inside the same
guix-shell --container --pure environment that bit mk-distsrc, so it
needs the same --no-same-owner flag to avoid the chown EINVAL on every
entry.
cuprate's workspace deps require rustc up to 1.91 (fjall, lsm-tree,
typed-index-collections, monero-daemon-rpc, etc). Guix v1.5.0 only
packages rust up to 1.88, so the build aborts before any work with:

  error: rustc 1.85.1 is not supported by the following packages:
    fjall@3.0.4 requires rustc 1.91.0
    lsm-tree@3.0.4 requires rustc 1.91.0
    ...

Bump the channel pin to a recent master commit (7041be9c11, 2026-05-17)
that ships rust-1.93 as the default. Reproducibility is still preserved
because the pin is concrete.
Guix's `gcc-toolchain` profile only installs `gcc`/`g++`/`ar` etc.; it
doesn't ship the legacy `cc` alias. cc-rs (used transitively by
libsqlite3-sys, openssl-sys, randomx-rs, ring, and most other -sys
crates) defaults to `cc` and aborts with

  ToolNotFound: failed to find tool "cc": No such file or directory

before any C source is compiled. Set the canonical compiler env vars
explicitly so cc-rs picks up gcc/g++/ar/etc. from the manifest profile.
…penssl

Two related failures hit at the cargo build step:

  randomx-rs:  CMake Error: unable to find build program 'Unix Makefiles'
               (manifest had cmake but not make)

  openssl-sys: openssl-src tried to build OpenSSL from source and failed
               with 'Command "make" not found'

Both need `make` in the profile. Add gnu-make to manifest.scm.

For openssl-sys, also set OPENSSL_NO_VENDOR=1 so it links against the
openssl package already in the manifest via pkg-config instead of
recompiling OpenSSL via the openssl-src vendored copy on every build
(faster + smaller artifact + actually uses the audited Guix openssl).
Hint OPENSSL_DIR and PKG_CONFIG_PATH from $GUIX_ENVIRONMENT for
crates that don't use pkg-config.
s/gnu-make/make/ — the Guix package is named `make` (gnu/packages/base.scm)
not gnu-make, so the previous commit's manifest entry failed with
'gnu-make: unknown package'.
…c 15

GCC 15.2 (now the default rust toolchain dep) rejects the unqualified
`fesetround(mode)` call at instructions_portable.cpp:87 because the
only relevant include is <cfenv>, which scopes fesetround to std::.
RandomX upstream still expects the global-namespace version. Inject an
extra `#include <fenv.h>` after <cfenv> to put fesetround back in the
global namespace; behaviour is unchanged on older toolchains.

Extend the .cargo-checksum.json regeneration to cover the new patched
file as well.
The previous attempt (adding #include <fenv.h>) was insufficient under
libstdc++ + gcc 15: even with the C header included, the global-namespace
fesetround declaration is not reliably emitted, so compilation still
fails with

  instructions_portable.cpp: error: 'fesetround' was not declared
  in this scope

<cfenv> guarantees std::fesetround, so qualify the single call site
to std::fesetround(mode) and stop trying to coax the global-namespace
version into existence.
Root-caused the persistent RandomX 'fesetround was not declared' /
'fesetround is not a member of std' failures. The Guix gcc-15.2
libstdc++ ships with both _GLIBCXX_HAVE_FENV_H and _GLIBCXX_USE_C99_FENV
undefined in bits/c++config.h. As a result <cfenv> never include_next's
glibc's <fenv.h>, the std:: namespace gets none of the fenv functions,
and the libstdc++ <fenv.h> compat wrapper is also a no-op. There is no
way to call fesetround from C++ in this toolchain without these defines.

Set both macros via CXXFLAGS so RandomX's CMake build (and any other
C++ caller of <cfenv>) compiles. Verified locally: a minimal
`std::fesetround(0)` translation unit fails to compile without the
defines and succeeds with them.
`guix` is intentionally not in manifest.scm — the build container is
supposed to be isolated from host tooling — so the unconditional
`guix describe` call at the tail of build.sh aborted the build with
`guix: command not found` AFTER cargo had already compiled cuprated.

Fall back to a JSON stub that records the profile path and points at
channels.scm as the deterministic source of truth, which is what
verification consumers actually need. Keep the real `guix describe`
when guix IS available (e.g. someone running the script directly on a
Guix System host).
@kim0 kim0 closed this May 17, 2026
@kim0 kim0 deleted the codex/fix-nondeterminism-in-randomx-rs-build branch May 17, 2026 15:11
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant