Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions contrib/guix/.gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
# Pre-fetched dependency caches (large, fetched by supplementary/deps/ scripts)
depends/

# Build output directories
guix-build-*/
55 changes: 55 additions & 0 deletions contrib/guix/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,55 @@
# Guix Reproducible Builds for Stack Wallet
Build infrastructure for producing reproducible (deterministic) Linux x86_64 builds of Stack Wallet inside a Guix container.

Based on Bitcoin Core's approach (`contrib/guix/`).

## Prerequisites
- [GNU Guix](https://guix.gnu.org/) installed (via the shell installer or distro package)
- GPG key (for signing attestations)
- ~20 GB disk space for dependency caches

## Quick Start
```bash
# 1. Fetch all dependencies (requires network)
supplementary/deps/fetch-pub-deps.sh
supplementary/deps/fetch-cargo-deps.sh

# 2. Verify dependency hashes
supplementary/deps/verify-deps.sh

# 3. Build (network-isolated, deterministic: reproducible)
./guix-build

# 4. Sign the build output
./guix-attest

# 5. (Other builders) Verify attestations match
./guix-verify
```

## Build Variants
Set `APP_NAME_ID` to select the variant:
| Variant | `APP_NAME_ID` | Rust Plugins |
|----------------|----------------|--------------------------------|
| Stack Wallet | `stack_wallet` | epiccash, mwc, frostdart |
| Stack Duo | `stack_duo` | frostdart |
| Campfire | `campfire` | (none) |

## Environment Variables
| Variable | Default | Description |
|---------------------|-------------------|-------------------------------------|
| `APP_NAME_ID` | `stack_wallet` | Build variant |
| `APP_VERSION` | from pubspec.yaml | Version string (e.g. `2.3.4`) |
| `APP_BUILD_NUMBER` | from pubspec.yaml | Build number (e.g. `234`) |
| `JOBS` | `$(nproc)` | Parallel job count |
| `HOSTS` | `x86_64-linux-gnu`| Target triplet(s) |
| `SOURCE_DATE_EPOCH` | from git log | Timestamp for determinism |
| `BASE_CACHE` | `depends` | Path to pre-fetched dependency dir |

## Known Limitations
- Flutter SDK is a hash-pinned binary input, not built from source.
- Pre-built native `.so` libs (Monero, Wownero, Salvium, Tor, etc.) ship in pub
packages and are accepted as-is with hash verification.
A future phase will see these dependencies also built via the same method.
- Linux x86_64 only (no cross-compilation).
- Flutter AOT reproducibility is unverified and may need investigation.
102 changes: 102 additions & 0 deletions contrib/guix/guix-attest
Original file line number Diff line number Diff line change
@@ -0,0 +1,102 @@
#!/usr/bin/env bash
# Copyright (c) Stack Wallet developers
# Distributed under the MIT software license, see the accompanying
# file COPYING or https://opensource.org/licenses/MIT.
#
# guix-attest — Collect SHA256SUMS from build outputs and GPG-sign them.
#
# Usage: ./guix-attest [--signer <gpg-key-id>]
#
# Adapted from Bitcoin Core's contrib/guix/guix-attest.

set -euo pipefail

SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
source "${SCRIPT_DIR}/libexec/prelude.bash"

SIGNER="${SIGNER:-}"

while [ $# -gt 0 ]; do
case "$1" in
--signer) SIGNER="$2"; shift 2 ;;
--help|-h)
echo "Usage: guix-attest [--signer <gpg-key-id>]"
echo ""
echo "Collects SHA256SUMS from the most recent guix-build output"
echo "and creates a GPG-signed attestation."
exit 0
;;
*) die "Unknown option: $1" ;;
esac
done

################
# Find output #
################

# Find the most recent guix-build-* directory.
BUILD_DIR="$(ls -dt "${SCRIPT_DIR}"/guix-build-* 2>/dev/null | head -1)"
if [ -z "$BUILD_DIR" ] || [ ! -d "$BUILD_DIR" ]; then
die "No guix-build-* directory found. Run guix-build first."
fi

log_info "Using build output: ${BUILD_DIR}"

################
# Collect sums #
################

SUMS_FILE="${BUILD_DIR}/SHA256SUMS"

if [ ! -s "$SUMS_FILE" ]; then
# Rebuild from .part files.
: > "$SUMS_FILE"
while IFS= read -r -d '' part; do
cat "$part" >> "$SUMS_FILE"
done < <(find "$BUILD_DIR" -name "SHA256SUMS.part" -print0 | sort -z)
fi

if [ ! -s "$SUMS_FILE" ]; then
die "No SHA256SUMS found. Build may have failed."
fi

log_info "SHA256SUMS:"
cat "$SUMS_FILE"
echo ""

################
# GPG sign #
################

# Determine signer identity.
if [ -z "$SIGNER" ]; then
# Try to get default GPG key.
SIGNER="$(gpg --list-secret-keys --keyid-format long 2>/dev/null \
| grep '^sec' | head -1 | awk '{print $2}' | cut -d'/' -f2)" || true
fi

if [ -z "$SIGNER" ]; then
log_error "No GPG key found. Specify --signer <key-id> or set SIGNER env var."
log_info "SHA256SUMS written to ${SUMS_FILE} (unsigned)"
exit 1
fi

log_info "Signing with GPG key: ${SIGNER}"

# Create detached signature.
SIG_FILE="${SUMS_FILE}.asc"
gpg --detach-sign --armor --local-user "$SIGNER" --output "$SIG_FILE" "$SUMS_FILE"

log_info "Attestation created:"
log_info " Checksums: ${SUMS_FILE}"
log_info " Signature: ${SIG_FILE}"

# Also create an attestation directory for multi-signer workflows.
ATTEST_DIR="${BUILD_DIR}/attestations"
mkdir -p "${ATTEST_DIR}/${SIGNER}"
cp "$SUMS_FILE" "${ATTEST_DIR}/${SIGNER}/SHA256SUMS"
cp "$SIG_FILE" "${ATTEST_DIR}/${SIGNER}/SHA256SUMS.asc"

log_info " Attestation dir: ${ATTEST_DIR}/${SIGNER}/"
log_info ""
log_info "Share the attestations/ directory with other builders for verification."
156 changes: 156 additions & 0 deletions contrib/guix/guix-build
Original file line number Diff line number Diff line change
@@ -0,0 +1,156 @@
#!/usr/bin/env bash
# Copyright (c) Stack Wallet developers
# Distributed under the MIT software license, see the accompanying
# file COPYING or https://opensource.org/licenses/MIT.
#
# guix-build — Outer orchestrator for reproducible Stack Wallet builds.
#
# Usage: ./guix-build [--app stack_wallet|stack_duo|campfire]
# [--jobs N]
# [--version X.Y.Z]
# [--build-number NNN]

set -euo pipefail

SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
source "${SCRIPT_DIR}/libexec/prelude.bash"

################
# CLI args #
################

while [ $# -gt 0 ]; do
case "$1" in
--app) APP_NAME_ID="$2"; shift 2 ;;
--jobs) JOBS="$2"; shift 2 ;;
--version) APP_VERSION="$2"; shift 2 ;;
--build-number) APP_BUILD_NUMBER="$2"; shift 2 ;;
--hosts) HOSTS="$2"; shift 2 ;;
--help|-h)
echo "Usage: guix-build [--app NAME] [--jobs N] [--version X.Y.Z] [--build-number NNN]"
exit 0
;;
*) die "Unknown option: $1" ;;
esac
done

auto_detect_version
compute_source_date_epoch

# Compute commit hash outside the container (source .git is not mounted).
if [ -e "${SOURCE_DIR}/.git" ]; then
BUILT_COMMIT_HASH="$(git -C "${SOURCE_DIR}" log -1 --pretty=format:"%H")"
else
BUILT_COMMIT_HASH="0000000000000000000000000000000000000000"
fi

export APP_NAME_ID APP_VERSION APP_BUILD_NUMBER JOBS SOURCE_DATE_EPOCH BUILT_COMMIT_HASH

################
# Validate #
################

log_info "=== Stack Wallet Guix Build ==="
log_info "App: ${APP_NAME_ID}"
log_info "Version: ${APP_VERSION}+${APP_BUILD_NUMBER}"
log_info "Jobs: ${JOBS}"
log_info "Hosts: ${HOSTS}"
log_info "Source: ${SOURCE_DIR}"
log_info "SOURCE_DATE_EPOCH: ${SOURCE_DATE_EPOCH}"
log_info "Output: ${OUTDIR}"

# Verify pre-fetched caches exist.
for cache_dir in "$PUB_CACHE_DIR" "$CARGO_CACHE_DIR" "$FLUTTER_SDK_DIR" "$RUST_DIR" "${BASE_CACHE}/native-sources"; do
if [ ! -d "$cache_dir" ]; then
die "Missing cache directory: ${cache_dir}
Run supplementary/deps/fetch-pub-deps.sh and fetch-cargo-deps.sh first."
fi
done

# Verify Flutter SDK is present.
if [ ! -x "${FLUTTER_SDK_DIR}/flutter/bin/flutter" ]; then
die "Flutter SDK not found at ${FLUTTER_SDK_DIR}/flutter"
fi

# Verify at least the default Rust toolchain.
if [ ! -x "${RUST_DIR}/${RUST_VERSION_DEFAULT}/bin/rustc" ]; then
die "Rust ${RUST_VERSION_DEFAULT} not found at ${RUST_DIR}/${RUST_VERSION_DEFAULT}"
fi

# Verify source directory.
if [ ! -f "${SOURCE_DIR}/pubspec.yaml" ] && [ ! -f "${SOURCE_DIR}/scripts/app_config/templates/pubspec.template.yaml" ]; then
die "Source directory does not look like a Stack Wallet checkout: ${SOURCE_DIR}"
fi

################
# Build loop #
################

mkdir -p "$OUTDIR"

for HOST in $HOSTS; do
log_info "--- Building for ${HOST} ---"

HOST_OUTDIR="${OUTDIR}/${HOST}"
mkdir -p "$HOST_OUTDIR"

# NOTE: guix shell --container --pure strips the environment.
# We use env inside the container to inject required variables.
# The guix scripts tree is mounted at /sw/guix since it lives
# outside the main source directory.
guix shell \
--container \
--pure \
--emulate-fhs \
--manifest="${SCRIPT_DIR}/manifest.scm" \
--expose="${SOURCE_DIR}=/sw/src" \
--expose="${SCRIPT_DIR}=/sw/guix" \
--share="${HOST_OUTDIR}=/sw/output" \
--share="${PUB_CACHE_DIR}=/sw/pub-cache" \
--expose="${CARGO_CACHE_DIR}=/sw/cargo-cache" \
--share="${FLUTTER_SDK_DIR}=/sw/flutter-sdk" \
--expose="${RUST_DIR}=/sw/rust" \
--expose="${BASE_CACHE}/native-sources=/sw/native-sources" \
--expose="${BASE_CACHE}/go-cache=/sw/go-cache" \
--no-cwd \
-- env \
HOME="/tmp" \
HOST="$HOST" \
JOBS="$JOBS" \
SOURCE_DATE_EPOCH="$SOURCE_DATE_EPOCH" \
APP_NAME_ID="$APP_NAME_ID" \
APP_VERSION="$APP_VERSION" \
APP_BUILD_NUMBER="$APP_BUILD_NUMBER" \
RUST_VERSION_DEFAULT="$RUST_VERSION_DEFAULT" \
RUST_VERSION_MWC="$RUST_VERSION_MWC" \
RUST_VERSION_FROSTDART="$RUST_VERSION_FROSTDART" \
BUILT_COMMIT_HASH="$BUILT_COMMIT_HASH" \
bash /sw/guix/libexec/build.sh \
2>&1 | tee "${HOST_OUTDIR}/build.log" \
|| die "Build failed for ${HOST}. See ${HOST_OUTDIR}/build.log"

log_info "Build complete for ${HOST}"
log_info "Output: ${HOST_OUTDIR}"
done

################
# Summary #
################

log_info "=== All builds complete ==="
log_info "Output directory: ${OUTDIR}"

# Collect all SHA256SUMS.part files.
SUMS_FILE="${OUTDIR}/SHA256SUMS"
: > "$SUMS_FILE"
for HOST in $HOSTS; do
PART="${OUTDIR}/${HOST}/SHA256SUMS.part"
if [ -f "$PART" ]; then
cat "$PART" >> "$SUMS_FILE"
fi
done

if [ -s "$SUMS_FILE" ]; then
log_info "Combined SHA256SUMS:"
cat "$SUMS_FILE"
fi
62 changes: 62 additions & 0 deletions contrib/guix/guix-clean
Original file line number Diff line number Diff line change
@@ -0,0 +1,62 @@
#!/usr/bin/env bash
# Copyright (c) Stack Wallet developers
# Distributed under the MIT software license, see the accompanying
# file COPYING or https://opensource.org/licenses/MIT.
#
# guix-clean — Remove build artifacts. Preserves depends/ caches.
#
# Usage: ./guix-clean [--all]
#
# --all Also remove depends/ caches (expensive to recreate)

set -euo pipefail

SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
source "${SCRIPT_DIR}/libexec/prelude.bash"

CLEAN_ALL=0

while [ $# -gt 0 ]; do
case "$1" in
--all) CLEAN_ALL=1; shift ;;
--help|-h)
echo "Usage: guix-clean [--all]"
echo " --all Also remove depends/ caches"
exit 0
;;
*) die "Unknown option: $1" ;;
esac
done

# Remove all guix-build-* output directories.
BUILD_DIRS=("${SCRIPT_DIR}"/guix-build-*)
if [ -e "${BUILD_DIRS[0]}" ]; then
log_info "Removing build output directories ..."
for d in "${BUILD_DIRS[@]}"; do
log_info " Removing: $(basename "$d")"
rm -rf "$d"
done
else
log_info "No build output directories to clean."
fi

# Remove downloaded archive files (but not the extracted caches).
for f in "${BASE_CACHE}"/*.tar.{gz,xz} "${BASE_CACHE}"/rust-*.tar.gz; do
if [ -f "$f" ]; then
log_info " Removing archive: $(basename "$f")"
rm -f "$f"
fi
done

if [ "$CLEAN_ALL" -eq 1 ]; then
log_info "Removing ALL dependency caches (--all) ..."
for d in "$PUB_CACHE_DIR" "$CARGO_CACHE_DIR" "$FLUTTER_SDK_DIR" "$RUST_DIR" \
"${BASE_CACHE}/pub-archives"; do
if [ -d "$d" ]; then
log_info " Removing: $(basename "$d")"
rm -rf "$d"
fi
done
fi

log_info "Clean complete."
Loading
Loading