#!/usr/bin/env bash set -euo pipefail ROOT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)" WORKSPACE_MANIFEST="$ROOT_DIR/Cargo.toml" CRATE_MANIFEST="$ROOT_DIR/traffic_core/Cargo.toml" TARGET_BIN="$ROOT_DIR/target/release/awg2-harness" REBOOT_BIN="$ROOT_DIR/target/release/awg2-reboot-scheduler" RUST_TOOLCHAIN_FILE="$ROOT_DIR/rust-toolchain.toml" RUST_TOOLCHAIN_VERSION="1.95.0" PREPARE_BUILD_ENV=false PREPARE_RUNTIME_ENV=false BOOTSTRAP_RUST=false DO_BUILD=false PACKAGE=false INSTALL=false ENABLE=false YES=false SKIP_FMT=false HAS_ACTION=false DIST_DIR="$ROOT_DIR/dist" PACKAGE_VERSION="" RUN_AS_ROOT=() BUILD_PACKAGES=( ca-certificates curl build-essential pkg-config file shellcheck dpkg-dev ) RUNTIME_PACKAGES=( iproute2 wireguard-tools nftables kmod util-linux procps systemd ) usage() { cat <<'EOF' Usage: ./tools/build.sh ./tools/build.sh --prepare-build-env --yes ./tools/build.sh --prepare-runtime-env --yes ./tools/build.sh --bootstrap-rust --yes ./tools/build.sh --build ./tools/build.sh --package ./tools/build.sh --build --package ./tools/build.sh --install ./tools/build.sh --enable Flags: --prepare-build-env Install missing build dependencies via apt --prepare-runtime-env Install missing runtime dependencies via apt --bootstrap-rust Install rustup/toolchain pinned by this repo --build Run build gate (default when no action flags) --package Build Debian package into dist/ (runs build gate by default) --dist-dir Output directory for package artifacts (default: ./dist) --version Override package version (default: read from Cargo.toml) --install Install binary and systemd unit (no enable/start) --enable Enable and start awg2-harness.service --yes Confirm system mutations (apt/rustup) --skip-fmt Skip cargo fmt --check --help Show this help Notes: --install is not the production deployment path; it is for manual source-tree installs (staging/debug). Use generated .deb packages on production hosts: apt install ./awg2-harness__amd64.deb EOF } while [[ $# -gt 0 ]]; do case "$1" in --prepare-build-env) PREPARE_BUILD_ENV=true; HAS_ACTION=true ;; --prepare-runtime-env) PREPARE_RUNTIME_ENV=true; HAS_ACTION=true ;; --bootstrap-rust) BOOTSTRAP_RUST=true; HAS_ACTION=true ;; --build) DO_BUILD=true; HAS_ACTION=true ;; --package) PACKAGE=true; HAS_ACTION=true ;; --dist-dir) [[ $# -ge 2 ]] || { echo "--dist-dir requires value"; exit 1; } DIST_DIR="$2" shift ;; --version) [[ $# -ge 2 ]] || { echo "--version requires value"; exit 1; } PACKAGE_VERSION="$2" shift ;; --install) INSTALL=true; HAS_ACTION=true ;; --enable) ENABLE=true; HAS_ACTION=true ;; --yes) YES=true ;; --skip-fmt) SKIP_FMT=true ;; --help|-h) usage; exit 0 ;; *) echo "Unknown arg: $1"; usage; exit 1 ;; esac shift done if [[ "$HAS_ACTION" == false ]]; then DO_BUILD=true fi require_cmd() { command -v "$1" >/dev/null 2>&1 || { echo "Missing command: $1" exit 1 } } setup_root_runner() { if [[ "$(id -u)" -eq 0 ]]; then RUN_AS_ROOT=() else require_cmd sudo RUN_AS_ROOT=(sudo) fi } ensure_supported_host() { if [[ -f /etc/os-release ]]; then # shellcheck disable=SC1091 source /etc/os-release case "${ID:-}" in debian|ubuntu) ;; *) echo "Unsupported OS: ${ID:-unknown}. Only Debian/Ubuntu are supported."; exit 1 ;; esac fi local arch arch="$(uname -m)" if [[ "$arch" != "x86_64" && "$arch" != "amd64" ]]; then echo "Unsupported arch: $arch. Only amd64/x86_64 is supported." exit 1 fi } apt_install_missing() { local packages=("$@") local missing=() require_cmd dpkg-query require_cmd apt-get for pkg in "${packages[@]}"; do if ! dpkg-query -W -f='${Status}' "$pkg" 2>/dev/null | grep -q "install ok installed"; then missing+=("$pkg") fi done if [[ "${#missing[@]}" -eq 0 ]]; then echo "All required packages already installed" return 0 fi echo "Missing packages: ${missing[*]}" if [[ "$YES" != true ]]; then echo "Re-run with --yes to install them" exit 1 fi setup_root_runner "${RUN_AS_ROOT[@]}" apt-get update "${RUN_AS_ROOT[@]}" apt-get install -y --no-install-recommends "${missing[@]}" } bootstrap_rust() { require_cmd curl if [[ "$YES" != true ]]; then echo "This will install rustup and Rust toolchain $RUST_TOOLCHAIN_VERSION" echo "Re-run with --yes to proceed" exit 1 fi if ! command -v rustup >/dev/null 2>&1; then local rustup_script rustup_script="$(mktemp)" curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs -o "$rustup_script" sh "$rustup_script" -y --profile minimal --default-toolchain "$RUST_TOOLCHAIN_VERSION" rm -f "$rustup_script" fi export PATH="$HOME/.cargo/bin:$PATH" require_cmd rustup rustup toolchain install "$RUST_TOOLCHAIN_VERSION" --profile minimal --component rustfmt --component clippy rustup component add rustfmt clippy --toolchain "$RUST_TOOLCHAIN_VERSION" rustc --version } prepare_build_gate() { require_cmd cargo require_cmd rustc require_cmd gcc require_cmd pkg-config require_cmd file require_cmd ldd if [[ ! -f "$RUST_TOOLCHAIN_FILE" ]]; then echo "Missing rust toolchain pin file: $RUST_TOOLCHAIN_FILE" exit 1 fi if [[ ! -f "$ROOT_DIR/Cargo.lock" ]]; then echo "Missing lockfile: $ROOT_DIR/Cargo.lock" exit 1 fi if [[ "$SKIP_FMT" != true ]]; then cargo fmt --manifest-path "$WORKSPACE_MANIFEST" --all --check fi if [[ ! -f "$WORKSPACE_MANIFEST" ]]; then echo "Missing workspace manifest: $WORKSPACE_MANIFEST" exit 1 fi cargo clippy --locked --workspace --all-targets --manifest-path "$WORKSPACE_MANIFEST" -- -D warnings cargo test --locked --workspace --release --manifest-path "$WORKSPACE_MANIFEST" cargo build --locked --workspace --release --manifest-path "$WORKSPACE_MANIFEST" cargo metadata --locked --format-version 1 --manifest-path "$WORKSPACE_MANIFEST" >/dev/null if command -v systemd-analyze >/dev/null 2>&1; then local docker_unit_present=false if [[ -e /lib/systemd/system/docker.service || -e /usr/lib/systemd/system/docker.service || -e /etc/systemd/system/docker.service ]]; then docker_unit_present=true fi if [[ -x /usr/sbin/awg2-harness && -x /usr/sbin/awg2-reboot-scheduler && "$docker_unit_present" == true ]]; then systemd-analyze verify "$ROOT_DIR/awg2-harness.service" systemd-analyze verify "$ROOT_DIR/awg2-reboot-scheduler.service" else echo "Skipping source-tree systemd-analyze verify: package runtime files or docker.service are not present on this build host" fi fi file "$TARGET_BIN" file "$REBOOT_BIN" ldd "$TARGET_BIN" || true ldd "$REBOOT_BIN" || true "$TARGET_BIN" --help >/dev/null "$REBOOT_BIN" --help >/dev/null sha256sum "$TARGET_BIN" sha256sum "$REBOOT_BIN" ls -lh "$TARGET_BIN" ls -lh "$REBOOT_BIN" } install_artifacts() { require_cmd install require_cmd systemctl if [[ ! -f "$TARGET_BIN" || ! -f "$REBOOT_BIN" ]]; then echo "Binary not found: $TARGET_BIN or $REBOOT_BIN" echo "Run build first: ./tools/build.sh" exit 1 fi if [[ ! -f /etc/awg2-harness/config.toml ]]; then echo "Missing /etc/awg2-harness/config.toml" echo "Refusing to install example config as production config" echo "Create config manually from $ROOT_DIR/config.example.toml and re-run with --install" exit 1 fi setup_root_runner "${RUN_AS_ROOT[@]}" install -d -m 0755 /usr/sbin "${RUN_AS_ROOT[@]}" install -m 0755 "$TARGET_BIN" /usr/sbin/awg2-harness "${RUN_AS_ROOT[@]}" install -m 0755 "$REBOOT_BIN" /usr/sbin/awg2-reboot-scheduler "${RUN_AS_ROOT[@]}" install -d -m 0755 /lib/systemd/system "${RUN_AS_ROOT[@]}" install -m 0644 "$ROOT_DIR/awg2-harness.service" /lib/systemd/system/awg2-harness.service "${RUN_AS_ROOT[@]}" install -m 0644 "$ROOT_DIR/awg2-reboot-scheduler.service" /lib/systemd/system/awg2-reboot-scheduler.service if command -v systemd-analyze >/dev/null 2>&1; then "${RUN_AS_ROOT[@]}" systemd-analyze verify /lib/systemd/system/awg2-harness.service "${RUN_AS_ROOT[@]}" systemd-analyze verify /lib/systemd/system/awg2-reboot-scheduler.service fi "${RUN_AS_ROOT[@]}" systemctl daemon-reload } read_package_version_from_manifest() { awk -F '"' '/^version\s*=\s*"/ { print $2; exit }' "$CRATE_MANIFEST" } package_deb() { require_cmd dpkg-deb require_cmd install require_cmd sha256sum require_cmd file require_cmd ldd require_cmd mktemp if [[ "$DO_BUILD" != true ]]; then prepare_build_gate fi if [[ ! -f "$TARGET_BIN" || ! -f "$REBOOT_BIN" ]]; then echo "Binary not found after build: $TARGET_BIN or $REBOOT_BIN" exit 1 fi local version if [[ -n "$PACKAGE_VERSION" ]]; then version="$PACKAGE_VERSION" else version="$(read_package_version_from_manifest)" fi if [[ -z "$version" ]]; then echo "Unable to determine package version" exit 1 fi local staging staging="$(mktemp -d)" trap 'rm -rf "$staging"' RETURN install -d -m 0755 "$staging/DEBIAN" install -d -m 0755 "$staging/usr/sbin" install -d -m 0755 "$staging/lib/systemd/system" install -d -m 0755 "$staging/etc/awg2-harness" install -d -m 0755 "$staging/usr/share/doc/awg2-harness/repo" install -d -m 0755 "$staging/usr/share/doc/awg2-harness/repo/docs" install -d -m 0755 "$staging/usr/share/doc/awg2-harness/repo/tools" install -m 0755 "$TARGET_BIN" "$staging/usr/sbin/awg2-harness" install -m 0755 "$REBOOT_BIN" "$staging/usr/sbin/awg2-reboot-scheduler" install -m 0644 "$ROOT_DIR/awg2-harness.service" "$staging/lib/systemd/system/awg2-harness.service" install -m 0644 "$ROOT_DIR/awg2-reboot-scheduler.service" "$staging/lib/systemd/system/awg2-reboot-scheduler.service" install -m 0644 "$ROOT_DIR/config.example.toml" "$staging/usr/share/doc/awg2-harness/repo/config.example.toml" install -m 0644 "$ROOT_DIR/README.md" "$staging/usr/share/doc/awg2-harness/repo/README.md" install -m 0644 "$ROOT_DIR/docs/INSTALLATION.md" "$staging/usr/share/doc/awg2-harness/repo/docs/INSTALLATION.md" install -m 0644 "$ROOT_DIR/docs/CONFIGURATION.md" "$staging/usr/share/doc/awg2-harness/repo/docs/CONFIGURATION.md" install -m 0644 "$ROOT_DIR/docs/ACCEPTANCE_CHECKLIST.md" "$staging/usr/share/doc/awg2-harness/repo/docs/ACCEPTANCE_CHECKLIST.md" install -m 0644 "$ROOT_DIR/tools/README.md" "$staging/usr/share/doc/awg2-harness/repo/tools/README.md" cat >"$staging/DEBIAN/control" <"$staging/DEBIAN/postinst" <<'EOF' #!/bin/sh set -e if command -v systemctl >/dev/null 2>&1; then systemctl daemon-reload || true fi exit 0 EOF cat >"$staging/DEBIAN/postrm" <<'EOF' #!/bin/sh set -e if command -v systemctl >/dev/null 2>&1; then systemctl daemon-reload || true fi exit 0 EOF chmod 0755 "$staging/DEBIAN/postinst" "$staging/DEBIAN/postrm" mkdir -p "$DIST_DIR" local pkg_name pkg_name="awg2-harness_${version}_amd64.deb" local pkg_path pkg_path="$DIST_DIR/$pkg_name" dpkg-deb --build "$staging" "$pkg_path" dpkg-deb --info "$pkg_path" dpkg-deb --contents "$pkg_path" | sort ( cd "$DIST_DIR" sha256sum "$pkg_name" > SHA256SUMS ) echo "Package created: $pkg_path" echo "Checksums: $DIST_DIR/SHA256SUMS" } enable_service() { require_cmd systemctl setup_root_runner "${RUN_AS_ROOT[@]}" systemctl enable --now awg2-harness.service "${RUN_AS_ROOT[@]}" systemctl enable --now awg2-reboot-scheduler.service } ensure_supported_host if [[ "$PREPARE_BUILD_ENV" == true ]]; then apt_install_missing "${BUILD_PACKAGES[@]}" fi if [[ "$PREPARE_RUNTIME_ENV" == true ]]; then apt_install_missing "${RUNTIME_PACKAGES[@]}" fi if [[ "$BOOTSTRAP_RUST" == true ]]; then bootstrap_rust fi if [[ "$DO_BUILD" == true ]]; then prepare_build_gate fi if [[ "$PACKAGE" == true ]]; then package_deb fi if [[ "$INSTALL" == true ]]; then install_artifacts fi if [[ "$ENABLE" == true ]]; then enable_service fi if [[ "$DO_BUILD" == true ]]; then echo "Build completed: $TARGET_BIN" fi