Skip to content

refactor: reproducible builds from devcontainers - #2017

Merged
i5hi merged 26 commits into
developfrom
repro-from-devcontainers
Apr 14, 2026
Merged

refactor: reproducible builds from devcontainers#2017
i5hi merged 26 commits into
developfrom
repro-from-devcontainers

Conversation

@ethicnology

Copy link
Copy Markdown
Member
  • Reproducible builds: two-stage Docker build root Dockerfile (toolchain only my devcontainers works)
  • new Dockerfile.apk (crafted from @basantagoswami 's reproducibility branch)
  • Verification tooling: reproducibility/verify_build.sh downloads an official APK (GitHub or local), rebuilds from source, decodes both with apktool, and diffs pre-signature contents (from @basantagoswami 's reproducibility branch)
  • Makefile targets: make apk [release|debug], make verify VERSION=x.y.z [APK=path]

basantagoswami and others added 17 commits March 24, 2026 01:04
- Remove sudo entirely; all privileged setup done as root before USER switch
- Consolidate all apt installs into a single RUN block including openjdk-21-jdk
- Remove unused packages: zip, software-properties-common
- Replace wget with curl throughout
- Rename USER to APP_USER to avoid shadowing the standard Linux variable
- Move all ENV declarations to the top; derive HOME from APP_USER
- Fix RUSTFLAGS hardcoded /home/docker path to use $HOME
- Merge Rust install, verification, and rustup target adds into one RUN
- Merge Android SDK setup and sdkmanager calls into one RUN
- Copy directly into /app instead of staging directory
- Remove redundant fvm flutter clean on fresh clone
- Add ENV_SOURCE arg to control .env origin (template or local)
- Add FAKE_KEYSTORE arg with secret mount support for production signing
- Update makefile docker-build to expose all build args as overridable variables
…ld timestamp

OpenSSL embeds a wall-clock build timestamp in compiled binaries, causing .so differences between Docker builds run at different times. Setting SOURCE_DATE_EPOCH to the latest git commit timestamp fixes this in both the Dockerfile and the makefile build target.
… Dockerfile

Root Dockerfile
- Remove SOURCE/VERSION args; always build from local source
- Fix ENV_SOURCE=local branch which was a no-op (cp file to itself)

reproducibility/Dockerfile
- Add --platform=linux/amd64; upgrade Java 17 -> 21
- Replace wget with curl; drop wget from apt installs
- Remove apksigner and Android SDK setup — only used for informational output that never affected the verdict
- Merge apktool and bundletool RUN layers

reproducibility/verify_build.sh
- Move all validation (args, git tag check, required tools) before any work
- Add --yes flag to skip interactive prompts for CI/automation
- Replace wget with curl; replace grep -c "^" with wc -l
- Use container_name consistently; get commit hash from local git instead of spinning up a container
- Write RESULTS.md to workspace with verdict, metadata, and full diff
…en --apk is provided, skip tag check for local APK verification
Resolve conflicts in Dockerfile and makefile:
- Dockerfile: keep develop's toolchain-only image (debian:trixie, pinned versions)
- Dockerfile.apk: new file for the APK build stage (FROM bull-mobile)
- makefile: combine both branches' targets, add 'make apk [release|debug]' and 'make verify'
- verify_build.sh: update to use two-stage Docker build
@ethicnology

Copy link
Copy Markdown
Member Author

Running this test:

#!/bin/bash
# ==============================================================================
# Bull Bitcoin Mobile - Reproducibility Test
# Builds the APK twice and compares pre-signature contents.
# If the diff is empty, the build is reproducible.
# ==============================================================================

set -euo pipefail

SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
REPO_ROOT="$(cd "$SCRIPT_DIR/.." && pwd)"

RED='\033[1;31m'
GREEN='\033[1;32m'
YELLOW='\033[1;33m'
NC='\033[0m'

# Detect container runtime
if command -v podman &> /dev/null; then
    CTR="podman"
elif command -v docker &> /dev/null; then
    CTR="docker"
else
    echo -e "${RED}Error: Neither podman nor docker found.${NC}"
    exit 1
fi
echo "Using $CTR"

# Parse arguments
MODE="${1:-debug}"
if [[ "$MODE" != "debug" && "$MODE" != "release" ]]; then
    echo "Usage: $0 [debug|release]"
    exit 1
fi

WORK_DIR="$SCRIPT_DIR/reproducibility_test_$(date +%s)"
mkdir -p "$WORK_DIR"
echo "Workspace: $WORK_DIR"

APK_PATH="build/app/outputs/flutter-apk/app-${MODE}.apk"
VERIFY_TOOLS_IMAGE="bullbitcoin-verify-tools:latest"

# Build verification tools container
echo "Building verification tools container..."
$CTR build -q -t "$VERIFY_TOOLS_IMAGE" "$SCRIPT_DIR" > /dev/null

# --- Build 1 ---
echo ""
echo -e "${YELLOW}=== Build 1 ===${NC}"
cd "$REPO_ROOT"
make apk "$MODE"

$CTR create --name repro_build1_$$ bull-mobile-apk > /dev/null
$CTR cp "repro_build1_$$:/app/$APK_PATH" "$WORK_DIR/build1.apk"
$CTR rm repro_build1_$$ > /dev/null

echo "Saved: $WORK_DIR/build1.apk"
sha256sum "$WORK_DIR/build1.apk"

# --- Build 2 (no cache) ---
echo ""
echo -e "${YELLOW}=== Build 2 (no cache) ===${NC}"
$CTR rmi bull-mobile-apk > /dev/null 2>&1 || true

make apk "$MODE"

$CTR create --name repro_build2_$$ bull-mobile-apk > /dev/null
$CTR cp "repro_build2_$$:/app/$APK_PATH" "$WORK_DIR/build2.apk"
$CTR rm repro_build2_$$ > /dev/null

echo "Saved: $WORK_DIR/build2.apk"
sha256sum "$WORK_DIR/build2.apk"

# --- Decode both APKs ---
echo ""
echo -e "${YELLOW}=== Decoding APKs ===${NC}"

$CTR run --rm \
    -v "$WORK_DIR":/work \
    "$VERIFY_TOOLS_IMAGE" \
    sh -c "apktool d -f -o /work/decoded1 /work/build1.apk && \
           apktool d -f -o /work/decoded2 /work/build2.apk"

# --- Compare ---
echo ""
echo "=== Comparing pre-signature contents ==="
diff_output=$(diff -r "$WORK_DIR/decoded1" "$WORK_DIR/decoded2" | grep -v META-INF || true)

if [[ -z "$diff_output" ]]; then
    echo -e "${GREEN}BUILD IS REPRODUCIBLE${NC}"
    echo "Both builds are identical (excluding META-INF signatures)."
    exit 0
else
    echo -e "${RED}DIFFERENCES FOUND${NC}"
    echo "$diff_output" | tee "$WORK_DIR/diff.txt"
    echo ""
    echo "Full diff saved to: $WORK_DIR/diff.txt"
    exit 1
fi
=== Comparing pre-signature contents ===
DIFFERENCES FOUND
diff -r /home/azad/Repos/BULL/reproducibility/reproducibility_test_1775908040/decoded1/apktool.yml /home/azad/Repos/BULL/reproducibility/reproducibility_test_1775908040/decoded2/apktool.yml
2c2
< apkFileName: build1.apk
---
> apkFileName: build2.apk
Binary files /home/azad/Repos/BULL/reproducibility/reproducibility_test_1775908040/decoded1/lib/arm64-v8a/libbdk_dart_ffi.so and /home/azad/Repos/BULL/reproducibility/reproducibility_test_1775908040/decoded2/lib/arm64-v8a/libbdk_dart_ffi.so differ
Binary files /home/azad/Repos/BULL/reproducibility/reproducibility_test_1775908040/decoded1/lib/armeabi-v7a/libbdk_dart_ffi.so and /home/azad/Repos/BULL/reproducibility/reproducibility_test_1775908040/decoded2/lib/armeabi-v7a/libbdk_dart_ffi.so differ
Binary files /home/azad/Repos/BULL/reproducibility/reproducibility_test_1775908040/decoded1/lib/x86_64/libbdk_dart_ffi.so and /home/azad/Repos/BULL/reproducibility/reproducibility_test_1775908040/decoded2/lib/x86_64/libbdk_dart_ffi.so differ

Full diff saved to: /home/azad/Repos/BULL/reproducibility/reproducibility_test_1775908040/diff.txt

real	123m58.905s
user	361m27.039s
sys	61m36.274s
< apkFileName: build1.apk
---
> apkFileName: build2.apk
Binary files /home/azad/Repos/BULL/reproducibility/reproducibility_test_1775908040/decoded1/lib/arm64-v8a/libbdk_dart_ffi.so and /home/azad/Repos/BULL/reproducibility/reproducibility_test_1775908040/decoded2/lib/arm64-v8a/libbdk_dart_ffi.so differ
Binary files /home/azad/Repos/BULL/reproducibility/reproducibility_test_1775908040/decoded1/lib/armeabi-v7a/libbdk_dart_ffi.so and /home/azad/Repos/BULL/reproducibility/reproducibility_test_1775908040/decoded2/lib/armeabi-v7a/libbdk_dart_ffi.so differ
Binary files /home/azad/Repos/BULL/reproducibility/reproducibility_test_1775908040/decoded1/lib/x86_64/libbdk_dart_ffi.so and /home/azad/Repos/BULL/reproducibility/reproducibility_test_1775908040/decoded2/lib/x86_64/libbdk_dart_ffi.so diffe

There is one last remaining fix which has to be found for native assets since they are the only two diffs between two builds
cc @i5hi and @basantagoswami

@ethicnology
ethicnology force-pushed the repro-from-devcontainers branch from 4c03a15 to 29c7172 Compare April 13, 2026 08:30
@ethicnology
ethicnology force-pushed the repro-from-devcontainers branch from 78fe546 to dbc6d2a Compare April 14, 2026 03:45
- Remove flutter_dotenv dependency and .env asset bundling
- Hardcode API URLs, auth credentials, and Sentry DSN in constants.dart
- Remove fake keystore generation from Dockerfile.apk
- Build unsigned APK when no key.properties is present
- Extract APK to host at end of make apk
- Use Platform.environment in integration test instead of dotenv
@ethicnology
ethicnology force-pushed the repro-from-devcontainers branch from 5e0505b to de44c1b Compare April 14, 2026 07:32

@i5hi i5hi left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

LFG

@i5hi
i5hi merged commit 8f0ff40 into develop Apr 14, 2026
1 check passed
@thibistaken
thibistaken deleted the repro-from-devcontainers branch May 13, 2026 13:39
@benalleng benalleng mentioned this pull request May 26, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants