Files
n_signer/build_static.sh
T

247 lines
7.5 KiB
Bash
Executable File

#!/bin/bash
# Build fully static MUSL binary for nsigner using Alpine Docker
#
# Speed optimization: if nothing changed since the last successful build,
# skip the Docker build entirely and reuse the existing binaries.
set -euo pipefail
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
BUILD_DIR="$SCRIPT_DIR/build"
DOCKERFILE="$SCRIPT_DIR/Dockerfile.alpine-musl"
HASH_FILE="$BUILD_DIR/.nsigner_build_hash"
TARGET_ARCH=""
FORCE=0
while [[ $# -gt 0 ]]; do
case "$1" in
--arch)
if [[ -z "${2:-}" ]]; then
echo "ERROR: --arch requires a value"
echo "Usage: $0 [--arch <x86_64|arm64|armv7>] [--force]"
exit 1
fi
case "$2" in
x86_64|arm64|armv7)
TARGET_ARCH="$2"
;;
*)
echo "ERROR: Unsupported architecture '$2'"
echo "Supported values: x86_64, arm64, armv7"
exit 1
;;
esac
shift 2
;;
--force)
FORCE=1
shift
;;
*)
echo "ERROR: Unknown argument '$1'"
echo "Usage: $0 [--arch <x86_64|arm64|armv7>] [--force]"
exit 1
;;
esac
done
if ! command -v docker >/dev/null 2>&1; then
echo "ERROR: Docker is not installed or not in PATH"
exit 1
fi
if ! docker info >/dev/null 2>&1; then
echo "ERROR: Docker daemon is not running or user lacks permissions"
exit 1
fi
HOST_UNAME="$(uname -m)"
case "$HOST_UNAME" in
x86_64)
HOST_ARCH="x86_64"
;;
aarch64|arm64)
HOST_ARCH="arm64"
;;
armv7l|armv7)
HOST_ARCH="armv7"
;;
*)
HOST_ARCH="x86_64"
;;
esac
ARCH="${TARGET_ARCH:-$HOST_ARCH}"
case "$ARCH" in
x86_64)
PLATFORM="linux/amd64"
OUTPUT_NAME="nsigner_static_x86_64"
CLIENT_NAME="nsigner_client_static_x86_64"
;;
arm64)
PLATFORM="linux/arm64"
OUTPUT_NAME="nsigner_static_arm64"
CLIENT_NAME="nsigner_client_static_arm64"
;;
armv7)
PLATFORM="linux/v7"
OUTPUT_NAME="nsigner_static_armv7"
CLIENT_NAME="nsigner_client_static_armv7"
;;
*)
echo "ERROR: Unsupported target architecture '$ARCH'"
exit 1
;;
esac
mkdir -p "$BUILD_DIR"
IMAGE_TAG="nsigner-static-builder:${ARCH}"
CONTAINER_NAME=""
cleanup() {
if [[ -n "$CONTAINER_NAME" ]]; then
docker rm -f "$CONTAINER_NAME" >/dev/null 2>&1 || true
fi
}
trap cleanup EXIT
echo "=========================================="
echo "nsigner static build"
echo "=========================================="
echo "Project root: $SCRIPT_DIR"
echo "Dockerfile: $DOCKERFILE"
echo "Platform: $PLATFORM"
echo "Output: $BUILD_DIR/$OUTPUT_NAME"
echo "Client: $BUILD_DIR/$CLIENT_NAME"
echo ""
# ---- Change detection ----
# Compute a hash of all files that feed into the Docker build.
# If the hash matches the last successful build and the output binaries
# exist, skip the Docker build entirely.
compute_source_hash() {
{
# Dockerfile itself
cat "$DOCKERFILE"
# .dockerignore
cat "$SCRIPT_DIR/.dockerignore" 2>/dev/null || true
# All source files
find "$SCRIPT_DIR/src" "$SCRIPT_DIR/client" "$SCRIPT_DIR/libotppad" \
"$SCRIPT_DIR/resources/tui_continuous" "$SCRIPT_DIR/resources/pqclean" \
-type f -not -path '*/.git/*' 2>/dev/null | sort | xargs cat 2>/dev/null
# nostr_core_lib source (exclude .git, backups, bare repos, examples, tests)
find "$SCRIPT_DIR/resources/nostr_core_lib" \
-type f \
-not -path '*/.git/*' \
-not -path '*/rewrite_mirror/*' \
-not -path '*/verify_remote_size*' \
-not -path '*/backups/*' \
-not -path '*/examples/*' \
-not -path '*/tests/*' \
-not -path '*/Trash/*' \
-not -path '*/node_modules/*' \
-not -path '*/nips/*' \
-not -path '*/nak/*' \
-not -path '*/nostr-tools/*' \
-not -path '*/libsodium/*' \
-not -path '*/monocypher*' \
-not -path '*/tiny-AES-c/*' \
-not -path '*/blossom/*' \
-not -path '*/ndk/*' \
-not -path '*/cline_history/*' \
2>/dev/null | sort | xargs cat 2>/dev/null
} | sha256sum | awk '{print $1}'
}
CURRENT_HASH="$(compute_source_hash)"
OUTPUT_PATH="$BUILD_DIR/$OUTPUT_NAME"
CLIENT_PATH="$BUILD_DIR/$CLIENT_NAME"
if [[ "$FORCE" -eq 0 ]] && \
[[ -f "$OUTPUT_PATH" ]] && \
[[ -f "$CLIENT_PATH" ]] && \
[[ -f "$HASH_FILE" ]] && \
[[ "$(cat "$HASH_FILE" 2>/dev/null)" == "$CURRENT_HASH" ]]; then
echo "No changes detected since last successful build."
echo "Skipping Docker build. Existing binaries:"
echo " $OUTPUT_PATH"
echo " $CLIENT_PATH"
echo ""
echo "Use --force to rebuild anyway."
exit 0
fi
if [ "$ARCH" != "$HOST_ARCH" ]; then
echo "[0/3] Preparing buildx + QEMU for cross-architecture build"
if ! docker buildx inspect >/dev/null 2>&1; then
echo "ERROR: docker buildx is not available"
exit 1
fi
docker run --privileged --rm tonistiigi/binfmt --install all >/dev/null
if ! docker buildx inspect nsigner-builder >/dev/null 2>&1; then
docker buildx create --name nsigner-builder --driver docker-container --use >/dev/null
else
docker buildx use nsigner-builder >/dev/null
fi
docker buildx inspect --bootstrap >/dev/null
fi
echo "[1/3] Building builder stage from project root context"
# Note: we no longer docker rmi before building. The buildx cache handles
# layer reuse, and the prune at the end prevents dangling images. Removing
# the image here forced a full --load re-export (~370MB) every time even
# when all layers were cache hits.
docker buildx build \
--platform "$PLATFORM" \
--target builder \
-f "$DOCKERFILE" \
-t "$IMAGE_TAG" \
--load \
"$SCRIPT_DIR"
echo "[2/3] Extracting static binaries"
CONTAINER_NAME="$(docker create "$IMAGE_TAG")"
docker cp "$CONTAINER_NAME:/build/nsigner_static" "$BUILD_DIR/$OUTPUT_NAME"
chmod +x "$BUILD_DIR/$OUTPUT_NAME"
strip "$BUILD_DIR/$OUTPUT_NAME" >/dev/null 2>&1 || true
docker cp "$CONTAINER_NAME:/build/nsigner_client_static" "$BUILD_DIR/$CLIENT_NAME"
chmod +x "$BUILD_DIR/$CLIENT_NAME"
strip "$BUILD_DIR/$CLIENT_NAME" >/dev/null 2>&1 || true
echo "[3/3] Verifying static binaries"
file "$BUILD_DIR/$OUTPUT_NAME"
file "$BUILD_DIR/$CLIENT_NAME"
LDD_OUTPUT="$(ldd "$BUILD_DIR/$OUTPUT_NAME" 2>&1 || true)"
echo "$LDD_OUTPUT"
if echo "$LDD_OUTPUT" | grep -Eq "not a dynamic executable|statically linked"; then
echo "nsigner static check: PASS"
else
echo "nsigner static check: WARNING (verify manually)"
fi
LDD_OUTPUT_CLIENT="$(ldd "$BUILD_DIR/$CLIENT_NAME" 2>&1 || true)"
echo "$LDD_OUTPUT_CLIENT"
if echo "$LDD_OUTPUT_CLIENT" | grep -Eq "not a dynamic executable|statically linked"; then
echo "nsigner_client static check: PASS"
else
echo "nsigner_client static check: WARNING (verify manually)"
fi
echo ""
echo "Build complete:"
echo " $BUILD_DIR/$OUTPUT_NAME"
echo " $BUILD_DIR/$CLIENT_NAME"
# Record the source hash so the next run can skip if nothing changed.
echo "$CURRENT_HASH" > "$HASH_FILE"
# Prune stale build cache older than 24h to prevent unbounded cache growth
# from repeated buildx builds. Recent layers are kept for fast rebuilds.
docker builder prune -af --filter "until=24h" >/dev/null 2>&1 || true