#!/usr/bin/env bash set -euo pipefail REPO_URL="${REPO_URL:-https://github.com/pacificao/agrarian.git}" WORKDIR="${WORKDIR:-$HOME/agrarian}" HOST_WIN64="${HOST_WIN64:-x86_64-w64-mingw32}" HOST_ARM64="${HOST_ARM64:-aarch64-linux-gnu}" MENU_CHOICE="${AGRARIAN_MENU_CHOICE:-}" ROOT="" DAEMON_WAS_RUNNING=0 DAEMON_RESTART_MODE="" DAEMON_RESTART_CMD=() DAEMON_SYSTEMD_SERVICE="" detect_script_branch() { local script_dir script_dir="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" git -C "$script_dir" rev-parse --abbrev-ref HEAD 2>/dev/null || true } if [[ -z "${BRANCH:-}" ]]; then BRANCH="$(detect_script_branch)" fi BRANCH="${BRANCH:-main}" detect_build_jobs() { if command -v nproc >/dev/null 2>&1; then nproc elif command -v getconf >/dev/null 2>&1; then getconf _NPROCESSORS_ONLN else echo 1 fi } JOBS="${JOBS:-$(detect_build_jobs)}" if [[ "${EUID:-$(id -u)}" -eq 0 && "${ALLOW_ROOT_BUILD_MENU:-0}" != "1" ]]; then cat >&2 </dev/null 2>&1 } progress() { local percent="$1" local message="$2" local width=30 local filled=$((percent * width / 100)) local empty=$((width - filled)) local bar bar="$(printf '%*s' "$filled" '' | tr ' ' '#')" bar+="$(printf '%*s' "$empty" '' | tr ' ' '-')" printf '\n[%s] %3d%% %s\n' "$bar" "$percent" "$message" } fail() { echo echo "ERROR: $*" >&2 exit 1 } run_step() { local percent="$1" local message="$2" shift 2 progress "$percent" "$message" "$@" } prompt() { local question="$1" local default="${2:-}" local answer read -r -p "$question${default:+ [$default]}: " answer echo "${answer:-$default}" } confirm() { local question="$1" local answer read -r -p "$question [y/N]: " answer [[ "$answer" == "y" || "$answer" == "Y" || "$answer" == "yes" || "$answer" == "YES" ]] } process_pids() { local name="$1" pgrep -u "$(id -u)" -x "$name" 2>/dev/null || true } all_process_pids() { local name="$1" pgrep -x "$name" 2>/dev/null || true } other_user_process_pids() { local name="$1" local pid owner current_user current_user="$(id -un)" while IFS= read -r pid; do [[ -n "$pid" ]] || continue owner="$(ps -o user= -p "$pid" 2>/dev/null | awk '{print $1}')" if [[ -n "$owner" && "$owner" != "$current_user" ]]; then printf '%s %s\n' "$pid" "$owner" fi done < <(all_process_pids "$name") return 0 } process_cmdline() { local pid="$1" tr '\0' '\n' < "/proc/$pid/cmdline" 2>/dev/null || true } daemon_cli_path() { if [[ -x "$ROOT/src/agrarian-cli" ]]; then echo "$ROOT/src/agrarian-cli" elif has_cmd agrarian-cli; then command -v agrarian-cli else return 1 fi } daemon_args_from_cmdline() { local pid="$1" local arg while IFS= read -r arg; do case "$arg" in -conf=*|-datadir=*|-testnet|-regtest) printf '%s\n' "$arg" ;; esac done < <(process_cmdline "$pid") } remember_daemon_restart() { local pid="$1" local arg exe DAEMON_RESTART_MODE="command" DAEMON_RESTART_CMD=() exe="$(readlink -f "/proc/$pid/exe" 2>/dev/null || true)" [[ -n "$exe" ]] || exe="$ROOT/src/agrariand" DAEMON_RESTART_CMD+=("$exe") while IFS= read -r arg; do DAEMON_RESTART_CMD+=("$arg") done < <(daemon_args_from_cmdline "$pid") case " ${DAEMON_RESTART_CMD[*]} " in *" -daemon "*) ;; *) DAEMON_RESTART_CMD+=("-daemon") ;; esac } active_user_daemon_service() { has_cmd systemctl || return 1 systemctl --user is-active --quiet agrariand.service 2>/dev/null || return 1 echo "agrariand.service" } stop_running_daemon() { local pids pid cli args=() service pids="$(process_pids agrariand)" [[ -n "$pids" ]] || return 0 DAEMON_WAS_RUNNING=1 service="$(active_user_daemon_service || true)" if [[ -n "$service" ]]; then DAEMON_RESTART_MODE="systemd" DAEMON_SYSTEMD_SERVICE="$service" echo "Detected running user service: $service" else pid="$(printf '%s\n' "$pids" | head -n 1)" remember_daemon_restart "$pid" echo "Detected running agrariand process: $pid" fi if ! confirm "Stop agrariand gracefully before building?"; then fail "Refusing to build while agrariand is running." fi if [[ "$DAEMON_RESTART_MODE" == "systemd" ]]; then systemctl --user stop "$DAEMON_SYSTEMD_SERVICE" else pid="$(printf '%s\n' "$pids" | head -n 1)" cli="$(daemon_cli_path)" || fail "agrarian-cli is required to stop the running daemon gracefully." mapfile -t args < <(daemon_args_from_cmdline "$pid") "$cli" "${args[@]}" stop fi local waited=0 while [[ -n "$(process_pids agrariand)" && "$waited" -lt 120 ]]; do sleep 1 waited=$((waited + 1)) done [[ -z "$(process_pids agrariand)" ]] || fail "agrariand did not stop within 120 seconds. Build was not started." } stop_running_qt_wallet() { local pids pids="$(process_pids agrarian-qt)" [[ -n "$pids" ]] || return 0 cat >&2 <&1 1>&2 2>&3)" || exit 0 else echo "Agrarian Build Menu" echo "1) Compile Linux daemon and CLI tools" echo "2) Compile Linux Qt GUI wallet" echo "3) Cross-compile Windows daemon and CLI tools" echo "4) Cross-compile Windows Qt GUI wallet" echo "5) Compile Linux ARM64 daemon and CLI tools" echo "6) Compile Linux ARM64 Qt GUI wallet" local choice read -r -p "Selection [1-6]: " choice case "$choice" in 1) MENU_CHOICE="linux-daemon" ;; 2) MENU_CHOICE="linux-qt" ;; 3) MENU_CHOICE="windows-daemon" ;; 4) MENU_CHOICE="windows-qt" ;; 5) MENU_CHOICE="linux-arm64-daemon" ;; 6) MENU_CHOICE="linux-arm64-qt" ;; *) fail "Invalid selection: $choice" ;; esac fi } sudo_cmd() { if [[ "${EUID:-$(id -u)}" -eq 0 ]]; then "$@" elif has_cmd sudo; then sudo "$@" else fail "sudo is required to install missing packages. Install sudo or run as root." fi } apt_source_text() { grep -RhsE '^(deb |Types:|URIs:|Suites:|Components:)' \ /etc/apt/sources.list /etc/apt/sources.list.d/*.list /etc/apt/sources.list.d/*.sources \ 2>/dev/null || true } ubuntu_sources_need_repair() { [[ -r /etc/os-release ]] || return 1 # shellcheck disable=SC1091 . /etc/os-release [[ "${ID:-}" == "ubuntu" && -n "${VERSION_CODENAME:-}" ]] || return 1 local sources sources="$(apt_source_text)" [[ "$sources" == *"$VERSION_CODENAME-updates"* && "$sources" == *"$VERSION_CODENAME-security"* ]] && return 1 return 0 } repair_ubuntu_sources() { [[ -r /etc/os-release ]] || return 0 # shellcheck disable=SC1091 . /etc/os-release [[ "${ID:-}" == "ubuntu" && -n "${VERSION_CODENAME:-}" ]] || return 0 local arch uri security_uri source_file arch="$(dpkg --print-architecture 2>/dev/null || true)" case "$arch" in arm64|armhf|ppc64el|riscv64|s390x) uri="http://ports.ubuntu.com/ubuntu-ports" security_uri="http://ports.ubuntu.com/ubuntu-ports" ;; *) uri="http://archive.ubuntu.com/ubuntu" security_uri="http://security.ubuntu.com/ubuntu" ;; esac source_file="/etc/apt/sources.list.d/agrarian-ubuntu.sources" cat >&2 < "$temp_file" </dev/null; then backup="$file.agrarian-disabled" temp_file="$(mktemp)" if [[ "$file" == *.sources ]]; then cat > "$temp_file" < "$temp_file" fi sudo_cmd cp "$file" "$backup" sudo_cmd install -m 0644 "$temp_file" "$file" rm -f "$temp_file" echo "Disabled broken Ubuntu mirror in $file" fi done } repair_ubuntu_apt_on_failure() { local reason="$1" cat >&2 <&2 </dev/null || true)" if [[ -n "$remote_head" && "$head" != "$remote_head" ]]; then fail "Checkout is at $head but origin/$BRANCH is $remote_head. Refusing to build an outdated checkout." fi echo "Using Agrarian $BRANCH at commit $head" cd "$ROOT" } reexec_from_checkout() { [[ "${AGRARIAN_REEXECED:-0}" == "1" ]] && return 0 local repo_script repo_script="$ROOT/contrib/agrarian-build-menu.sh" [[ -x "$repo_script" ]] || return 0 repo_script="$(cd "$(dirname "$repo_script")" && pwd -P)/$(basename "$repo_script")" echo echo "Restarting with the updated build menu from the checkout..." exec env \ AGRARIAN_REEXECED=1 \ AGRARIAN_MENU_CHOICE="$MENU_CHOICE" \ AGRARIAN_PROMPTS_DONE=1 \ BRANCH="$BRANCH" \ WORKDIR="$WORKDIR" \ JOBS="$JOBS" \ HOST_WIN64="$HOST_WIN64" \ HOST_ARM64="$HOST_ARM64" \ "$repo_script" } detect_native_host() { "$ROOT/depends/config.guess" } ensure_posix_mingw() { local gcc_path="/usr/bin/$HOST_WIN64-gcc-posix" local gxx_path="/usr/bin/$HOST_WIN64-g++-posix" if [[ -x "$gcc_path" && -x "$gxx_path" ]]; then sudo_cmd update-alternatives --set "$HOST_WIN64-gcc" "$gcc_path" >/dev/null || true sudo_cmd update-alternatives --set "$HOST_WIN64-g++" "$gxx_path" >/dev/null || true fi has_cmd "$HOST_WIN64-g++" || fail "Missing $HOST_WIN64-g++ after package install." "$HOST_WIN64-g++" --version | head -n 1 | grep -qi posix || fail "$HOST_WIN64-g++ is not using the POSIX thread model." } build_windows_daemon() { ensure_posix_mingw run_step 45 "Building Windows daemon depends" make -C depends HOST="$HOST_WIN64" NO_QT=1 -j1 if [[ ! -f configure || ! -f src/secp256k1/configure || ! -f src/secp256k1/Makefile.in ]]; then run_step 60 "Generating configure script" ./autogen.sh fi run_step 72 "Configuring Windows daemon build" env CONFIG_SITE="$ROOT/depends/$HOST_WIN64/share/config.site" ./configure \ --prefix=/ \ --without-gui \ --disable-maintainer-mode \ --disable-tests \ --disable-bench \ --disable-zmq \ --with-miniupnpc=no run_step 82 "Cleaning stale target objects" make clean run_step 90 "Compiling Windows daemon and CLI tools" make -j"$JOBS" } ensure_arm64_toolchain() { if [[ "$(detect_native_host)" == aarch64-* ]]; then return 0 fi has_cmd aarch64-linux-gnu-g++ || fail "Missing aarch64-linux-gnu-g++. Install g++-aarch64-linux-gnu." has_cmd aarch64-linux-gnu-ar || fail "Missing aarch64-linux-gnu-ar. Install binutils-aarch64-linux-gnu." } build_linux_arm64_daemon() { local build_host build_host="$(detect_native_host)" if [[ "$build_host" == aarch64-* ]]; then run_step 45 "Compiling native Linux ARM64 daemon and CLI tools" env JOBS="$JOBS" ./contrib/build-linux.sh return 0 fi ensure_arm64_toolchain run_step 45 "Building Linux ARM64 daemon depends" make -C depends HOST="$HOST_ARM64" NO_QT=1 -j1 if [[ ! -f configure || ! -f src/secp256k1/configure || ! -f src/secp256k1/Makefile.in ]]; then run_step 60 "Generating configure script" ./autogen.sh fi run_step 72 "Configuring Linux ARM64 daemon build" env CONFIG_SITE="$ROOT/depends/$HOST_ARM64/share/config.site" ./configure \ --build="$build_host" \ --host="$HOST_ARM64" \ --prefix=/ \ --without-gui \ --disable-maintainer-mode \ --disable-tests \ --disable-bench \ --disable-zmq \ --with-miniupnpc=no run_step 82 "Cleaning stale target objects" make clean run_step 90 "Compiling Linux ARM64 daemon and CLI tools" make -j"$JOBS" } build_linux_arm64_qt() { local build_host build_host="$(detect_native_host)" if [[ "$build_host" != aarch64-* ]]; then fail "Linux ARM64 Qt wallet builds are native-only for now. Run this option on an ARM64 Ubuntu machine, or use the ARM64 daemon cross-build from this host." fi run_step 45 "Compiling native Linux ARM64 Qt GUI wallet" env JOBS="$JOBS" ./contrib/build-linux-wallet.sh } build_selected() { case "$MENU_CHOICE" in linux-daemon) run_step 45 "Compiling Linux daemon and CLI tools" env JOBS="$JOBS" ./contrib/build-linux.sh ;; linux-qt) run_step 45 "Compiling Linux Qt GUI wallet" env JOBS="$JOBS" ./contrib/build-linux-wallet.sh ;; windows-daemon) build_windows_daemon ;; windows-qt) run_step 45 "Compiling Windows Qt GUI wallet" env JOBS="$JOBS" ./contrib/build-win64-wallet.sh ;; linux-arm64-daemon) build_linux_arm64_daemon ;; linux-arm64-qt) build_linux_arm64_qt ;; *) fail "Unknown build choice: $MENU_CHOICE" ;; esac } install_user_daemon_service() { local bindir="$HOME/.local/bin" local confdir="$HOME/.agrarian" local systemd_dir="$HOME/.config/systemd/user" local service_file="$systemd_dir/agrariand.service" local conf_file="$confdir/agrarian.conf" local rpcpass [[ -x "$ROOT/src/agrariand" ]] || fail "Linux daemon binary not found at src/agrariand." [[ -x "$ROOT/src/agrarian-cli" ]] || fail "Linux CLI binary not found at src/agrarian-cli." has_cmd systemctl || fail "systemctl is required for user service setup." mkdir -p "$bindir" "$confdir" "$systemd_dir" install -m 0755 "$ROOT/src/agrariand" "$bindir/agrariand" install -m 0755 "$ROOT/src/agrarian-cli" "$bindir/agrarian-cli" if [[ ! -f "$conf_file" ]]; then if has_cmd openssl; then rpcpass="$(openssl rand -hex 32)" else rpcpass="$(date +%s)-$RANDOM-$RANDOM" fi cat > "$conf_file" < "$service_file" </dev/null 2>&1 || true fi echo echo "Agrarian daemon service is installed for the current user." echo "Config: $conf_file" echo "Service: $service_file" echo "Status: systemctl --user status agrariand" } show_completion() { progress 100 "Build finished" echo case "$MENU_CHOICE" in linux-daemon|linux-arm64-daemon) echo "Linux daemon binaries:" echo " $ROOT/src/agrariand" echo " $ROOT/src/agrarian-cli" echo " $ROOT/src/agrarian-tx" if [[ "$DAEMON_WAS_RUNNING" == "1" ]]; then echo "agrariand was running before the build and has been handled by the restart step." elif confirm "Start agrariand now and enable automatic start at boot for the current user?"; then install_user_daemon_service else echo "Start manually with: $ROOT/src/agrariand -daemon" fi ;; linux-qt|linux-arm64-qt) echo "Linux wallet binaries:" echo " $ROOT/src/qt/agrarian-qt" echo " $ROOT/src/agrariand" echo " $ROOT/src/agrarian-cli" if [[ "$DAEMON_WAS_RUNNING" == "1" ]]; then echo "agrariand was running before the build and has been handled by the restart step." elif confirm "Start agrariand now and enable automatic start at boot for the current user?"; then install_user_daemon_service fi ;; windows-daemon) echo "Windows daemon binaries:" echo " $ROOT/src/agrariand.exe" echo " $ROOT/src/agrarian-cli.exe" echo " $ROOT/src/agrarian-tx.exe" echo echo "Copy those .exe files to a folder on the Windows machine." echo "Start the daemon from PowerShell or Command Prompt:" echo " agrariand.exe" echo "Then use:" echo " agrarian-cli.exe help" ;; windows-qt) echo "Windows wallet binaries:" echo " $ROOT/src/qt/agrarian-qt.exe" echo " $ROOT/src/agrariand.exe" echo " $ROOT/src/agrarian-cli.exe" echo " $ROOT/src/agrarian-tx.exe" echo echo "Copy those .exe files to a folder on the Windows machine." echo "Launch the GUI wallet with:" echo " agrarian-qt.exe" ;; esac } main() { select_target progress 5 "Selected target: $MENU_CHOICE" run_step 10 "Installing bootstrap Ubuntu packages" install_bootstrap_packages ensure_repo reexec_from_checkout run_step 40 "Installing required Ubuntu packages" install_packages run_step 43 "Preparing running Agrarian processes" prepare_running_processes_for_build build_selected restart_previous_daemon show_completion } main "$@"