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-*/
72 changes: 72 additions & 0 deletions contrib/guix/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,72 @@
# Experimental Guix builds for Stack Wallet

**Blocked on current staging:** this prototype predates the migration from
`crypto_plugins` submodules to Dart native-assets hooks. Merging staging does not
make the old offline build recipe compatible. The fetch, verification, and build
entry points now fail before downloading dependencies or starting a container
when the required legacy workspaces are absent. This is a diagnostic guard, not
an implementation of native-assets support.

The orchestration, cache, and attestation scripts remain useful as a starting
point. They have not produced a verified reproducible build of current staging.
Do not use their presence checks or signed checksums as evidence that the native
libraries were built reproducibly from reviewed source.

## Work required before enabling builds

1. Resolve native packages from the generated app configuration and locked pub
dependencies. Replace all `crypto_plugins` paths and legacy Cargokit setup.
2. Vendor every selected native hook's Rust dependencies (including Xelis and
Tor), pin the toolchain each hook actually invokes, and honor locked sources.
3. Fetch and verify the Go module cache for MWEB and all CMake/source downloads.
Make the hooks consume those caches without network access. The current
scripts do not populate the `go-cache` mount.
4. Pin Guix channels, Flutter SDK archives, Rust archives, and native sources.
Verify extracted cache contents as well as archive hashes. `verify-deps.sh`
currently checks package presence; it does not authenticate those contents.
5. Build the same commit independently in two clean network-isolated containers,
compare outputs, investigate Flutter AOT differences, and only then publish
reproducibility claims or attestations.

The historical variant table is obsolete: current native packages and hooks
must be derived from each variant's generated pubspec, including Campfire.

## Entry points

Run commands from `contrib/guix`. `SOURCE_DIR` defaults to the checkout containing
these scripts, including Git worktrees; set it explicitly only for another source
checkout. The current staging SDK version is Flutter 3.47.2.

```bash
# These commands intentionally stop on current staging until the port is ready.
supplementary/deps/fetch-pub-deps.sh
supplementary/deps/fetch-cargo-deps.sh
supplementary/deps/verify-deps.sh
./guix-build --app stack_wallet --version 2.3.4 --build-number 234
```

The fetch/build/verification pipeline requires `FLUTTER_SDK_SHA256`, a 64-digit
SHA-256 obtained independently for the exact Flutter Linux SDK archive. It no
longer accepts an unverified download and merely prints its hash. SDK metadata
must parse and match the pinned version; empty or malformed output fails.

`guix-build` accepts `--app stack_wallet|stack_duo|campfire`, `--version`,
`--build-number`, `--jobs`, and `--hosts`. Linux x86_64 is the only intended target.
`BASE_CACHE` selects the dependency cache; `OUTDIR` selects output;
`SOURCE_DATE_EPOCH` otherwise comes from the source commit.

`guix-attest` signs build checksums with GPG, and `guix-verify` compares
attestations. Signing proves who signed particular checksums, not that the
build is reproducible. `guix-clean` removes outputs; `--all` also removes caches.

## Local validation

From the repository root:

```bash
python3 -m unittest discover -s contrib/guix/tests -v
```

The tests exercise worktree source selection, early failure on current staging,
checksum validation, and SDK metadata handling. They do not run a Guix container
or demonstrate artifact reproducibility.
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."
160 changes: 160 additions & 0 deletions contrib/guix/guix-build
Original file line number Diff line number Diff line change
@@ -0,0 +1,160 @@
#!/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

require_supported_native_layout
require_flutter_checksum
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" "${BASE_CACHE}/go-cache"; 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_flutter_sdk_version "${FLUTTER_SDK_DIR}/flutter/bin/flutter"

# 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
Loading
Loading