Skip to content

Commit 71199dd

Browse files
committed
feat: add Guix reproducible build system
1 parent 243015b commit 71199dd

12 files changed

Lines changed: 1992 additions & 0 deletions

File tree

contrib/guix/.gitignore

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
# Pre-fetched dependency caches (large, fetched by supplementary/deps/ scripts)
2+
depends/
3+
4+
# Build output directories
5+
guix-build-*/

contrib/guix/README.md

Lines changed: 55 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,55 @@
1+
# Guix Reproducible Builds for Stack Wallet
2+
Build infrastructure for producing reproducible (deterministic) Linux x86_64 builds of Stack Wallet inside a Guix container.
3+
4+
Based on Bitcoin Core's approach (`contrib/guix/`).
5+
6+
## Prerequisites
7+
- [GNU Guix](https://guix.gnu.org/) installed (via the shell installer or distro package)
8+
- GPG key (for signing attestations)
9+
- ~20 GB disk space for dependency caches
10+
11+
## Quick Start
12+
```bash
13+
# 1. Fetch all dependencies (requires network)
14+
supplementary/deps/fetch-pub-deps.sh
15+
supplementary/deps/fetch-cargo-deps.sh
16+
17+
# 2. Verify dependency hashes
18+
supplementary/deps/verify-deps.sh
19+
20+
# 3. Build (network-isolated, deterministic: reproducible)
21+
./guix-build
22+
23+
# 4. Sign the build output
24+
./guix-attest
25+
26+
# 5. (Other builders) Verify attestations match
27+
./guix-verify
28+
```
29+
30+
## Build Variants
31+
Set `APP_NAME_ID` to select the variant:
32+
| Variant | `APP_NAME_ID` | Rust Plugins |
33+
|----------------|----------------|--------------------------------|
34+
| Stack Wallet | `stack_wallet` | epiccash, mwc, frostdart |
35+
| Stack Duo | `stack_duo` | frostdart |
36+
| Campfire | `campfire` | (none) |
37+
38+
## Environment Variables
39+
| Variable | Default | Description |
40+
|---------------------|-------------------|-------------------------------------|
41+
| `APP_NAME_ID` | `stack_wallet` | Build variant |
42+
| `APP_VERSION` | from pubspec.yaml | Version string (e.g. `2.3.4`) |
43+
| `APP_BUILD_NUMBER` | from pubspec.yaml | Build number (e.g. `234`) |
44+
| `JOBS` | `$(nproc)` | Parallel job count |
45+
| `HOSTS` | `x86_64-linux-gnu`| Target triplet(s) |
46+
| `SOURCE_DATE_EPOCH` | from git log | Timestamp for determinism |
47+
| `BASE_CACHE` | `depends` | Path to pre-fetched dependency dir |
48+
49+
## Known Limitations
50+
- Flutter SDK is a hash-pinned binary input, not built from source.
51+
- Pre-built native `.so` libs (Monero, Wownero, Salvium, Tor, etc.) ship in pub
52+
packages and are accepted as-is with hash verification.
53+
A future phase will see these dependencies also built via the same method.
54+
- Linux x86_64 only (no cross-compilation).
55+
- Flutter AOT reproducibility is unverified and may need investigation.

contrib/guix/guix-attest

Lines changed: 102 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,102 @@
1+
#!/usr/bin/env bash
2+
# Copyright (c) Stack Wallet developers
3+
# Distributed under the MIT software license, see the accompanying
4+
# file COPYING or https://opensource.org/licenses/MIT.
5+
#
6+
# guix-attest — Collect SHA256SUMS from build outputs and GPG-sign them.
7+
#
8+
# Usage: ./guix-attest [--signer <gpg-key-id>]
9+
#
10+
# Adapted from Bitcoin Core's contrib/guix/guix-attest.
11+
12+
set -euo pipefail
13+
14+
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
15+
source "${SCRIPT_DIR}/libexec/prelude.bash"
16+
17+
SIGNER="${SIGNER:-}"
18+
19+
while [ $# -gt 0 ]; do
20+
case "$1" in
21+
--signer) SIGNER="$2"; shift 2 ;;
22+
--help|-h)
23+
echo "Usage: guix-attest [--signer <gpg-key-id>]"
24+
echo ""
25+
echo "Collects SHA256SUMS from the most recent guix-build output"
26+
echo "and creates a GPG-signed attestation."
27+
exit 0
28+
;;
29+
*) die "Unknown option: $1" ;;
30+
esac
31+
done
32+
33+
################
34+
# Find output #
35+
################
36+
37+
# Find the most recent guix-build-* directory.
38+
BUILD_DIR="$(ls -dt "${SCRIPT_DIR}"/guix-build-* 2>/dev/null | head -1)"
39+
if [ -z "$BUILD_DIR" ] || [ ! -d "$BUILD_DIR" ]; then
40+
die "No guix-build-* directory found. Run guix-build first."
41+
fi
42+
43+
log_info "Using build output: ${BUILD_DIR}"
44+
45+
################
46+
# Collect sums #
47+
################
48+
49+
SUMS_FILE="${BUILD_DIR}/SHA256SUMS"
50+
51+
if [ ! -s "$SUMS_FILE" ]; then
52+
# Rebuild from .part files.
53+
: > "$SUMS_FILE"
54+
while IFS= read -r -d '' part; do
55+
cat "$part" >> "$SUMS_FILE"
56+
done < <(find "$BUILD_DIR" -name "SHA256SUMS.part" -print0 | sort -z)
57+
fi
58+
59+
if [ ! -s "$SUMS_FILE" ]; then
60+
die "No SHA256SUMS found. Build may have failed."
61+
fi
62+
63+
log_info "SHA256SUMS:"
64+
cat "$SUMS_FILE"
65+
echo ""
66+
67+
################
68+
# GPG sign #
69+
################
70+
71+
# Determine signer identity.
72+
if [ -z "$SIGNER" ]; then
73+
# Try to get default GPG key.
74+
SIGNER="$(gpg --list-secret-keys --keyid-format long 2>/dev/null \
75+
| grep '^sec' | head -1 | awk '{print $2}' | cut -d'/' -f2)" || true
76+
fi
77+
78+
if [ -z "$SIGNER" ]; then
79+
log_error "No GPG key found. Specify --signer <key-id> or set SIGNER env var."
80+
log_info "SHA256SUMS written to ${SUMS_FILE} (unsigned)"
81+
exit 1
82+
fi
83+
84+
log_info "Signing with GPG key: ${SIGNER}"
85+
86+
# Create detached signature.
87+
SIG_FILE="${SUMS_FILE}.asc"
88+
gpg --detach-sign --armor --local-user "$SIGNER" --output "$SIG_FILE" "$SUMS_FILE"
89+
90+
log_info "Attestation created:"
91+
log_info " Checksums: ${SUMS_FILE}"
92+
log_info " Signature: ${SIG_FILE}"
93+
94+
# Also create an attestation directory for multi-signer workflows.
95+
ATTEST_DIR="${BUILD_DIR}/attestations"
96+
mkdir -p "${ATTEST_DIR}/${SIGNER}"
97+
cp "$SUMS_FILE" "${ATTEST_DIR}/${SIGNER}/SHA256SUMS"
98+
cp "$SIG_FILE" "${ATTEST_DIR}/${SIGNER}/SHA256SUMS.asc"
99+
100+
log_info " Attestation dir: ${ATTEST_DIR}/${SIGNER}/"
101+
log_info ""
102+
log_info "Share the attestations/ directory with other builders for verification."

contrib/guix/guix-build

Lines changed: 155 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,155 @@
1+
#!/usr/bin/env bash
2+
# Copyright (c) Stack Wallet developers
3+
# Distributed under the MIT software license, see the accompanying
4+
# file COPYING or https://opensource.org/licenses/MIT.
5+
#
6+
# guix-build — Outer orchestrator for reproducible Stack Wallet builds.
7+
#
8+
# Usage: ./guix-build [--app stack_wallet|stack_duo|campfire]
9+
# [--jobs N]
10+
# [--version X.Y.Z]
11+
# [--build-number NNN]
12+
13+
set -euo pipefail
14+
15+
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
16+
source "${SCRIPT_DIR}/libexec/prelude.bash"
17+
18+
################
19+
# CLI args #
20+
################
21+
22+
while [ $# -gt 0 ]; do
23+
case "$1" in
24+
--app) APP_NAME_ID="$2"; shift 2 ;;
25+
--jobs) JOBS="$2"; shift 2 ;;
26+
--version) APP_VERSION="$2"; shift 2 ;;
27+
--build-number) APP_BUILD_NUMBER="$2"; shift 2 ;;
28+
--hosts) HOSTS="$2"; shift 2 ;;
29+
--help|-h)
30+
echo "Usage: guix-build [--app NAME] [--jobs N] [--version X.Y.Z] [--build-number NNN]"
31+
exit 0
32+
;;
33+
*) die "Unknown option: $1" ;;
34+
esac
35+
done
36+
37+
auto_detect_version
38+
compute_source_date_epoch
39+
40+
# Compute commit hash outside the container (source .git is not mounted).
41+
if [ -d "${SOURCE_DIR}/.git" ]; then
42+
BUILT_COMMIT_HASH="$(git -C "${SOURCE_DIR}" log -1 --pretty=format:"%H")"
43+
else
44+
BUILT_COMMIT_HASH="0000000000000000000000000000000000000000"
45+
fi
46+
47+
export APP_NAME_ID APP_VERSION APP_BUILD_NUMBER JOBS SOURCE_DATE_EPOCH BUILT_COMMIT_HASH
48+
49+
################
50+
# Validate #
51+
################
52+
53+
log_info "=== Stack Wallet Guix Build ==="
54+
log_info "App: ${APP_NAME_ID}"
55+
log_info "Version: ${APP_VERSION}+${APP_BUILD_NUMBER}"
56+
log_info "Jobs: ${JOBS}"
57+
log_info "Hosts: ${HOSTS}"
58+
log_info "Source: ${SOURCE_DIR}"
59+
log_info "SOURCE_DATE_EPOCH: ${SOURCE_DATE_EPOCH}"
60+
log_info "Output: ${OUTDIR}"
61+
62+
# Verify pre-fetched caches exist.
63+
for cache_dir in "$PUB_CACHE_DIR" "$CARGO_CACHE_DIR" "$FLUTTER_SDK_DIR" "$RUST_DIR" "${BASE_CACHE}/native-sources"; do
64+
if [ ! -d "$cache_dir" ]; then
65+
die "Missing cache directory: ${cache_dir}
66+
Run supplementary/deps/fetch-pub-deps.sh and fetch-cargo-deps.sh first."
67+
fi
68+
done
69+
70+
# Verify Flutter SDK is present.
71+
if [ ! -x "${FLUTTER_SDK_DIR}/flutter/bin/flutter" ]; then
72+
die "Flutter SDK not found at ${FLUTTER_SDK_DIR}/flutter"
73+
fi
74+
75+
# Verify at least the default Rust toolchain.
76+
if [ ! -x "${RUST_DIR}/${RUST_VERSION_DEFAULT}/bin/rustc" ]; then
77+
die "Rust ${RUST_VERSION_DEFAULT} not found at ${RUST_DIR}/${RUST_VERSION_DEFAULT}"
78+
fi
79+
80+
# Verify source directory.
81+
if [ ! -f "${SOURCE_DIR}/pubspec.yaml" ]; then
82+
die "Source directory does not look like a Stack Wallet checkout: ${SOURCE_DIR}"
83+
fi
84+
85+
################
86+
# Build loop #
87+
################
88+
89+
mkdir -p "$OUTDIR"
90+
91+
for HOST in $HOSTS; do
92+
log_info "--- Building for ${HOST} ---"
93+
94+
HOST_OUTDIR="${OUTDIR}/${HOST}"
95+
mkdir -p "$HOST_OUTDIR"
96+
97+
# NOTE: guix shell --container --pure strips the environment.
98+
# We use env inside the container to inject required variables.
99+
# The guix scripts tree is mounted at /sw/guix since it lives
100+
# outside the main source directory.
101+
guix shell \
102+
--container \
103+
--pure \
104+
--emulate-fhs \
105+
--manifest="${SCRIPT_DIR}/manifest.scm" \
106+
--expose="${SOURCE_DIR}=/sw/src" \
107+
--expose="${SCRIPT_DIR}=/sw/guix" \
108+
--share="${HOST_OUTDIR}=/sw/output" \
109+
--share="${PUB_CACHE_DIR}=/sw/pub-cache" \
110+
--expose="${CARGO_CACHE_DIR}=/sw/cargo-cache" \
111+
--share="${FLUTTER_SDK_DIR}=/sw/flutter-sdk" \
112+
--expose="${RUST_DIR}=/sw/rust" \
113+
--expose="${BASE_CACHE}/native-sources=/sw/native-sources" \
114+
--no-cwd \
115+
-- env \
116+
HOME="/tmp" \
117+
HOST="$HOST" \
118+
JOBS="$JOBS" \
119+
SOURCE_DATE_EPOCH="$SOURCE_DATE_EPOCH" \
120+
APP_NAME_ID="$APP_NAME_ID" \
121+
APP_VERSION="$APP_VERSION" \
122+
APP_BUILD_NUMBER="$APP_BUILD_NUMBER" \
123+
RUST_VERSION_DEFAULT="$RUST_VERSION_DEFAULT" \
124+
RUST_VERSION_MWC="$RUST_VERSION_MWC" \
125+
RUST_VERSION_FROSTDART="$RUST_VERSION_FROSTDART" \
126+
BUILT_COMMIT_HASH="$BUILT_COMMIT_HASH" \
127+
bash /sw/guix/libexec/build.sh \
128+
2>&1 | tee "${HOST_OUTDIR}/build.log" \
129+
|| die "Build failed for ${HOST}. See ${HOST_OUTDIR}/build.log"
130+
131+
log_info "Build complete for ${HOST}"
132+
log_info "Output: ${HOST_OUTDIR}"
133+
done
134+
135+
################
136+
# Summary #
137+
################
138+
139+
log_info "=== All builds complete ==="
140+
log_info "Output directory: ${OUTDIR}"
141+
142+
# Collect all SHA256SUMS.part files.
143+
SUMS_FILE="${OUTDIR}/SHA256SUMS"
144+
: > "$SUMS_FILE"
145+
for HOST in $HOSTS; do
146+
PART="${OUTDIR}/${HOST}/SHA256SUMS.part"
147+
if [ -f "$PART" ]; then
148+
cat "$PART" >> "$SUMS_FILE"
149+
fi
150+
done
151+
152+
if [ -s "$SUMS_FILE" ]; then
153+
log_info "Combined SHA256SUMS:"
154+
cat "$SUMS_FILE"
155+
fi

contrib/guix/guix-clean

Lines changed: 62 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,62 @@
1+
#!/usr/bin/env bash
2+
# Copyright (c) Stack Wallet developers
3+
# Distributed under the MIT software license, see the accompanying
4+
# file COPYING or https://opensource.org/licenses/MIT.
5+
#
6+
# guix-clean — Remove build artifacts. Preserves depends/ caches.
7+
#
8+
# Usage: ./guix-clean [--all]
9+
#
10+
# --all Also remove depends/ caches (expensive to recreate)
11+
12+
set -euo pipefail
13+
14+
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
15+
source "${SCRIPT_DIR}/libexec/prelude.bash"
16+
17+
CLEAN_ALL=0
18+
19+
while [ $# -gt 0 ]; do
20+
case "$1" in
21+
--all) CLEAN_ALL=1; shift ;;
22+
--help|-h)
23+
echo "Usage: guix-clean [--all]"
24+
echo " --all Also remove depends/ caches"
25+
exit 0
26+
;;
27+
*) die "Unknown option: $1" ;;
28+
esac
29+
done
30+
31+
# Remove all guix-build-* output directories.
32+
BUILD_DIRS=("${SCRIPT_DIR}"/guix-build-*)
33+
if [ -e "${BUILD_DIRS[0]}" ]; then
34+
log_info "Removing build output directories ..."
35+
for d in "${BUILD_DIRS[@]}"; do
36+
log_info " Removing: $(basename "$d")"
37+
rm -rf "$d"
38+
done
39+
else
40+
log_info "No build output directories to clean."
41+
fi
42+
43+
# Remove downloaded archive files (but not the extracted caches).
44+
for f in "${BASE_CACHE}"/*.tar.{gz,xz} "${BASE_CACHE}"/rust-*.tar.gz; do
45+
if [ -f "$f" ]; then
46+
log_info " Removing archive: $(basename "$f")"
47+
rm -f "$f"
48+
fi
49+
done
50+
51+
if [ "$CLEAN_ALL" -eq 1 ]; then
52+
log_info "Removing ALL dependency caches (--all) ..."
53+
for d in "$PUB_CACHE_DIR" "$CARGO_CACHE_DIR" "$FLUTTER_SDK_DIR" "$RUST_DIR" \
54+
"${BASE_CACHE}/pub-archives"; do
55+
if [ -d "$d" ]; then
56+
log_info " Removing: $(basename "$d")"
57+
rm -rf "$d"
58+
fi
59+
done
60+
fi
61+
62+
log_info "Clean complete."

0 commit comments

Comments
 (0)