diff --git a/contrib/guix/.gitignore b/contrib/guix/.gitignore new file mode 100644 index 000000000..be1529bf3 --- /dev/null +++ b/contrib/guix/.gitignore @@ -0,0 +1,5 @@ +# Pre-fetched dependency caches (large, fetched by supplementary/deps/ scripts) +depends/ + +# Build output directories +guix-build-*/ diff --git a/contrib/guix/README.md b/contrib/guix/README.md new file mode 100644 index 000000000..d494fd17e --- /dev/null +++ b/contrib/guix/README.md @@ -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. diff --git a/contrib/guix/guix-attest b/contrib/guix/guix-attest new file mode 100755 index 000000000..44943289f --- /dev/null +++ b/contrib/guix/guix-attest @@ -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 ] +# +# 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 ]" + 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 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." diff --git a/contrib/guix/guix-build b/contrib/guix/guix-build new file mode 100755 index 000000000..60beafc58 --- /dev/null +++ b/contrib/guix/guix-build @@ -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 diff --git a/contrib/guix/guix-clean b/contrib/guix/guix-clean new file mode 100755 index 000000000..d6838c48f --- /dev/null +++ b/contrib/guix/guix-clean @@ -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." diff --git a/contrib/guix/guix-verify b/contrib/guix/guix-verify new file mode 100755 index 000000000..1e60c809e --- /dev/null +++ b/contrib/guix/guix-verify @@ -0,0 +1,120 @@ +#!/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-verify — Compare attestations across multiple signers and verify GPG sigs. +# +# Usage: ./guix-verify [build-dir] +# +# Adapted from Bitcoin Core's contrib/guix/guix-verify. + +set -euo pipefail + +SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" +source "${SCRIPT_DIR}/libexec/prelude.bash" + +################ +# Find build # +################ + +BUILD_DIR="${1:-}" +if [ -z "$BUILD_DIR" ]; then + BUILD_DIR="$(ls -dt "${SCRIPT_DIR}"/guix-build-* 2>/dev/null | head -1)" +fi + +if [ -z "$BUILD_DIR" ] || [ ! -d "$BUILD_DIR" ]; then + die "No build directory found. Specify path or run guix-build first." +fi + +ATTEST_DIR="${BUILD_DIR}/attestations" +if [ ! -d "$ATTEST_DIR" ]; then + die "No attestations/ directory in ${BUILD_DIR}. Run guix-attest first." +fi + +log_info "Verifying attestations in: ${ATTEST_DIR}" + +################ +# Collect # +################ + +SIGNERS=() +while IFS= read -r -d '' signer_dir; do + signer="$(basename "$signer_dir")" + SIGNERS+=("$signer") +done < <(find "$ATTEST_DIR" -mindepth 1 -maxdepth 1 -type d -print0 | sort -z) + +if [ ${#SIGNERS[@]} -eq 0 ]; then + die "No signer directories found in ${ATTEST_DIR}" +fi + +log_info "Found ${#SIGNERS[@]} signer(s): ${SIGNERS[*]}" + +################ +# Verify GPG # +################ + +GPG_FAILURES=0 + +for signer in "${SIGNERS[@]}"; do + sums="${ATTEST_DIR}/${signer}/SHA256SUMS" + sig="${ATTEST_DIR}/${signer}/SHA256SUMS.asc" + + if [ ! -f "$sums" ]; then + log_error " ${signer}: SHA256SUMS missing!" + GPG_FAILURES=$((GPG_FAILURES + 1)) + continue + fi + + if [ ! -f "$sig" ]; then + log_error " ${signer}: SHA256SUMS.asc missing (unsigned)" + GPG_FAILURES=$((GPG_FAILURES + 1)) + continue + fi + + if gpg --verify "$sig" "$sums" 2>/dev/null; then + log_info " ${signer}: GPG signature VALID" + else + log_error " ${signer}: GPG signature INVALID" + GPG_FAILURES=$((GPG_FAILURES + 1)) + fi +done + +################ +# Compare sums # +################ + +MATCH_FAILURES=0 +REFERENCE_SIGNER="${SIGNERS[0]}" +REFERENCE_SUMS="${ATTEST_DIR}/${REFERENCE_SIGNER}/SHA256SUMS" + +log_info "" +log_info "Comparing checksums (reference: ${REFERENCE_SIGNER}) ..." + +for signer in "${SIGNERS[@]:1}"; do + other_sums="${ATTEST_DIR}/${signer}/SHA256SUMS" + if diff -q "$REFERENCE_SUMS" "$other_sums" > /dev/null 2>&1; then + log_info " ${signer}: MATCH" + else + log_error " ${signer}: MISMATCH" + log_error " Differences:" + diff "$REFERENCE_SUMS" "$other_sums" || true + MATCH_FAILURES=$((MATCH_FAILURES + 1)) + fi +done + +################ +# Summary # +################ + +echo "" +log_info "=== Verification Summary ===" +log_info "Signers: ${#SIGNERS[@]}" +log_info "GPG failures: ${GPG_FAILURES}" +log_info "Match failures: ${MATCH_FAILURES}" + +if [ "$GPG_FAILURES" -gt 0 ] || [ "$MATCH_FAILURES" -gt 0 ]; then + die "Verification FAILED" +fi + +log_info "All attestations match and GPG signatures are valid." diff --git a/contrib/guix/libexec/build.sh b/contrib/guix/libexec/build.sh new file mode 100755 index 000000000..b6343d0fd --- /dev/null +++ b/contrib/guix/libexec/build.sh @@ -0,0 +1,668 @@ +#!/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. +# +# build.sh — Inner build script that runs INSIDE the Guix container. +# No network access is available. All dependencies are pre-mounted. + +set -euo pipefail + +################ +# Determinism # +################ + +export LC_ALL=C +export TZ=UTC +umask 0022 + +# Force single codegen unit in Rust release builds for deterministic output. +export CARGO_PROFILE_RELEASE_CODEGEN_UNITS=1 + +# Disable Dart/Flutter analytics and telemetry. +export CI=true +export FLUTTER_SUPPRESS_ANALYTICS=true +export PUB_ENVIRONMENT=guix + +################ +# Paths # +################ + +# These paths correspond to the --share mounts set up by guix-build. +SRC_MOUNT="/sw/src" +OUTPUT_MOUNT="/sw/output" +PUB_CACHE_MOUNT="/sw/pub-cache" +CARGO_CACHE_MOUNT="/sw/cargo-cache" +FLUTTER_MOUNT="/sw/flutter-sdk/flutter" +RUST_MOUNT="/sw/rust" +NATIVE_MOUNT="/sw/native-sources" + +BUILD_DIR="/tmp/build" + +# Validate mounts. +for d in "$SRC_MOUNT" "$PUB_CACHE_MOUNT" "$FLUTTER_MOUNT" "$RUST_MOUNT"; do + [ -d "$d" ] || { echo "!!! Missing mount: $d" >&2; exit 1; } +done + +################ +# Env vars # +################ + +HOST="${HOST:?HOST not set}" +JOBS="${JOBS:-$(nproc)}" +SOURCE_DATE_EPOCH="${SOURCE_DATE_EPOCH:?SOURCE_DATE_EPOCH not set}" +APP_NAME_ID="${APP_NAME_ID:?APP_NAME_ID not set}" +APP_VERSION="${APP_VERSION:?APP_VERSION not set}" +APP_BUILD_NUMBER="${APP_BUILD_NUMBER:?APP_BUILD_NUMBER not set}" + +# Go: offline builds for flutter_mwebd. +export GOMODCACHE="/sw/go-cache" +export GOFLAGS="-buildvcs=false" +export GONOSUMCHECK="*" +export GONOSUMDB="*" +export GOPROXY=off + +RUST_VERSION_DEFAULT="${RUST_VERSION_DEFAULT:-1.89.0}" +RUST_VERSION_MWC="${RUST_VERSION_MWC:-1.85.1}" +RUST_VERSION_FROSTDART="${RUST_VERSION_FROSTDART:-1.71.0}" +RUST_VERSION_XELIS="${RUST_VERSION_XELIS:-1.91.0}" + +echo "--- [build] Host: ${HOST}" +echo "--- [build] App: ${APP_NAME_ID} v${APP_VERSION}+${APP_BUILD_NUMBER}" +echo "--- [build] Jobs: ${JOBS}" +echo "--- [build] SOURCE_DATE_EPOCH: ${SOURCE_DATE_EPOCH}" + +################ +# Helpers # +################ + +set_rust_version() { + local version="$1" + local rust_prefix="${RUST_MOUNT}/${version}" + [ -d "$rust_prefix" ] || { echo "!!! Rust ${version} not found at ${rust_prefix}" >&2; exit 1; } + + # Shim dir must come first so the cargo shim intercepts `cargo +VERSION`. + export PATH="/tmp/shims:${rust_prefix}/bin:${PATH_ORIG}" + export CARGO_HOME="${CARGO_CACHE_MOUNT}" + export RUSTUP_HOME="/tmp/fake-rustup" + + # Rust 1.91+ build scripts need libgcc_s.so.1 which lives at /lib/ in the + # Guix FHS emulation. Set LD_LIBRARY_PATH so the linker can find it. + export LD_LIBRARY_PATH="/lib:/lib64:/usr/lib" + + echo "--- [build] Rust version: $(rustc --version)" +} + +# Create shims for rustup and cargo so existing scripts work without +# modification inside the container. +install_shims() { + local shim_dir="/tmp/shims" + mkdir -p "$shim_dir" + + # rustup shim: silently succeeds so scripts that call `rustup default X` + # don't fail. + cat > "$shim_dir/rustup" << 'SHIM' +#!/bin/sh +case "$1" in + toolchain) echo "rustup shim: toolchain command ignored" ;; + default) echo "rustup shim: default command ignored" ;; + show|--version) echo "rustup-shim 0.0.0 (guix)" ;; + target) echo "rustup shim: target command ignored" ;; + run) + # `rustup run [args...]` — strip 'run' and toolchain, + # then exec the command. The correct toolchain is already on PATH. + shift # drop 'run' + shift # drop toolchain name (e.g. 'stable') + exec "$@" + ;; + *) ;; +esac +exit 0 +SHIM + chmod +x "$shim_dir/rustup" + + # cargo shim: strips `+VERSION` argument (rustup toolchain syntax) and + # delegates to the real cargo on PATH. This handles frostdart's + # `cargo +1.71.0 build ...` without needing rustup. + cat > "$shim_dir/cargo" << 'CARGO_SHIM' +#!/bin/sh +# Strip +VERSION argument if present (e.g. `cargo +1.71.0 build` → `cargo build`). +# The correct Rust toolchain is already on PATH via set_rust_version(). +REAL_CARGO="" +for p in $(echo "$PATH" | tr ':' '\n'); do + if [ "$p" != "/tmp/shims" ] && [ -x "$p/cargo" ]; then + REAL_CARGO="$p/cargo" + break + fi +done +if [ -z "$REAL_CARGO" ]; then + echo "cargo shim: cannot find real cargo on PATH" >&2 + exit 1 +fi +# Drop the first arg if it starts with '+' +case "${1:-}" in + +*) shift ;; +esac +exec "$REAL_CARGO" "$@" +CARGO_SHIM + chmod +x "$shim_dir/cargo" + + # git shim: intercepts `git log` to return BUILT_COMMIT_HASH (since .git + # is excluded from the rsync copy), and makes `git pull`/`git clone` no-ops + # for pre-populated native dependency repos. + local git_real + git_real="$(command -v git)" + cat > "$shim_dir/git" << GITSHIM +#!/bin/sh +# Git wrapper for offline Guix builds. +case "\$*" in + *log*--pretty=format*|*log*--format*) + # Return the pre-computed commit hash. + echo "${BUILT_COMMIT_HASH:-0000000000000000000000000000000000000000}" + exit 0 + ;; + *pull*|*fetch*) + exit 0 + ;; + *clone*) + # If target dir already exists, succeed silently. + last_arg="\${*##* }" + if [ -d "\$last_arg" ]; then + exit 0 + fi + ;; +esac +exec "$git_real" "\$@" +GITSHIM + chmod +x "$shim_dir/git" + + # CRITICAL: Wrap the REAL Dart VM binary to inject --offline for pub + # commands. Flutter's PubDependencies.update calls the dart binary at + # bin/cache/dart-sdk/bin/dart by absolute path (bypassing any PATH shims). + # It runs `dart pub get --example` on packages/flutter_tools. Without + # --offline, this tries to reach pub.dev and fails in the container. + local dart_sdk_bin="${FLUTTER_MOUNT}/bin/cache/dart-sdk/bin" + local dart_real="${dart_sdk_bin}/dart.real" + if [ ! -f "$dart_real" ]; then + mv "${dart_sdk_bin}/dart" "$dart_real" + fi + cat > "${dart_sdk_bin}/dart" << 'DARTWRAP' +#!/bin/bash +SELF_DIR="$(cd "$(dirname "$0")" && pwd)" +REAL="${SELF_DIR}/dart.real" +# Intercept pub commands and inject --offline. +is_pub=false +for a in "$@"; do + [ "$a" = "pub" ] && is_pub=true && break +done +if $is_pub; then + args=() + for a in "$@"; do + args+=("$a") + case "$a" in + get|upgrade|downgrade) args+=("--offline") ;; + esac + done + exec "$REAL" "${args[@]}" +fi +exec "$REAL" "$@" +DARTWRAP + chmod +x "${dart_sdk_bin}/dart" + + # dart shim on PATH: bypass the Flutter SDK's bin/dart shell wrapper + # (which sources shared.sh and may run update_engine_version.sh, pub + # upgrade, etc.). We invoke dart.real directly. + cat > "$shim_dir/dart" << DARTSHIM +#!/bin/sh +exec "$dart_real" "\$@" +DARTSHIM + chmod +x "$shim_dir/dart" + + # Replace Flutter SDK's bin/dart wrapper with a simple passthrough too. + cp "$shim_dir/dart" "${FLUTTER_MOUNT}/bin/dart" + + # flutter shim: bypasses the Flutter SDK's shared.sh which tries to run + # `dart pub upgrade` on the Flutter tools and passes --packages= pointing + # to a non-existent file. Instead, we invoke the pre-built flutter_tools + # snapshot directly via the Dart VM. + local snapshot="${FLUTTER_MOUNT}/bin/cache/flutter_tools.snapshot" + cat > "$shim_dir/flutter" << FLUTTERSHIM +#!/bin/sh +exec "$dart_real" "$snapshot" "\$@" +FLUTTERSHIM + chmod +x "$shim_dir/flutter" + + export PATH="$shim_dir:$PATH" +} + +# Set up Cargo to use vendored crates for a given plugin. +setup_cargo_vendor() { + local plugin_name="$1" + local workspace_dir="$2" + local vendor_dir="${CARGO_CACHE_MOUNT}/vendor-${plugin_name}" + + if [ ! -d "$vendor_dir" ]; then + echo "!!! Vendor dir not found: ${vendor_dir}" >&2 + return 1 + fi + + mkdir -p "$workspace_dir/.cargo" + + # Use the config generated by `cargo vendor` during fetch (it includes + # source redirects for both crates-io and git dependencies). + local vendor_config="${CARGO_CACHE_MOUNT}/vendor-${plugin_name}-config.toml" + if [ -f "$vendor_config" ]; then + # The saved config references the vendor dir by its original path. + # Replace with the container-local path. + sed "s|directory = .*|directory = \"${vendor_dir}\"|g" \ + "$vendor_config" > "$workspace_dir/.cargo/config.toml" + else + # Fallback: basic config for crates-io only. + cat > "$workspace_dir/.cargo/config.toml" << EOF +[source.crates-io] +replace-with = "vendored-sources" + +[source.vendored-sources] +directory = "${vendor_dir}" +EOF + fi + + # Ensure offline mode. + cat >> "$workspace_dir/.cargo/config.toml" << 'EOF' + +[net] +offline = true +EOF +} + +################ +# Copy source # +################ + +echo "--- [build] Copying source tree to ${BUILD_DIR} ..." +rm -rf "$BUILD_DIR" +mkdir -p "$BUILD_DIR" +# Use rsync to exclude heavy build artifacts from the copy. +# The source tree can be 30+ GB with build/, .git/, target/ dirs. +rsync -a \ + --exclude='build/' \ + --exclude='.dart_tool/' \ + --exclude='.git/' \ + --exclude='target/' \ + --exclude='.idea/' \ + "$SRC_MOUNT/" "$BUILD_DIR/" + +cd "$BUILD_DIR" + +# Set up Flutter and Dart (Dart ships inside the Flutter SDK). +# Add the real dart-sdk bin to PATH temporarily — install_shims() will +# prepend shims that bypass the Flutter SDK's shared.sh bootstrap. +export PATH="${FLUTTER_MOUNT}/bin/cache/dart-sdk/bin:${PATH}" +export PUB_CACHE="$PUB_CACHE_MOUNT" + +# Disable Flutter upgrade checks and analytics. +export FLUTTER_ROOT="$FLUTTER_MOUNT" + +# Install shims BEFORE any dart/flutter invocations. The shims bypass +# shared.sh which would otherwise try to write to the (read-only) Flutter SDK, +# run `dart pub upgrade`, and download artifacts. +install_shims + +# Preconfigure Flutter for linux desktop (now goes through our shim). +flutter config --enable-linux-desktop 2>/dev/null || true + +# Save PATH after Flutter and shims are set up but before any Rust modifications. +# set_rust_version() resets PATH to "${rust_prefix}/bin:${PATH_ORIG}" so that +# switching Rust versions doesn't accumulate stale entries. +PATH_ORIG="$PATH" + +echo "--- [build] Flutter: $(flutter --version --machine 2>/dev/null | head -1)" + +################ +# App config # +################ + +echo "--- [build] Configuring app variant: ${APP_NAME_ID} ..." + +export APP_PROJECT_ROOT_DIR="$BUILD_DIR" + +# BUILT_COMMIT_HASH is passed via env from guix-build (computed outside container). +# Fall back to placeholder if not set. +: "${BUILT_COMMIT_HASH:=0000000000000000000000000000000000000000}" +export BUILT_COMMIT_HASH +echo "--- [build] Commit hash: ${BUILT_COMMIT_HASH:0:12}" + +# Set placeholder variables (from env.sh, but without sourcing it since it +# recomputes APP_PROJECT_ROOT_DIR relative to the script's own location). +export APP_NAME_PLACEHOLDER="PlaceHolderName" +export APP_ID_PLACEHOLDER="com.place.holder" +export APP_ID_PLACEHOLDER_CAMEL="com.place.holderCamel" +export APP_ID_PLACEHOLDER_SNAKE="com.place.holder_snake" +export APP_BASIC_NAME_PLACEHOLDER="place_holder" + +# Step 1: Configure template files (copies pubspec template, platform templates). +# We use `source` (not `bash`) because these scripts `export` variables that +# later steps depend on (LINUX_TF_0, TEMPLATES_DIR, NEW_NAME, etc.). +export BUILD_ISAR_FROM_SOURCE=0 +source "$BUILD_DIR/scripts/app_config/templates/configure_template_files.sh" + +# Step 2: Update version. +source "$BUILD_DIR/scripts/app_config/shared/update_version.sh" \ + -v "$APP_VERSION" -b "$APP_BUILD_NUMBER" + +# Step 3: Link app-specific assets. +source "$BUILD_DIR/scripts/app_config/shared/link_assets.sh" "$APP_NAME_ID" "linux" + +# Step 4: Run variant-specific configuration. +# Pass "linux" as $1 — configure_campfire.sh uses it for APP_BUILD_PLATFORM, +# and configure_stack_wallet.sh checks if $1 == "windows". +source "$BUILD_DIR/scripts/app_config/configure_${APP_NAME_ID}.sh" linux + +# Step 5: Linux platform config. +source "$BUILD_DIR/scripts/app_config/platforms/linux/platform_config.sh" + +# Step 6: Run prebuild.sh to create stub external_api_keys.dart etc. +pushd "$BUILD_DIR/scripts" > /dev/null +bash prebuild.sh +popd > /dev/null + +################ +# GCC compat # +################ + +# Guix provides GCC 15+ which is stricter than the GCC versions Flutter plugins +# were tested with. The project's APPLY_STANDARD_SETTINGS function applies +# -Wall -Werror to ALL plugin targets, causing new warnings to become errors. +# Relax specific warnings that GCC 15 triggers in upstream plugin code. +_linux_cmake="${BUILD_DIR}/linux/CMakeLists.txt" +if [ -f "$_linux_cmake" ]; then + echo "--- [build] Relaxing -Werror for GCC 15 compatibility ..." + sed -i 's/-Wall -Werror/-Wall -Werror -Wno-sign-compare/' "$_linux_cmake" +fi + +################ +# Native deps # +################ + +echo "--- [build] Building native C dependencies ..." + +# JsonCPP 1.7.4 CMakeLists.txt predates CMake 3.5 requirement; newer CMake +# refuses to configure without this compatibility flag. Patch the copied +# build script to add the flag. +sed -i 's/cmake -DCMAKE_BUILD_TYPE/cmake -DCMAKE_POLICY_VERSION_MINIMUM=3.5 -DCMAKE_BUILD_TYPE/' \ + "$BUILD_DIR/scripts/linux/build_secure_storage_deps.sh" + +# Pre-populate git repos from the pre-fetched native sources cache. +# The existing build scripts do `git -C dir pull || git clone ...` which fails +# offline. The git shim (installed by install_shims) makes pull/clone no-ops +# when the target directory already exists. +LINUX_BUILD="${BUILD_DIR}/scripts/linux/build" +mkdir -p "$LINUX_BUILD" + +_prepopulate_repo() { + local name="$1" tag="$2" + if [ -d "$NATIVE_MOUNT/$name" ]; then + cp -a "$NATIVE_MOUNT/$name" "$LINUX_BUILD/$name" + # Use the real git for checkout (bypass shim). + local real_git + for p in $(echo "$PATH" | tr ':' '\n'); do + if [ "$p" != "/tmp/shims" ] && [ -x "$p/git" ]; then + real_git="$p/git" + break + fi + done + "$real_git" -C "$LINUX_BUILD/$name" checkout "$tag" 2>/dev/null || true + "$real_git" -C "$LINUX_BUILD/$name" remote remove origin 2>/dev/null || true + fi +} + +_prepopulate_repo jsoncpp 1.7.4 +_prepopulate_repo libsecret 0.21.4 +_prepopulate_repo secp256k1 68b55209f1ba3e6c0417789598f5f75649e9c14c + +# Build JsonCPP + libsecret (secure storage deps). +( + cd "$BUILD_DIR/scripts/linux" + bash build_secure_storage_deps.sh +) + +# Build secp256k1. +( + cd "$BUILD_DIR/scripts/linux" + bash build_secp256k1.sh +) + +################ +# Rust plugins # +################ + +echo "--- [build] Building Rust crypto plugins for variant: ${APP_NAME_ID} ..." + +CRYPTO_DIR="$BUILD_DIR/crypto_plugins" + +case "$APP_NAME_ID" in + stack_wallet) + # 1. flutter_libepiccash (Rust $RUST_VERSION_DEFAULT) + echo "--- [build] Building flutter_libepiccash ..." + set_rust_version "$RUST_VERSION_DEFAULT" + setup_cargo_vendor "epiccash" "${CRYPTO_DIR}/flutter_libepiccash/rust" + ( + cd "${CRYPTO_DIR}/flutter_libepiccash/scripts/linux" + # GCC 15 in Guix: libstdc++ was built without C99 fenv support, + # so never exposes fesetround/fegetround. Force the + # config macros so the C++ wrapper actually includes . + export CXXFLAGS="${CXXFLAGS:-} -D_GLIBCXX_HAVE_FENV_H=1 -D_GLIBCXX_USE_C99_FENV=1" + bash build_all.sh + ) + + # 2. flutter_libmwc (Rust $RUST_VERSION_MWC) + echo "--- [build] Building flutter_libmwc ..." + set_rust_version "$RUST_VERSION_MWC" + setup_cargo_vendor "mwc" "${CRYPTO_DIR}/flutter_libmwc/rust" + ( + cd "${CRYPTO_DIR}/flutter_libmwc/scripts/linux" + bash build_all.sh + ) + + # 3. frostdart (Rust $RUST_VERSION_FROSTDART) + echo "--- [build] Building frostdart ..." + set_rust_version "$RUST_VERSION_FROSTDART" + setup_cargo_vendor "frostdart" "${CRYPTO_DIR}/frostdart/src/serai" + ( + cd "${CRYPTO_DIR}/frostdart/scripts/linux" + bash build_all.sh + ) + ;; + + stack_duo) + # frostdart only. + echo "--- [build] Building frostdart ..." + set_rust_version "$RUST_VERSION_FROSTDART" + setup_cargo_vendor "frostdart" "${CRYPTO_DIR}/frostdart/src/serai" + ( + cd "${CRYPTO_DIR}/frostdart/scripts/linux" + bash build_all.sh + ) + ;; + + campfire) + # No Rust plugins for Campfire (secp256k1 already built above via cmake). + echo "--- [build] No Rust plugins needed for campfire" + ;; + + *) + echo "!!! Unknown APP_NAME_ID: ${APP_NAME_ID}" >&2 + exit 1 + ;; +esac + +# Switch back to default Rust for any remaining steps. +set_rust_version "$RUST_VERSION_DEFAULT" + +################ +# Cargokit prep # +################ + +# Cargokit plugins (tor_ffi_plugin, xelis_flutter) build Rust code during +# `flutter build linux` via cmake. Cargokit's run_build_tool.sh changes +# directory to a temp folder before invoking cargo. Cargo searches for +# .cargo/config.toml from CWD upwards, so per-workspace configs are not +# found. Solution: give each Cargokit plugin its own CARGO_HOME with the +# correct vendor config. We patch each plugin's run_build_tool.sh to +# export CARGO_HOME before running the Dart build tool. + +echo "--- [build] Setting up Cargokit vendor configs for offline Rust builds ..." + +# Helper: create a per-plugin CARGO_HOME with vendor config. +setup_cargokit_plugin() { + local plugin_name="$1" + local plugin_dir="$2" # root of the plugin (contains cargokit/, rust/, linux/) + local rust_dir="$plugin_dir/rust" + + # Set up workspace-level vendor config (for any direct cargo invocations). + setup_cargo_vendor "$plugin_name" "$rust_dir" + + # Create a per-plugin CARGO_HOME with the same config. + local cargo_home="/tmp/cargo-home-${plugin_name}" + mkdir -p "$cargo_home" + cp "$rust_dir/.cargo/config.toml" "$cargo_home/config.toml" + + # Patch run_build_tool.sh to export this CARGO_HOME before running Dart. + local rbt="$plugin_dir/cargokit/run_build_tool.sh" + if [ -f "$rbt" ]; then + sed -i "2i export CARGO_HOME=\"${cargo_home}\"" "$rbt" + fi +} + +for _tor_dir in "${PUB_CACHE_MOUNT}"/git/tor-*/; do + if [ -f "$_tor_dir/rust/Cargo.toml" ]; then + setup_cargokit_plugin "tor" "$_tor_dir" + break + fi +done +for _xelis_dir in "${PUB_CACHE_MOUNT}"/git/xelis-flutter-ffi-*/ "${PUB_CACHE_MOUNT}"/git/xelis_flutter-*/ "${PUB_CACHE_MOUNT}"/git/xelis-flutter-*/; do + if [ -f "$_xelis_dir/rust/Cargo.toml" ]; then + setup_cargokit_plugin "xelis" "$_xelis_dir" + break + fi +done + +# Use the highest Rust version for the flutter build phase. +# Cargokit plugins (tor, xelis) build via cmake during `flutter build linux`. +# Our rustup shim ignores rust-toolchain.toml, so we must set PATH to a +# version that satisfies all plugins. 1.91.0 >= all requirements. +set_rust_version "$RUST_VERSION_XELIS" + +################ +# Flutter build # +################ + +echo "--- [build] Running flutter pub get (offline) ..." +flutter pub get --offline + +# Patch FetchContent URLs to use pre-fetched sources (no network in container). +_plugin_symlinks="${BUILD_DIR}/linux/flutter/ephemeral/.plugin_symlinks" + +_sqlite3_cmake="${_plugin_symlinks}/sqlite3_flutter_libs/linux/CMakeLists.txt" +if [ -f "$_sqlite3_cmake" ]; then + echo "--- [build] Patching sqlite3_flutter_libs for offline build ..." + sed -i "s|URL https://sqlite.org/2024/sqlite-autoconf-3460000.tar.gz|URL file://${NATIVE_MOUNT}/sqlite-autoconf-3460000.tar.gz|g" \ + "$_sqlite3_cmake" +fi + +_spark_cmake="${_plugin_symlinks}/flutter_libsparkmobile/src/CMakeLists.txt" +if [ -f "$_spark_cmake" ]; then + echo "--- [build] Patching flutter_libsparkmobile Boost URL for offline build ..." + sed -i "s|https://archives.boost.io/release/1.71.0/source/boost_1_71_0.zip|file://${NATIVE_MOUNT}/boost_1_71_0.zip|g" \ + "$_spark_cmake" +fi + +# coinlib_flutter uses ExternalProject_Add to git-clone secp256k1 from GitHub. +# Replace with our pre-fetched local clone. +_coinlib_cmake="${_plugin_symlinks}/coinlib_flutter/src/CMakeLists.txt" +if [ -f "$_coinlib_cmake" ]; then + echo "--- [build] Patching coinlib_flutter secp256k1 for offline build ..." + sed -i "s|GIT_REPOSITORY https://github.com/bitcoin-core/secp256k1|GIT_REPOSITORY ${NATIVE_MOUNT}/secp256k1|g" \ + "$_coinlib_cmake" + # Guix's x86_64 CMake defaults CMAKE_INSTALL_LIBDIR to lib64, but + # coinlib_flutter hardcodes lib/. Force lib. + sed -i 's/CMAKE_ARGS ${SECP256K1_ARGS}/CMAKE_ARGS ${SECP256K1_ARGS} -DCMAKE_INSTALL_LIBDIR=lib/' \ + "$_coinlib_cmake" +fi + +# Flutter hardcodes CC=clang/CXX=clang++ when running cmake (build_linux.dart:185). +# Guix's libstdc++ headers from GCC 15+ use _GLIBCXX26_* macros that Clang +# doesn't understand. Patch the Flutter tools source to use GCC and recompile. +_flutter_build_linux="${FLUTTER_MOUNT}/packages/flutter_tools/lib/src/linux/build_linux.dart" +if grep -q "'CC': 'clang'" "$_flutter_build_linux" 2>/dev/null; then + echo "--- [build] Patching Flutter tools to use GCC instead of Clang ..." + sed -i "s/'CC': 'clang', 'CXX': 'clang++'/'CC': 'gcc', 'CXX': 'g++'/" \ + "$_flutter_build_linux" + # Recompile the flutter_tools snapshot from patched source. + _dart_real="${FLUTTER_MOUNT}/bin/cache/dart-sdk/bin/dart.real" + _snapshot="${FLUTTER_MOUNT}/bin/cache/flutter_tools.snapshot" + echo "--- [build] Recompiling flutter_tools.snapshot ..." + "$_dart_real" compile kernel \ + --output="$_snapshot" \ + "${FLUTTER_MOUNT}/packages/flutter_tools/bin/flutter_tools.dart" 2>&1 || { + echo "!!! Failed to recompile flutter_tools.snapshot" >&2 + exit 1 + } +fi + +echo "--- [build] Building Flutter Linux release ..." +flutter build linux --release --no-pub + +################ +# Package # +################ + +echo "--- [build] Packaging output ..." + +BUNDLE_DIR="$BUILD_DIR/build/linux/x64/release/bundle" +[ -d "$BUNDLE_DIR" ] || { echo "!!! Bundle dir not found: $BUNDLE_DIR" >&2; exit 1; } + +DIST_NAME="${APP_NAME_ID}-${APP_VERSION}-${HOST}" +STAGING="/tmp/staging/${DIST_NAME}" +rm -rf "/tmp/staging" +mkdir -p "$STAGING" + +# Copy bundle contents. +cp -a "$BUNDLE_DIR"/* "$STAGING/" + +# Normalize timestamps and permissions for determinism. +find "$STAGING" -exec touch --date="@${SOURCE_DATE_EPOCH}" {} + +find "$STAGING" -type f -exec chmod 644 {} + +find "$STAGING" -type d -exec chmod 755 {} + +# Restore executable bit on the main binary and .so files. +find "$STAGING" -name "*.so" -exec chmod 755 {} + +[ -f "$STAGING/${APP_NAME_ID}" ] && chmod 755 "$STAGING/${APP_NAME_ID}" +# Try alternative binary names. +for candidate in stackwallet stackduo paymint campfire; do + [ -f "$STAGING/${candidate}" ] && chmod 755 "$STAGING/${candidate}" +done + +# Create deterministic tarball. +mkdir -p "$OUTPUT_MOUNT" +TARBALL="${OUTPUT_MOUNT}/${DIST_NAME}.tar.gz" +tar --create \ + --sort=name \ + --mtime="@${SOURCE_DATE_EPOCH}" \ + --owner=0 \ + --group=0 \ + --numeric-owner \ + --gzip \ + --file="$TARBALL" \ + -C "/tmp/staging" \ + "$DIST_NAME" + +echo "--- [build] Output: ${TARBALL}" + +# Write partial SHA256SUMS. +(cd "$OUTPUT_MOUNT" && sha256sum "${DIST_NAME}.tar.gz") \ + > "${OUTPUT_MOUNT}/SHA256SUMS.part" + +echo "--- [build] SHA256SUMS.part:" +cat "${OUTPUT_MOUNT}/SHA256SUMS.part" + +echo "--- [build] Build complete for ${HOST}" diff --git a/contrib/guix/libexec/prelude.bash b/contrib/guix/libexec/prelude.bash new file mode 100644 index 000000000..1670168eb --- /dev/null +++ b/contrib/guix/libexec/prelude.bash @@ -0,0 +1,117 @@ +#!/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. +# +# prelude.bash — shared variables and helpers for guix build scripts. + +set -euo pipefail + +################ +# Directories # +################ + +# Root of the guix contrib tree (where guix-build lives). +GUIX_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)" + +# Root of the stack_wallet source tree. +# Layout: /stack_wallet/stack_wallet-guix/contrib/guix/ (GUIX_DIR) +# /stack_wallet/stack_wallet/ (SOURCE_DIR) +: "${SOURCE_DIR:="$(cd "${GUIX_DIR}/../../../stack_wallet" && pwd)"}" + +# Pre-fetched dependency caches. +: "${BASE_CACHE:="${GUIX_DIR}/depends"}" + +export PUB_CACHE_DIR="${BASE_CACHE}/pub-cache" +export CARGO_CACHE_DIR="${BASE_CACHE}/cargo-cache" +export FLUTTER_SDK_DIR="${BASE_CACHE}/flutter-sdk" +export RUST_DIR="${BASE_CACHE}/rust-toolchains" + +# Build output directory (per invocation). +: "${OUTDIR:="${GUIX_DIR}/guix-build-$(date +%Y%m%d%H%M%S)"}" +export OUTDIR + +################ +# Versions # +################ + +export FLUTTER_VERSION="3.38.1" +export FLUTTER_CHANNEL="stable" + +export RUST_VERSION_DEFAULT="1.89.0" +export RUST_VERSION_MWC="1.85.1" +export RUST_VERSION_FROSTDART="1.71.0" +export RUST_VERSION_XELIS="1.91.0" + +# All Rust versions we need toolchains for. +export RUST_VERSIONS=("${RUST_VERSION_DEFAULT}" "${RUST_VERSION_MWC}" "${RUST_VERSION_FROSTDART}" "${RUST_VERSION_XELIS}") + +################ +# Build config # +################ + +: "${APP_NAME_ID:=stack_wallet}" +: "${APP_VERSION:=}" +: "${APP_BUILD_NUMBER:=}" +: "${JOBS:=$(nproc 2>/dev/null || echo 4)}" +: "${HOSTS:=x86_64-linux-gnu}" + +export APP_NAME_ID APP_VERSION APP_BUILD_NUMBER JOBS HOSTS + +################ +# Helpers # +################ + +log_info() { + echo "--- [guix] $*" +} + +log_error() { + echo "!!! [guix] $*" >&2 +} + +die() { + log_error "$@" + exit 1 +} + +# Verify that a file matches an expected SHA-256 hash. +verify_sha256() { + local file="$1" + local expected="$2" + local actual + actual="$(sha256sum "$file" | awk '{print $1}')" + if [ "$actual" != "$expected" ]; then + die "SHA-256 mismatch for ${file}: expected ${expected}, got ${actual}" + fi +} + +# Extract version and build number from pubspec.yaml if not set. +auto_detect_version() { + if [ -z "${APP_VERSION}" ] || [ -z "${APP_BUILD_NUMBER}" ]; then + local pubspec="${SOURCE_DIR}/pubspec.yaml" + if [ -f "$pubspec" ]; then + local version_line + version_line="$(grep '^version:' "$pubspec" | head -1)" + # Format: version: X.Y.Z+NNN + if [[ "$version_line" =~ version:\ *([0-9]+\.[0-9]+\.[0-9]+)\+([0-9]+) ]]; then + : "${APP_VERSION:="${BASH_REMATCH[1]}"}" + : "${APP_BUILD_NUMBER:="${BASH_REMATCH[2]}"}" + export APP_VERSION APP_BUILD_NUMBER + fi + fi + fi +} + +# Compute SOURCE_DATE_EPOCH from git log. +compute_source_date_epoch() { + if [ -z "${SOURCE_DATE_EPOCH:-}" ]; then + # -e handles both regular .git dirs and worktree .git files. + if [ -e "${SOURCE_DIR}/.git" ]; then + SOURCE_DATE_EPOCH="$(git -C "${SOURCE_DIR}" log -1 --format=%ct)" + else + SOURCE_DATE_EPOCH="$(date +%s)" + fi + export SOURCE_DATE_EPOCH + fi +} diff --git a/contrib/guix/manifest.scm b/contrib/guix/manifest.scm new file mode 100644 index 000000000..04d24281a --- /dev/null +++ b/contrib/guix/manifest.scm @@ -0,0 +1,67 @@ +;; Guix manifest for Stack Wallet reproducible builds. +;; +;; Declares all packages that must be present inside the guix shell container. +;; Flutter SDK and Rust toolchains are NOT Guix packages — they are hash-pinned +;; tarballs fetched by supplementary/deps/ scripts and mounted into the container. + +(specifications->manifest + (list + ;; C/C++ toolchain + "gcc-toolchain" + "cmake" + "pkg-config" + "meson" + "ninja" + + ;; System libraries required by Flutter Linux builds and plugins + "gtk+" ; GTK 3 + "glib" + "glib:bin" ; gdbus-codegen (needed by libsecret build) + "openssl" + "pango" + "cairo" + "gdk-pixbuf" + "at-spi2-core" ; successor to atk + + ;; Build tools + "git-minimal" + "python" + "perl" ; needed by openssl-sys Rust crate + "coreutils" + "findutils" + "tar" + "xz" + "gzip" + "sed" + "grep" + "gawk" + "diffutils" + "patch" + "make" + "which" + "bash" + "rsync" + "util-linux" ; for getopt + + ;; Clang/LLVM (needed by some Rust crates' build.rs) + "clang-toolchain" + + ;; Go (needed by flutter_mwebd plugin for Litecoin MWEB) + "go" + + ;; CA certificates (for Rust vendored-openssl verification) + "nss-certs" + + ;; libsecret build deps + "gobject-introspection" + "vala" + "libxslt" ; for xsltproc + "docbook-xsl" + "libgcrypt" ; required by libsecret + + ;; camera_linux plugin + "opencv" + + ;; Additional libs pulled in at link time + "eudev" ; libudev + )) diff --git a/contrib/guix/supplementary/deps/fetch-cargo-deps.sh b/contrib/guix/supplementary/deps/fetch-cargo-deps.sh new file mode 100755 index 000000000..c9050386f --- /dev/null +++ b/contrib/guix/supplementary/deps/fetch-cargo-deps.sh @@ -0,0 +1,105 @@ +#!/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. +# +# fetch-cargo-deps.sh — Pre-fetch Cargo crates for each Rust plugin workspace. +# Must be run OUTSIDE the Guix container (needs network). + +set -euo pipefail + +SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" +source "${SCRIPT_DIR}/../../libexec/prelude.bash" + +CARGO_CACHE="${CARGO_CACHE_DIR}" +mkdir -p "$CARGO_CACHE" + +# Use the default Rust toolchain for cargo fetch. +CARGO="${RUST_DIR}/${RUST_VERSION_DEFAULT}/bin/cargo" +if [ ! -x "$CARGO" ]; then + die "Rust ${RUST_VERSION_DEFAULT} not found at ${RUST_DIR}. Run fetch-pub-deps.sh first." +fi + +export CARGO_HOME="${CARGO_CACHE}" + +# Vendor crates for a given workspace directory. +vendor_workspace() { + local name="$1" + local workspace_dir="$2" + local rust_version="${3:-${RUST_VERSION_DEFAULT}}" + local cargo_bin="${RUST_DIR}/${rust_version}/bin/cargo" + + if [ ! -d "$workspace_dir" ]; then + log_info "Skipping ${name}: workspace not found at ${workspace_dir}" + return + fi + + local vendor_dir="${CARGO_CACHE}/vendor-${name}" + if [ -d "$vendor_dir" ]; then + log_info "${name}: vendor directory exists, skipping (delete to re-fetch)" + return + fi + + log_info "Vendoring crates for ${name} (Rust ${rust_version}) ..." + mkdir -p "$vendor_dir" + + # cargo vendor prints a .cargo/config.toml snippet to stdout that tells + # cargo how to redirect all sources (crates-io + git) to the vendor dir. + # Capture it so build.sh can use it. + local config_file="${CARGO_CACHE}/vendor-${name}-config.toml" + ( + cd "$workspace_dir" + "$cargo_bin" vendor --versioned-dirs "$vendor_dir" 2>/dev/null + ) > "$config_file" + + log_info "${name}: vendored to ${vendor_dir}" + log_info "${name}: config saved to ${config_file}" +} + +############################################################ +# Vendor each Rust plugin workspace # +############################################################ + +CRYPTO="${SOURCE_DIR}/crypto_plugins" + +# flutter_libepiccash — the Rust code lives in crypto_plugins/flutter_libepiccash/rust/ +vendor_workspace "epiccash" \ + "${CRYPTO}/flutter_libepiccash/rust" \ + "${RUST_VERSION_DEFAULT}" + +# flutter_libmwc — Rust code in crypto_plugins/flutter_libmwc/rust/ +vendor_workspace "mwc" \ + "${CRYPTO}/flutter_libmwc/rust" \ + "${RUST_VERSION_MWC}" + +# frostdart — Rust code in crypto_plugins/frostdart/src/serai/ +vendor_workspace "frostdart" \ + "${CRYPTO}/frostdart/src/serai" \ + "${RUST_VERSION_FROSTDART}" + +############################################################ +# tor_ffi_plugin (cargokit — Rust code in pub git cache) # +############################################################ + +# The tor plugin's Rust workspace lives inside the pub git cache, not in +# crypto_plugins. Locate it by the resolved-ref hash. +TOR_WORKSPACE="" +for d in "${PUB_CACHE_DIR}"/git/tor-*/rust; do + if [ -f "$d/Cargo.toml" ]; then + TOR_WORKSPACE="$d" + break + fi +done + +vendor_workspace "tor" \ + "${TOR_WORKSPACE}" \ + "${RUST_VERSION_DEFAULT}" + +############################################################ +# Also vendor secp256k1 build deps (cmake-based, no Cargo) # +############################################################ + +log_info "secp256k1 is built via cmake (no Cargo deps to vendor)" + +log_info "=== Cargo dependency fetch complete ===" +log_info "Vendor dirs created in ${CARGO_CACHE}/" diff --git a/contrib/guix/supplementary/deps/fetch-pub-deps.sh b/contrib/guix/supplementary/deps/fetch-pub-deps.sh new file mode 100755 index 000000000..2dfd2fb46 --- /dev/null +++ b/contrib/guix/supplementary/deps/fetch-pub-deps.sh @@ -0,0 +1,383 @@ +#!/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. +# +# fetch-pub-deps.sh — Pre-fetch Dart pub dependencies, Flutter SDK, and Rust +# toolchains. Must be run OUTSIDE the Guix container (needs network). + +set -euo pipefail + +SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" +source "${SCRIPT_DIR}/../../libexec/prelude.bash" + +auto_detect_version + +############################################################ +# Flutter SDK # +############################################################ + +FLUTTER_TARBALL="flutter_linux_${FLUTTER_VERSION}-${FLUTTER_CHANNEL}.tar.xz" +FLUTTER_URL="https://storage.googleapis.com/flutter_infra_release/releases/${FLUTTER_CHANNEL}/linux/${FLUTTER_TARBALL}" +# Pin the SHA-256 of the Flutter SDK tarball. Update when FLUTTER_VERSION changes. +FLUTTER_SHA256="${FLUTTER_SDK_SHA256:-}" + +fetch_flutter_sdk() { + log_info "Fetching Flutter SDK ${FLUTTER_VERSION} ..." + mkdir -p "${FLUTTER_SDK_DIR}" + + local tarball="${BASE_CACHE}/${FLUTTER_TARBALL}" + if [ ! -f "$tarball" ]; then + curl -L --fail -o "$tarball" "$FLUTTER_URL" + fi + + if [ -n "$FLUTTER_SHA256" ]; then + verify_sha256 "$tarball" "$FLUTTER_SHA256" + else + log_info "FLUTTER_SDK_SHA256 not set — recording hash for future pinning:" + sha256sum "$tarball" + fi + + # Extract only if flutter/bin/flutter doesn't exist yet. + if [ ! -x "${FLUTTER_SDK_DIR}/flutter/bin/flutter" ]; then + log_info "Extracting Flutter SDK ..." + tar -xf "$tarball" -C "${FLUTTER_SDK_DIR}" + fi + log_info "Flutter SDK ready at ${FLUTTER_SDK_DIR}/flutter" +} + +############################################################ +# Rust toolchains # +############################################################ + +fetch_rust_toolchains() { + mkdir -p "${RUST_DIR}" + local target="x86_64-unknown-linux-gnu" + + for version in "${RUST_VERSIONS[@]}"; do + local dir="${RUST_DIR}/${version}" + if [ -d "${dir}" ] && [ -x "${dir}/bin/rustc" ]; then + log_info "Rust ${version} already present, skipping" + continue + fi + + log_info "Fetching Rust ${version} ..." + local tarball="rust-${version}-${target}.tar.gz" + local url="https://static.rust-lang.org/dist/${tarball}" + local dest="${BASE_CACHE}/${tarball}" + + curl -L --fail -o "$dest" "$url" + + # Also fetch the SHA-256 checksum file from upstream. + local sha_url="${url}.sha256" + local expected + expected="$(curl -sL --fail "$sha_url" | awk '{print $1}')" + verify_sha256 "$dest" "$expected" + + local tmp="${BASE_CACHE}/rust-extract-tmp" + rm -rf "$tmp" + mkdir -p "$tmp" + tar -xzf "$dest" -C "$tmp" + + # The official installer places everything under a prefix. + # Run install.sh with a --prefix to get a usable sysroot. + ( + cd "${tmp}/rust-${version}-${target}" + ./install.sh --prefix="${dir}" --without=rust-docs + ) + rm -rf "$tmp" + log_info "Rust ${version} installed to ${dir}" + done + + # Also fetch the Rust src component for versions that need it (some crates + # use -Z build-std or need the src for proc-macros). + for version in "${RUST_VERSIONS[@]}"; do + local src_dir="${RUST_DIR}/${version}/lib/rustlib/src/rust" + if [ -d "$src_dir" ]; then + continue + fi + log_info "Fetching rust-src for ${version} ..." + local tarball="rust-src-${version}.tar.gz" + local url="https://static.rust-lang.org/dist/${tarball}" + local dest="${BASE_CACHE}/${tarball}" + curl -L --fail -o "$dest" "$url" + + local tmp="${BASE_CACHE}/rust-src-extract-tmp" + rm -rf "$tmp" + mkdir -p "$tmp" + tar -xzf "$dest" -C "$tmp" + ( + cd "${tmp}/rust-src-${version}" + ./install.sh --prefix="${RUST_DIR}/${version}" 2>/dev/null || true + ) + rm -rf "$tmp" + done +} + +############################################################ +# Dart pub hosted packages # +############################################################ + +fetch_hosted_packages() { + local lockfile="${1:-${SOURCE_DIR}/pubspec.lock}" + local label="${2:-pubspec.lock}" + [ -f "$lockfile" ] || die "Lockfile not found at ${lockfile}" + + local pub_hosted_dir="${PUB_CACHE_DIR}/hosted/pub.dev" + mkdir -p "$pub_hosted_dir" + + log_info "Parsing hosted packages from ${label} ..." + + # Parse pubspec.lock YAML (simple state machine — no external YAML parser needed). + local in_packages=0 current_name="" current_version="" current_sha256="" + local current_source="" count=0 skipped=0 + + while IFS= read -r line; do + # Top-level package name (2-space indent, ends with colon). + if [[ "$line" =~ ^\ \ ([a-zA-Z0-9_]+):$ ]]; then + # Flush previous package. + if [ -n "$current_name" ] && [ "$current_source" = "hosted" ] && [ -n "$current_version" ]; then + _fetch_one_hosted "$current_name" "$current_version" "$current_sha256" + count=$((count + 1)) + fi + current_name="${BASH_REMATCH[1]}" + current_version="" + current_sha256="" + current_source="" + elif [[ "$line" =~ ^\ {4}source:\ *\"?([a-z]+)\"? ]]; then + current_source="${BASH_REMATCH[1]}" + elif [[ "$line" =~ ^\ {4}version:\ *\"([^\"]+)\" ]]; then + current_version="${BASH_REMATCH[1]}" + elif [[ "$line" =~ ^\ {6}sha256:\ *\"?([0-9a-f]+)\"? ]]; then + current_sha256="${BASH_REMATCH[1]}" + fi + done < "$lockfile" + + # Flush last package. + if [ -n "$current_name" ] && [ "$current_source" = "hosted" ] && [ -n "$current_version" ]; then + _fetch_one_hosted "$current_name" "$current_version" "$current_sha256" + count=$((count + 1)) + fi + + log_info "Fetched ${count} hosted packages from ${label} (${skipped} already cached)" +} + +_fetch_one_hosted() { + local name="$1" version="$2" sha256="$3" + local pkg_dir="${PUB_CACHE_DIR}/hosted/pub.dev/${name}-${version}" + + if [ -d "$pkg_dir" ]; then + skipped=$((skipped + 1)) + return + fi + + local url="https://pub.dev/api/archives/${name}-${version}.tar.gz" + local tarball="${BASE_CACHE}/pub-archives/${name}-${version}.tar.gz" + mkdir -p "${BASE_CACHE}/pub-archives" + + if [ ! -f "$tarball" ]; then + curl -sL --fail -o "$tarball" "$url" || { + log_error "Failed to fetch ${name}-${version}" + return 1 + } + fi + + if [ -n "$sha256" ]; then + verify_sha256 "$tarball" "$sha256" + fi + + mkdir -p "$pkg_dir" + tar -xzf "$tarball" -C "$pkg_dir" +} + +############################################################ +# Dart pub git packages # +############################################################ + +fetch_git_packages() { + local lockfile="${SOURCE_DIR}/pubspec.lock" + + local git_cache_dir="${PUB_CACHE_DIR}/git/cache" + local git_pkg_dir="${PUB_CACHE_DIR}/git" + mkdir -p "$git_cache_dir" + + log_info "Parsing git packages from pubspec.lock ..." + + local in_git_desc=0 current_name="" current_url="" current_ref="" current_resolved="" + local current_source="" current_path="" count=0 + + while IFS= read -r line; do + if [[ "$line" =~ ^\ \ ([a-zA-Z0-9_]+):$ ]]; then + # Flush previous. + if [ -n "$current_name" ] && [ "$current_source" = "git" ] && [ -n "$current_url" ]; then + _fetch_one_git "$current_name" "$current_url" "$current_resolved" "$current_path" + count=$((count + 1)) + fi + current_name="${BASH_REMATCH[1]}" + current_url="" current_ref="" current_resolved="" current_source="" current_path="" + in_git_desc=0 + elif [[ "$line" =~ ^\ {4}source:\ *\"?([a-z]+)\"? ]]; then + current_source="${BASH_REMATCH[1]}" + elif [[ "$line" =~ ^\ {6}url:\ *\"([^\"]+)\" ]]; then + current_url="${BASH_REMATCH[1]}" + elif [[ "$line" =~ ^\ {6}ref:\ *\"?([^\"[:space:]]+)\"? ]]; then + current_ref="${BASH_REMATCH[1]}" + elif [[ "$line" =~ ^\ {6}resolved-ref:\ *\"?([0-9a-f]+)\"? ]]; then + current_resolved="${BASH_REMATCH[1]}" + elif [[ "$line" =~ ^\ {6}path:\ *\"([^\"]+)\" ]]; then + current_path="${BASH_REMATCH[1]}" + fi + done < "$lockfile" + + # Flush last. + if [ -n "$current_name" ] && [ "$current_source" = "git" ] && [ -n "$current_url" ]; then + _fetch_one_git "$current_name" "$current_url" "$current_resolved" "$current_path" + count=$((count + 1)) + fi + + log_info "Fetched ${count} git packages" +} + +_fetch_one_git() { + local name="$1" url="$2" resolved_ref="$3" path="${4:-.}" + + # Dart pub caches bare repos keyed by a SHA-1 hash of the URL. + local url_hash + url_hash="$(echo -n "$url" | sha1sum | cut -c1-40)" + local bare_dir="${PUB_CACHE_DIR}/git/cache/${url_hash}" + + if [ ! -d "$bare_dir" ]; then + log_info "Cloning (bare) ${url} ..." + git clone --bare "$url" "$bare_dir" + else + log_info "Updating bare cache for ${url} ..." + git -C "$bare_dir" fetch --all --prune 2>/dev/null || true + fi + + # Checkout the resolved ref into the working tree location that pub expects. + local ref_short="${resolved_ref:0:8}" + local checkout_dir="${PUB_CACHE_DIR}/git/${name}-${resolved_ref}" + + if [ ! -d "$checkout_dir" ]; then + log_info "Checking out ${name} at ${ref_short}..." + git clone --no-checkout "$bare_dir" "$checkout_dir" + git -C "$checkout_dir" checkout "$resolved_ref" + fi +} + +############################################################ +# Path packages (just verify submodules) # +############################################################ + +verify_path_packages() { + log_info "Verifying path packages (crypto_plugins submodules) ..." + local crypto_dir="${SOURCE_DIR}/crypto_plugins" + for plugin in flutter_libepiccash flutter_libmwc frostdart; do + if [ -d "${crypto_dir}/${plugin}" ]; then + log_info " ${plugin}: OK" + else + log_error " ${plugin}: MISSING — run 'git submodule update --init' in the source tree" + fi + done +} + +############################################################ +# Native C dependency sources (cloned at build time by # +# existing scripts — we pre-fetch them for offline builds) # +############################################################ + +fetch_native_sources() { + local native_dir="${BASE_CACHE}/native-sources" + mkdir -p "$native_dir" + + # sqlite3 source (used by sqlite3_flutter_libs CMake FetchContent). + local sqlite_ver="3460000" + local sqlite_tarball="sqlite-autoconf-${sqlite_ver}.tar.gz" + local sqlite_dest="${native_dir}/${sqlite_tarball}" + if [ ! -f "$sqlite_dest" ]; then + log_info "Fetching sqlite3 source ..." + curl -L --fail -o "$sqlite_dest" \ + "https://sqlite.org/2024/${sqlite_tarball}" + fi + log_info " sqlite3: OK" + + # Boost 1.71.0 (used by flutter_libsparkmobile CMake FetchContent). + local boost_zip="boost_1_71_0.zip" + local boost_dest="${native_dir}/${boost_zip}" + if [ ! -f "$boost_dest" ]; then + log_info "Fetching Boost 1.71.0 ..." + curl -L --fail -o "$boost_dest" \ + "https://archives.boost.io/release/1.71.0/source/${boost_zip}" + fi + verify_sha256 "$boost_dest" "85a94ac71c28e59cf97a96714e4c58a18550c227ac8b0388c260d6c717e47a69" + log_info " boost: OK" + + # JsonCPP 1.7.4 + local jsoncpp_dir="${native_dir}/jsoncpp" + if [ ! -d "$jsoncpp_dir" ]; then + log_info "Cloning jsoncpp ..." + git clone https://github.com/open-source-parsers/jsoncpp.git "$jsoncpp_dir" + fi + git -C "$jsoncpp_dir" fetch --all 2>/dev/null || true + log_info " jsoncpp: OK" + + # libsecret 0.21.4 (CypherStack fork) + local libsecret_dir="${native_dir}/libsecret" + if [ ! -d "$libsecret_dir" ]; then + log_info "Cloning libsecret ..." + git clone https://git.cypherstack.com/Cypher_Stack/libsecret.git "$libsecret_dir" + fi + git -C "$libsecret_dir" fetch --all 2>/dev/null || true + log_info " libsecret: OK" + + # secp256k1 (bitcoin-core) + local secp_dir="${native_dir}/secp256k1" + if [ ! -d "$secp_dir" ]; then + log_info "Cloning secp256k1 ..." + git clone https://github.com/bitcoin-core/secp256k1 "$secp_dir" + fi + git -C "$secp_dir" fetch --all 2>/dev/null || true + log_info " secp256k1: OK" +} + +############################################################ +# Main # +############################################################ + +main() { + log_info "=== Fetching dependencies for Stack Wallet Guix build ===" + log_info "Source dir: ${SOURCE_DIR}" + log_info "Cache dir: ${BASE_CACHE}" + + fetch_flutter_sdk + fetch_rust_toolchains + fetch_hosted_packages "${SOURCE_DIR}/pubspec.lock" "Stack Wallet pubspec.lock" + + # Flutter tools' own dependencies (needed by PubDependencies.update inside + # the container — Flutter's build tool runs `dart pub get` on its own + # packages/flutter_tools directory). + local flutter_tools_lock="${FLUTTER_SDK_DIR}/flutter/packages/flutter_tools/pubspec.lock" + if [ -f "$flutter_tools_lock" ]; then + fetch_hosted_packages "$flutter_tools_lock" "Flutter tools pubspec.lock" + fi + + fetch_git_packages + verify_path_packages + + # Cargokit build_tool dependencies (tor_ffi_plugin uses cargokit which + # has its own Dart build tool with pinned dependencies). + local cargokit_lock + for cargokit_lock in "${PUB_CACHE_DIR}"/git/tor-*/cargokit/build_tool/pubspec.lock; do + if [ -f "$cargokit_lock" ]; then + fetch_hosted_packages "$cargokit_lock" "cargokit build_tool pubspec.lock" + break + fi + done + + fetch_native_sources + + log_info "=== All dependencies fetched ===" + log_info "Next step: run fetch-cargo-deps.sh, then guix-build" +} + +main "$@" diff --git a/contrib/guix/supplementary/deps/verify-deps.sh b/contrib/guix/supplementary/deps/verify-deps.sh new file mode 100755 index 000000000..5de15bb4c --- /dev/null +++ b/contrib/guix/supplementary/deps/verify-deps.sh @@ -0,0 +1,221 @@ +#!/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. +# +# verify-deps.sh — Re-verify all pre-fetched dependency hashes. + +set -euo pipefail + +SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" +source "${SCRIPT_DIR}/../../libexec/prelude.bash" + +ERRORS=0 + +############################################################ +# Verify Flutter SDK # +############################################################ + +verify_flutter() { + log_info "Verifying Flutter SDK ..." + local flutter_bin="${FLUTTER_SDK_DIR}/flutter/bin/flutter" + if [ ! -x "$flutter_bin" ]; then + log_error "Flutter SDK not found at ${FLUTTER_SDK_DIR}/flutter" + ERRORS=$((ERRORS + 1)) + return + fi + local actual_version + actual_version="$("$flutter_bin" --version --machine 2>/dev/null | grep -o '"frameworkVersion":"[^"]*"' | cut -d'"' -f4)" || true + if [ -n "$actual_version" ] && [ "$actual_version" != "$FLUTTER_VERSION" ]; then + log_error "Flutter version mismatch: expected ${FLUTTER_VERSION}, got ${actual_version}" + ERRORS=$((ERRORS + 1)) + else + log_info " Flutter SDK ${FLUTTER_VERSION}: OK" + fi +} + +############################################################ +# Verify Rust toolchains # +############################################################ + +verify_rust() { + log_info "Verifying Rust toolchains ..." + for version in "${RUST_VERSIONS[@]}"; do + local rustc="${RUST_DIR}/${version}/bin/rustc" + if [ ! -x "$rustc" ]; then + log_error "Rust ${version} not found at ${RUST_DIR}/${version}" + ERRORS=$((ERRORS + 1)) + continue + fi + local actual + actual="$("$rustc" --version | awk '{print $2}')" + if [ "$actual" != "$version" ]; then + log_error "Rust version mismatch: expected ${version}, got ${actual}" + ERRORS=$((ERRORS + 1)) + else + log_info " Rust ${version}: OK" + fi + done +} + +############################################################ +# Verify pub hosted packages against pubspec.lock # +############################################################ + +verify_hosted_packages() { + local lockfile="${SOURCE_DIR}/pubspec.lock" + [ -f "$lockfile" ] || die "pubspec.lock not found" + + log_info "Verifying hosted packages against pubspec.lock ..." + + local count=0 missing=0 + local current_name="" current_version="" current_source="" + + while IFS= read -r line; do + if [[ "$line" =~ ^\ \ ([a-zA-Z0-9_]+):$ ]]; then + if [ -n "$current_name" ] && [ "$current_source" = "hosted" ] && [ -n "$current_version" ]; then + local pkg_dir="${PUB_CACHE_DIR}/hosted/pub.dev/${current_name}-${current_version}" + if [ -d "$pkg_dir" ]; then + count=$((count + 1)) + else + log_error " MISSING: ${current_name}-${current_version}" + missing=$((missing + 1)) + fi + fi + current_name="${BASH_REMATCH[1]}" + current_version="" current_source="" + elif [[ "$line" =~ ^\ {4}source:\ *\"?([a-z]+)\"? ]]; then + current_source="${BASH_REMATCH[1]}" + elif [[ "$line" =~ ^\ {4}version:\ *\"([^\"]+)\" ]]; then + current_version="${BASH_REMATCH[1]}" + fi + done < "$lockfile" + + # Flush last. + if [ -n "$current_name" ] && [ "$current_source" = "hosted" ] && [ -n "$current_version" ]; then + local pkg_dir="${PUB_CACHE_DIR}/hosted/pub.dev/${current_name}-${current_version}" + if [ -d "$pkg_dir" ]; then + count=$((count + 1)) + else + log_error " MISSING: ${current_name}-${current_version}" + missing=$((missing + 1)) + fi + fi + + if [ "$missing" -gt 0 ]; then + log_error "${missing} hosted packages missing!" + ERRORS=$((ERRORS + missing)) + fi + log_info " ${count} hosted packages verified present" +} + +############################################################ +# Verify git packages # +############################################################ + +verify_git_packages() { + local lockfile="${SOURCE_DIR}/pubspec.lock" + + log_info "Verifying git packages ..." + + local count=0 missing=0 + local current_name="" current_source="" current_url="" current_resolved="" + + while IFS= read -r line; do + if [[ "$line" =~ ^\ \ ([a-zA-Z0-9_]+):$ ]]; then + if [ -n "$current_name" ] && [ "$current_source" = "git" ] && [ -n "$current_resolved" ]; then + local checkout_dir="${PUB_CACHE_DIR}/git/${current_name}-${current_resolved}" + if [ -d "$checkout_dir" ]; then + count=$((count + 1)) + else + log_error " MISSING: ${current_name} @ ${current_resolved:0:8}" + missing=$((missing + 1)) + fi + fi + current_name="${BASH_REMATCH[1]}" + current_source="" current_url="" current_resolved="" + elif [[ "$line" =~ ^\ {4}source:\ *\"?([a-z]+)\"? ]]; then + current_source="${BASH_REMATCH[1]}" + elif [[ "$line" =~ ^\ {6}resolved-ref:\ *\"?([0-9a-f]+)\"? ]]; then + current_resolved="${BASH_REMATCH[1]}" + fi + done < "$lockfile" + + # Flush last. + if [ -n "$current_name" ] && [ "$current_source" = "git" ] && [ -n "$current_resolved" ]; then + local checkout_dir="${PUB_CACHE_DIR}/git/${current_name}-${current_resolved}" + if [ -d "$checkout_dir" ]; then + count=$((count + 1)) + else + log_error " MISSING: ${current_name} @ ${current_resolved:0:8}" + missing=$((missing + 1)) + fi + fi + + if [ "$missing" -gt 0 ]; then + log_error "${missing} git packages missing!" + ERRORS=$((ERRORS + missing)) + fi + log_info " ${count} git packages verified present" +} + +############################################################ +# Verify Cargo vendor directories # +############################################################ + +verify_cargo_vendors() { + log_info "Verifying Cargo vendor directories ..." + for name in epiccash mwc frostdart; do + local vendor_dir="${CARGO_CACHE_DIR}/vendor-${name}" + if [ -d "$vendor_dir" ]; then + local crate_count + crate_count="$(find "$vendor_dir" -mindepth 1 -maxdepth 1 -type d | wc -l)" + log_info " ${name}: ${crate_count} crates" + else + log_error " ${name}: vendor directory MISSING" + ERRORS=$((ERRORS + 1)) + fi + done +} + +############################################################ +# Verify native C dependency sources # +############################################################ + +verify_native_sources() { + log_info "Verifying native C dependency sources ..." + local native_dir="${BASE_CACHE}/native-sources" + for repo in jsoncpp libsecret secp256k1; do + if [ -d "${native_dir}/${repo}/.git" ]; then + log_info " ${repo}: OK" + else + log_error " ${repo}: MISSING at ${native_dir}/${repo}" + ERRORS=$((ERRORS + 1)) + fi + done +} + +############################################################ +# Main # +############################################################ + +main() { + log_info "=== Verifying pre-fetched dependencies ===" + log_info "Source dir: ${SOURCE_DIR}" + log_info "Cache dir: ${BASE_CACHE}" + + verify_flutter + verify_rust + verify_hosted_packages + verify_git_packages + verify_cargo_vendors + verify_native_sources + + echo "" + if [ "$ERRORS" -gt 0 ]; then + die "Verification FAILED with ${ERRORS} error(s). Run fetch scripts to fix." + fi + log_info "=== All dependencies verified OK ===" +} + +main "$@" diff --git a/scripts/build_app.sh b/scripts/build_app.sh index 30bbc8215..61e8fd94a 100755 --- a/scripts/build_app.sh +++ b/scripts/build_app.sh @@ -9,7 +9,8 @@ APP_NAMED_IDS=("stack_wallet" "stack_duo" "campfire") # Function to display usage. usage() { - echo "Usage: $0 -v -b -p -a [-i] [-f]" + echo "Usage: $0 -v -b -p -a [-i] [-f] [-g]" + echo " -g Guix reproducible build (linux only). Delegates to contrib/guix/guix-build." exit 1 } @@ -34,9 +35,10 @@ unset -v APP_NAMED_ID # optional args (with defaults) BUILD_CRYPTO_PLUGINS=0 BUILD_ISAR_FROM_SOURCE=0 +BUILD_WITH_GUIX=0 # Parse command-line arguments. -while getopts "v:b:p:a:i:f" opt; do +while getopts "v:b:p:a:ifg" opt; do case "${opt}" in v) APP_VERSION_STRING="$OPTARG" ;; b) APP_BUILD_NUMBER="$OPTARG" ;; @@ -44,6 +46,7 @@ while getopts "v:b:p:a:i:f" opt; do a) APP_NAMED_ID="$OPTARG" ;; i) BUILD_CRYPTO_PLUGINS=1 ;; f) BUILD_ISAR_FROM_SOURCE=1 ;; + g) BUILD_WITH_GUIX=1 ;; *) usage ;; esac done @@ -69,6 +72,27 @@ if [ -z "$APP_NAMED_ID" ]; then fi confirmDisclaimer + +# Guix reproducible build: short-circuit before any configure/build steps. +# guix-build handles its own source mounting, config, and build inside a container. +if [ "$BUILD_WITH_GUIX" -eq 1 ]; then + if [ "$APP_BUILD_PLATFORM" != "linux" ]; then + echo "Error: -g (Guix build) is only supported with -p linux" + exit 1 + fi + GUIX_BUILD="${APP_PROJECT_ROOT_DIR}/contrib/guix/guix-build" + if [ ! -x "$GUIX_BUILD" ]; then + echo "guix-build not found at: $GUIX_BUILD" + echo "Ensure contrib/guix/ exists (e.g. feat/guix branch)." + exit 1 + fi + export SOURCE_DIR="$APP_PROJECT_ROOT_DIR" + exec "$GUIX_BUILD" \ + --app "$APP_NAMED_ID" \ + --version "$APP_VERSION_STRING" \ + --build-number "$APP_BUILD_NUMBER" +fi + set -x source "${APP_PROJECT_ROOT_DIR}/scripts/app_config/templates/configure_template_files.sh"