#!/usr/bin/env bash # Debian 13+ one-line bootstrap for first install or safe source update. # Builds as an unprivileged user, then delegates host changes to install.sh/update.sh. set -euo pipefail REPO_URL="${RUSTPROXY_REPO_URL:-https://git.linuxiarz.pl/gru/RustProxy.git}" ADMIN_LISTEN="${RUSTPROXY_ADMIN_LISTEN:-127.0.0.1:9090}" ALLOW_REMOTE_ADMIN="${RUSTPROXY_ALLOW_REMOTE_ADMIN:-0}" PREFIX="${RUSTPROXY_PREFIX:-/opt/rustproxy}" MODE="${1:-auto}" case "$MODE" in auto|install|update) ;; --install) MODE=install ;; --update) MODE=update ;; -h|--help) cat <<'USAGE' Usage: install-debian13.sh [auto|install|update] auto Install when RustProxy is absent; update when /opt/rustproxy/install.json exists (default) install Require a fresh installation update Require an existing managed installation and perform a safe update Environment: RUSTPROXY_REPO_URL Git repository URL RUSTPROXY_ADMIN_LISTEN Admin bind used only on first install (default 127.0.0.1:9090) RUSTPROXY_ALLOW_REMOTE_ADMIN Set to 1 only to permit plain-HTTP non-loopback admin on first install RUSTPROXY_PREFIX Managed install prefix (default /opt/rustproxy) RUSTPROXY_BUILD_JOBS Cargo jobs for low-memory builds (default 1) The bootstrap installs/updates the builder Rust toolchain only when required by Cargo.toml (and respects rust-toolchain.toml, if present). USAGE exit 0 ;; *) echo "Unknown mode: $MODE (expected auto, install or update)." >&2 exit 2 ;; esac if [[ ${EUID:-$(id -u)} -ne 0 ]]; then echo "Run this bootstrap as root (for example: wget -qO- ... | bash)." >&2 exit 1 fi . /etc/os-release if [[ ${ID:-} != debian ]] || [[ ${VERSION_ID%%.*} -lt 13 ]]; then echo "This bootstrap supports Debian 13+. Detected: ${PRETTY_NAME:-unknown}." >&2 exit 1 fi install_record="$PREFIX/install.json" if [[ "$MODE" == auto ]]; then if [[ -f "$install_record" ]]; then MODE=update elif [[ -e "$PREFIX/current" || -e /etc/systemd/system/rustproxy.service ]]; then echo "RustProxy artifacts exist but $install_record is missing; refusing automatic overwrite." >&2 echo "Recover the managed install record or use the project migration/recovery procedure." >&2 exit 1 else MODE=install fi fi if [[ "$MODE" == update && ! -f "$install_record" ]]; then echo "Update requested, but managed installation record is missing: $install_record" >&2 exit 1 fi if [[ "$MODE" == install && -e "$install_record" ]]; then echo "Install requested, but RustProxy is already installed. Use update mode." >&2 exit 1 fi echo "RustProxy bootstrap mode: $MODE" export DEBIAN_FRONTEND=noninteractive apt-get update apt-get install -y build-essential gcc git ca-certificates python3 python3-venv python3-pip pkg-config wget sudo logrotate created_builder=0 if [[ -n ${SUDO_USER:-} && ${SUDO_USER} != root ]] && id "${SUDO_USER}" >/dev/null 2>&1; then builder=${SUDO_USER} builder_home=$(getent passwd "$builder" | cut -d: -f6) else builder=rustproxy-build builder_home=/var/tmp/rustproxy-build if ! id "$builder" >/dev/null 2>&1; then useradd --create-home --home-dir "$builder_home" --shell /bin/bash "$builder" created_builder=1 fi fi work=$(mktemp -d /var/tmp/rustproxy-source.XXXXXX) chown "$builder":"$(id -gn "$builder")" "$work" cleanup() { rm -rf "$work" if [[ $created_builder -eq 1 ]]; then userdel -r "$builder" >/dev/null 2>&1 || true fi } trap cleanup EXIT run_builder() { runuser -u "$builder" -- env HOME="$builder_home" PATH="$builder_home/.cargo/bin:/usr/local/bin:/usr/bin:/bin" "$@" } run_builder git clone --depth 1 "$REPO_URL" "$work/RustProxy" # The builder, not root, owns the toolchain. Never update a system-wide # apt-managed compiler or change the user's default rustup toolchain. # rustup is needed even when an older system-installed cargo exists. if ! run_builder bash -c 'command -v rustup >/dev/null 2>&1'; then echo "Installing rustup for build user: $builder" run_builder env RUSTUP_INIT_SKIP_PATH_CHECK=yes bash -c \ 'set -o pipefail; wget -qO- --https-only --secure-protocol=TLSv1_2 https://sh.rustup.rs | sh -s -- -y --profile minimal --no-modify-path --default-toolchain none' fi # Read the actual requirements from the checked-out sources, not a hardcoded # Rust version. The project may inherit rust-version from [workspace.package]. # The channel from rust-toolchain.toml (or legacy rust-toolchain) takes precedence. rust_settings=$(python3 - "$work/RustProxy" <<'PY_RUST' import pathlib import re import sys import tomllib repo = pathlib.Path(sys.argv[1]) with (repo / "Cargo.toml").open("rb") as f: manifest = tomllib.load(f) minimum = manifest.get("package", {}).get("rust-version") if isinstance(minimum, dict) and minimum.get("workspace"): minimum = manifest.get("workspace", {}).get("package", {}).get("rust-version") if not isinstance(minimum, str) or not re.fullmatch(r"[0-9]+\.[0-9]+(?:\.[0-9]+)?", minimum): sys.exit("Cargo.toml must declare a valid package rust-version for safe automatic toolchain updates") channel = "stable" toolchain_toml = repo / "rust-toolchain.toml" toolchain_legacy = repo / "rust-toolchain" if toolchain_toml.exists(): with toolchain_toml.open("rb") as f: channel = tomllib.load(f).get("toolchain", {}).get("channel", "") elif toolchain_legacy.exists(): channel = toolchain_legacy.read_text(encoding="utf-8").strip() if not isinstance(channel, str) or not re.fullmatch( r"(?:stable|beta|nightly|[0-9]+\.[0-9]+(?:\.[0-9]+)?(?:-[A-Za-z0-9_\-]+)?)", channel ): sys.exit(f"Unsupported rust-toolchain channel: {channel!r}") print(f"{minimum}|{channel}") PY_RUST ) required_rust=${rust_settings%%|*} build_toolchain=${rust_settings#*|} # Compare numerically (1.100 > 1.99); do not use lexicographic ordering. rust_version_at_least() { python3 - "$1" "$2" <<'PY_VERSION' import sys def version(value): parts = value.split(".") if len(parts) not in (2, 3) or not all(part.isdecimal() for part in parts): sys.exit(2) return tuple(map(int, (parts + ["0"] * (3 - len(parts))))) sys.exit(0 if version(sys.argv[1]) >= version(sys.argv[2]) else 1) PY_VERSION } # rustup run is a non-updating check: it fails when the channel is missing. installed_rust=$(run_builder rustup run "$build_toolchain" rustc --version 2>/dev/null | awk '$1 == "rustc" { print $2; exit }' || true) if [[ -z "$installed_rust" ]] || ! rust_version_at_least "$installed_rust" "$required_rust"; then echo "Rust toolchain '$build_toolchain' needs installation/update (installed: ${installed_rust:-none}; required: >= $required_rust)." case "$build_toolchain" in stable|beta|nightly) run_builder rustup update "$build_toolchain" ;; *) # A fixed toolchain must be respected. Do not silently override a pin # that is older than the manifest's minimum supported Rust version. if [[ -n "$installed_rust" ]]; then echo "Pinned toolchain '$build_toolchain' is older than rust-version $required_rust; fix rust-toolchain.toml in the repository." >&2 exit 1 fi run_builder rustup toolchain install "$build_toolchain" --profile minimal ;; esac installed_rust=$(run_builder rustup run "$build_toolchain" rustc --version | awk '$1 == "rustc" { print $2; exit }') fi if ! rust_version_at_least "$installed_rust" "$required_rust"; then echo "Toolchain '$build_toolchain' provides Rust $installed_rust, but Cargo.toml requires $required_rust. Aborting before installation/update." >&2 exit 1 fi run_builder rustup run "$build_toolchain" cargo --version echo "Building with Rust $installed_rust ($build_toolchain); required >= $required_rust." # Keep source builds reliable on small Debian VPSes. The repository release # profile may use LTO/codegen settings that can make rustc exceed RAM limits. BUILD_JOBS="${RUSTPROXY_BUILD_JOBS:-1}" if ! [[ "$BUILD_JOBS" =~ ^[1-9][0-9]*$ ]]; then echo "RUSTPROXY_BUILD_JOBS must be a positive integer." >&2 exit 1 fi if [[ "$ALLOW_REMOTE_ADMIN" != "0" && "$ALLOW_REMOTE_ADMIN" != "1" ]]; then echo "RUSTPROXY_ALLOW_REMOTE_ADMIN must be 0 or 1." >&2 exit 1 fi run_builder env RUSTUP_TOOLCHAIN="$build_toolchain" \ CARGO_BUILD_JOBS="$BUILD_JOBS" \ CARGO_PROFILE_RELEASE_LTO=false \ CARGO_PROFILE_RELEASE_CODEGEN_UNITS=8 \ bash -c 'cd "$1" && exec cargo build --release --locked' _ "$work/RustProxy" cd "$work/RustProxy" if [[ "$MODE" == install ]]; then install_args=( --binary target/release/rust-proxy --prefix "$PREFIX" --admin "$ADMIN_LISTEN" ) if [[ "$ALLOW_REMOTE_ADMIN" == "1" ]]; then install_args+=(--allow-remote-admin) fi bash install.sh "${install_args[@]}" printf '\nRustProxy installed. Admin panel: http://%s\n' "$ADMIN_LISTEN" if [[ "$ALLOW_REMOTE_ADMIN" == "1" ]]; then printf 'WARNING: plain-HTTP remote administration was explicitly enabled. Restrict it to a trusted private network and migrate to Administration HTTPS.\n' else printf 'Administration is loopback-only by default. Use an SSH tunnel or enable Administration HTTPS before remote exposure.\n' fi else # update.sh reads the saved installation record and therefore preserves the # existing data directory, HTTP/TLS/admin binds and service account. bash update.sh \ --binary target/release/rust-proxy \ --prefix "$PREFIX" printf '\nRustProxy updated successfully. Existing configuration and data were preserved.\n' fi