From af78f6528c6d124c5d57907123f1f48c33472027 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Mateusz=20Gruszczy=C5=84ski?= Date: Sat, 10 Oct 2026 23:34:10 +0200 Subject: [PATCH] update rust version --- README.md | 25 ++++++ scripts/build-linux-release.sh | 13 +-- scripts/build-opnsense.sh | 1 + scripts/build-release.sh | 2 +- scripts/ensure-rust-toolchain.sh | 133 ++++++++++++++++++++++++++++++ tests/static.sh | 6 +- tests/toolchains.sh | 136 +++++++++++++++++++++++++++++++ 7 files changed, 307 insertions(+), 9 deletions(-) create mode 100755 scripts/ensure-rust-toolchain.sh create mode 100755 tests/toolchains.sh diff --git a/README.md b/README.md index f17452d..6541bbb 100644 --- a/README.md +++ b/README.md @@ -124,6 +124,31 @@ APPLY_FREEBSD_PATCHES=1 `OPNSENSE_RETENTION=10` zachowuje ostatnie 10 wersji pakietu dla danej serii/architektury OPNsense. `0` wyłącza usuwanie starych wersji. +## Automatyczny Rust toolchain podczas budowania + +Builder sprawdza `rust-version` w `Cargo.toml` **pobranego commita** i opcjonalny +`rust-toolchain.toml`/`rust-toolchain` przed kompilacją każdej gałęzi. + +- **Linux DEB/RPM:** używa istniejącego `stable`; wywołuje `rustup` tylko, + gdy brakuje toolchainu albo wersja jest starsza od wymaganej przez projekt. +- **FreeBSD/OPNsense:** `FREEBSD_RUST_TOOLCHAIN` pozostaje bazowym toolchainem. + Gdy jest starszy od `rust-version`, builder automatycznie dobiera wersję + minimalną projektu oraz doinstalowuje target `x86_64-unknown-freebsd`. +- Jeśli projekt przypina konkretny toolchain, builder **respektuje pin**. + Sprzeczny pin i `rust-version` powodują czytelny błąd zamiast cichej zmiany. +- Testy i build danej gałęzi używają tego samego wybranego toolchainu. +- Aktualizacja nie zmienia globalnego `rustup default`, systemowych pakietów + ani wersji już opublikowanych pakietów RustProxy. +- Toolchainy pobrane w trakcie działania są w warstwie kontenera + (`/opt/rustup`); po **odtworzeniu kontenera** mogą wymagać ponownego pobrania. + Cache Cargo w `data/cargo` pozostaje trwały. +- Jeśli pobranie toolchainu lub kompilacja się nie powiedzie, dany kanał + nie zostaje opublikowany; pozostały kanał builder obsługuje niezależnie. + +Działanie nie wymaga nowej opcji w `.env`. Uruchom `./start.sh`, żeby +przebudować obraz z nowym skryptem. Aktualizacje toolchainu na podstawie +źródeł będą potem automatyczne przy kolejnych buildach. + ## FreeBSD cross-build Obraz buildera pobiera i weryfikuje `base.txz` dla `FREEBSD_RELEASE`, a następnie używa: diff --git a/scripts/build-linux-release.sh b/scripts/build-linux-release.sh index 15f5ee1..171c834 100755 --- a/scripts/build-linux-release.sh +++ b/scripts/build-linux-release.sh @@ -54,6 +54,7 @@ fi export PACKAGE_RELEASE cd "$SRC" +LINUX_RUST_TOOLCHAIN=$(/opt/rust-package-builder/scripts/ensure-rust-toolchain.sh "$SRC" linux) log "Linux branch: version $VERSION release $PACKAGE_RELEASE" TEST_MODE="${RUN_LINUX_TESTS:-compile}" @@ -64,18 +65,18 @@ case "$TEST_MODE" in 1|compile|check|no-run) log "Linux cargo tests compile check started (--no-run)" if [[ -f Cargo.lock ]]; then - cargo test --release --locked --no-run + cargo "+$LINUX_RUST_TOOLCHAIN" test --release --locked --no-run else - cargo test --release --no-run + cargo "+$LINUX_RUST_TOOLCHAIN" test --release --no-run fi log "Linux cargo tests compile check completed" ;; full|run) log "Linux full cargo tests started" if [[ -f Cargo.lock ]]; then - cargo test --release --locked + cargo "+$LINUX_RUST_TOOLCHAIN" test --release --locked else - cargo test --release + cargo "+$LINUX_RUST_TOOLCHAIN" test --release fi log "Linux full cargo tests completed" ;; @@ -86,9 +87,9 @@ esac log "Linux cargo build started" if [[ -f Cargo.lock ]]; then - cargo build --release --locked + cargo "+$LINUX_RUST_TOOLCHAIN" build --release --locked else - cargo build --release + cargo "+$LINUX_RUST_TOOLCHAIN" build --release fi log "Linux cargo build completed" diff --git a/scripts/build-opnsense.sh b/scripts/build-opnsense.sh index 4177e2d..24f1d85 100755 --- a/scripts/build-opnsense.sh +++ b/scripts/build-opnsense.sh @@ -29,6 +29,7 @@ export CARGO_TARGET_X86_64_UNKNOWN_FREEBSD_RUSTFLAGS="-C link-arg=--target=x86_6 log "cross-compiling $BINARY_NAME for x86_64-unknown-freebsd${FREEBSD_TARGET_VERSION}" cd "$BUILD_SOURCE" +FREEBSD_RUST_TOOLCHAIN=$(/opt/rust-package-builder/scripts/ensure-rust-toolchain.sh "$BUILD_SOURCE" freebsd) if [[ -f Cargo.lock ]]; then cargo "+$FREEBSD_RUST_TOOLCHAIN" build --release --locked --target x86_64-unknown-freebsd else diff --git a/scripts/build-release.sh b/scripts/build-release.sh index 9da1d10..d5b1699 100755 --- a/scripts/build-release.sh +++ b/scripts/build-release.sh @@ -37,7 +37,7 @@ ACTUAL_SHA=$(git -C "$SRC" rev-parse HEAD) SOURCE_REVISION=$(git -C "$SRC" rev-list --count "$SHA") cd "$SRC" -META_JSON=$(cargo metadata --format-version 1 --no-deps) +META_JSON=$(cargo +stable metadata --format-version 1 --no-deps) export META_JSON SRC readarray -t META < <(python3 <<'PY' diff --git a/scripts/ensure-rust-toolchain.sh b/scripts/ensure-rust-toolchain.sh new file mode 100755 index 0000000..42411eb --- /dev/null +++ b/scripts/ensure-rust-toolchain.sh @@ -0,0 +1,133 @@ +#!/usr/bin/env bash +# Select and verify a stable Rust toolchain for this source checkout. +# Prints the resolved channel on stdout. Progress/errors go to stderr. +set -Eeuo pipefail + +SOURCE_DIR=${1:?usage: ensure-rust-toolchain.sh SOURCE_DIR linux|freebsd} +PLATFORM=${2:?usage: ensure-rust-toolchain.sh SOURCE_DIR linux|freebsd} +[[ "$PLATFORM" == linux || "$PLATFORM" == freebsd ]] || { + printf 'Unsupported build target: %s\n' "$PLATFORM" >&2 + exit 2 +} +[[ -r "$SOURCE_DIR/Cargo.toml" ]] || { + printf 'Missing Cargo.toml: %s\n' "$SOURCE_DIR" >&2 + exit 1 +} + +# Rocky Linux 9 builder image supplies Python 3.11 for TOML parsing. +TOML_PYTHON=${TOML_PYTHON:-python3.11} +metadata=$("$TOML_PYTHON" - "$SOURCE_DIR" <<'PY' +import pathlib +import re +import sys +import tomllib + +root = pathlib.Path(sys.argv[1]) +manifest = tomllib.loads((root / "Cargo.toml").read_text(encoding="utf-8")) +rust_version = manifest.get("package", {}).get("rust-version", "") +if isinstance(rust_version, dict) and rust_version.get("workspace") is True: + rust_version = manifest.get("workspace", {}).get("package", {}).get("rust-version", "") +if not isinstance(rust_version, str): + raise SystemExit("Unsupported Cargo.toml rust-version") +if rust_version and not re.fullmatch(r"[0-9]+\.[0-9]+(?:\.[0-9]+)?", rust_version): + raise SystemExit(f"Unsupported Cargo.toml rust-version: {rust_version}") + +pinned = "" +for filename in ("rust-toolchain.toml", "rust-toolchain"): + path = root / filename + if not path.exists(): + continue + if filename.endswith(".toml"): + pinned = tomllib.loads(path.read_text(encoding="utf-8")).get("toolchain", {}).get("channel", "") + else: + value = path.read_text(encoding="utf-8").strip() + pinned = tomllib.loads(value).get("toolchain", {}).get("channel", "") if value.startswith("[") else value + break +if pinned and not isinstance(pinned, str): + raise SystemExit("Invalid rust-toolchain channel") +if pinned and not (pinned == "stable" or re.fullmatch(r"[0-9]+\.[0-9]+(?:\.[0-9]+)?", pinned)): + raise SystemExit(f"Only stable Rust toolchains are supported: {pinned}") +print(rust_version) +print(pinned) +PY +) +mapfile -t requested <<< "$metadata" +MIN_RUST=${requested[0]:-} +PINNED_TOOLCHAIN=${requested[1]:-} + +version_components() { + local version=$1 + [[ "$version" =~ ^([0-9]+)\.([0-9]+)(\.([0-9]+))?$ ]] || return 1 + V_MAJOR=${BASH_REMATCH[1]} + V_MINOR=${BASH_REMATCH[2]} + V_PATCH=${BASH_REMATCH[4]:-0} +} +version_less() { + local a=$1 b=$2 a_major a_minor a_patch + version_components "$a" || return 2 + a_major=$V_MAJOR a_minor=$V_MINOR a_patch=$V_PATCH + version_components "$b" || return 2 + (( 10#$a_major < 10#$V_MAJOR || \ + (10#$a_major == 10#$V_MAJOR && 10#$a_minor < 10#$V_MINOR) || \ + (10#$a_major == 10#$V_MAJOR && 10#$a_minor == 10#$V_MINOR && 10#$a_patch < 10#$V_PATCH) )) +} + +if [[ -n "$PINNED_TOOLCHAIN" ]]; then + channel=$PINNED_TOOLCHAIN +elif [[ "$PLATFORM" == linux ]]; then + channel=stable +else + channel=${FREEBSD_RUST_TOOLCHAIN:-1.88.0} + if [[ "$channel" != stable ]] && [[ -n "$MIN_RUST" ]] && version_less "$channel" "$MIN_RUST"; then + printf 'FreeBSD base toolchain %s is below project MSRV %s; selecting project MSRV\n' "$channel" "$MIN_RUST" >&2 + channel=$MIN_RUST + fi +fi +[[ "$channel" == stable || "$channel" =~ ^[0-9]+\.[0-9]+(\.[0-9]+)?$ ]] || { + printf 'Unsupported Rust toolchain: %s\n' "$channel" >&2 + exit 1 +} +if [[ -n "$PINNED_TOOLCHAIN" && -n "$MIN_RUST" && "$channel" != stable ]] && version_less "$channel" "$MIN_RUST"; then + printf 'Explicit rust-toolchain pin %s is below project MSRV %s; update the source pin\n' "$channel" "$MIN_RUST" >&2 + exit 1 +fi + +rustc_version() { + local result + result=$(rustup run "$channel" rustc --version 2>/dev/null) || return 1 + [[ "$result" =~ ^rustc[[:space:]]+([0-9]+\.[0-9]+\.[0-9]+) ]] || return 1 + printf '%s' "${BASH_REMATCH[1]}" +} + +if ! installed=$(rustc_version); then + printf 'Installing Rust toolchain %s for %s\n' "$channel" "$PLATFORM" >&2 + rustup toolchain install "$channel" --profile minimal >&2 + installed=$(rustc_version) || { printf 'Toolchain %s unavailable after installation\n' "$channel" >&2; exit 1; } +fi + +if [[ -n "$MIN_RUST" ]] && version_less "$installed" "$MIN_RUST"; then + if [[ "$channel" == stable ]]; then + printf 'Updating stable Rust (%s) to satisfy project MSRV %s\n' "$installed" "$MIN_RUST" >&2 + rustup toolchain install stable --profile minimal >&2 + installed=$(rustc_version) || { printf 'Stable toolchain unavailable after update\n' >&2; exit 1; } + fi + if version_less "$installed" "$MIN_RUST"; then + printf 'Rust %s (%s) does not satisfy project MSRV %s\n' "$installed" "$channel" "$MIN_RUST" >&2 + exit 1 + fi +fi + +if [[ "$PLATFORM" == freebsd ]]; then + target=x86_64-unknown-freebsd + if ! rustup target list --toolchain "$channel" --installed | grep -Fxq "$target"; then + printf 'Installing %s target for Rust %s\n' "$target" "$channel" >&2 + rustup target add "$target" --toolchain "$channel" >&2 + fi + rustup target list --toolchain "$channel" --installed | grep -Fxq "$target" || { + printf 'FreeBSD target is unavailable for Rust %s\n' "$channel" >&2 + exit 1 + } +fi + +printf 'Selected Rust %s (%s) for %s\n' "$installed" "$channel" "$PLATFORM" >&2 +printf '%s\n' "$channel" diff --git a/tests/static.sh b/tests/static.sh index 62fa6b2..5c3f9c7 100755 --- a/tests/static.sh +++ b/tests/static.sh @@ -19,8 +19,10 @@ grep -q 'build-opnsense-release.sh' "$ROOT/scripts/build-release.sh" grep -q 'OPNsense branch will still run' "$ROOT/scripts/build-release.sh" # Linux tests gate Linux packaging only. -grep -q 'cargo test --release' "$ROOT/scripts/build-linux-release.sh" -grep -q 'nfpm package' "$ROOT/scripts/build-linux-release.sh" +grep -Fq 'cargo "+$LINUX_RUST_TOOLCHAIN" test --release' "$ROOT/scripts/build-linux-release.sh" +grep -Fq 'ensure-rust-toolchain.sh' "$ROOT/scripts/build-linux-release.sh" +grep -Fq 'ensure-rust-toolchain.sh' "$ROOT/scripts/build-opnsense.sh" +grep -Fq 'nfpm package' "$ROOT/scripts/build-linux-release.sh" grep -q 'aptly .*publish' "$ROOT/scripts/build-linux-release.sh" grep -q 'createrepo_c' "$ROOT/scripts/build-linux-release.sh" ! grep -q 'cargo test' "$ROOT/scripts/build-opnsense-release.sh" diff --git a/tests/toolchains.sh b/tests/toolchains.sh new file mode 100755 index 0000000..006ff2b --- /dev/null +++ b/tests/toolchains.sh @@ -0,0 +1,136 @@ +#!/usr/bin/env bash +# Offline simulation of rustup installation/selection; no Rust or network required. +set -Eeuo pipefail +ROOT=$(CDPATH= cd -- "$(dirname -- "$0")/.." && pwd) +WORK=$(mktemp -d) +trap 'rm -rf "$WORK"' EXIT +mkdir -p "$WORK/bin" "$WORK/source" "$WORK/state" +cat > "$WORK/bin/rustup" <<'MOCK' +#!/usr/bin/env bash +set -Eeuo pipefail +state=${MOCK_RUSTUP_STATE:?} +case "$1" in + run) + channel=$2 + [[ -r "$state/$channel.version" ]] || exit 1 + printf 'rustc %s (mock)\n' "$(cat "$state/$channel.version")" + ;; + toolchain) + [[ "$2" == install ]] || exit 7 + channel=$3 + [[ ${MOCK_INSTALL_FAIL:-0} != 1 ]] || exit 9 + case "$channel" in + stable) version=${MOCK_NEW_STABLE:-1.100.0} ;; + *) case "$channel" in *.*.*) version=$channel ;; *) version=${channel}.0 ;; esac ;; + esac + echo "$version" > "$state/$channel.version" + printf 'install %s\n' "$channel" >> "$state/calls" + ;; + target) + action=$2 + if [[ "$action" == list ]]; then + [[ "$3" == --toolchain && "$5" == --installed ]] || exit 7 + [[ -f "$state/$4.target" ]] && cat "$state/$4.target" + elif [[ "$action" == add ]]; then + [[ "$4" == --toolchain ]] || exit 7 + echo "$3" > "$state/$5.target" + printf 'target %s %s\n' "$5" "$3" >> "$state/calls" + else exit 7 + fi + ;; + *) printf 'Unexpected rustup arguments: %s\n' "$*" >&2; exit 7 ;; +esac +MOCK +chmod +x "$WORK/bin/rustup" +export PATH="$WORK/bin:$PATH" MOCK_RUSTUP_STATE="$WORK/state" TOML_PYTHON=python3 +TOOL="$ROOT/scripts/ensure-rust-toolchain.sh" +reset_state() { rm -f "$WORK/state/"*; } +manifest() { printf '[package]\nname = "rust-proxy"\nversion = "0.31.2"\nrust-version = "%s"\n' "$1" > "$WORK/source/Cargo.toml"; rm -f "$WORK/source/rust-toolchain" "$WORK/source/rust-toolchain.toml"; } + +# No update or download when installed stable already satisfies the manifest. +reset_state +manifest 1.99 +printf '1.100.0\n' > "$WORK/state/stable.version" +result=$($TOOL "$WORK/source" linux) +[[ "$result" == stable ]] +[[ ! -e "$WORK/state/calls" ]] + +# Upgrade stable only when too old, then reuse it on subsequent checks. +reset_state +printf '1.98.1\n' > "$WORK/state/stable.version" +result=$($TOOL "$WORK/source" linux) +[[ "$result" == stable ]] +grep -Fqx 'install stable' "$WORK/state/calls" +[[ $($TOOL "$WORK/source" linux) == stable ]] +[[ $(wc -l < "$WORK/state/calls") == 1 ]] + +# FreeBSD default 1.88 is auto raised to the project's 1.99 MSRV with its own target. +reset_state +result=$(FREEBSD_RUST_TOOLCHAIN=1.88.0 "$TOOL" "$WORK/source" freebsd) +[[ "$result" == 1.99 ]] +grep -Fqx 'install 1.99' "$WORK/state/calls" +grep -Fqx 'target 1.99 x86_64-unknown-freebsd' "$WORK/state/calls" +[[ $(FREEBSD_RUST_TOOLCHAIN=1.88.0 "$TOOL" "$WORK/source" freebsd) == 1.99 ]] +[[ $(wc -l < "$WORK/state/calls") == 2 ]] + +# A deliberate rust-toolchain.toml pin is respected; do not silently override it. +reset_state +cat > "$WORK/source/rust-toolchain.toml" <<'PIN' +[toolchain] +channel = "1.98.0" +PIN +if "$TOOL" "$WORK/source" linux >/dev/null 2>&1; then + printf 'An incompatible explicit pin should have failed\n' >&2 + exit 1 +fi +[[ ! -e "$WORK/state/calls" ]] || { echo 'Should not install an incompatible pin' >&2; exit 1; } +rm "$WORK/source/rust-toolchain.toml" + +# Pin that meets the project requirement is installed without changing stable. +reset_state +cat > "$WORK/source/rust-toolchain.toml" <<'PIN' +[toolchain] +channel = "1.99.0" +PIN +[[ $($TOOL "$WORK/source" linux) == 1.99.0 ]] +grep -Fqx 'install 1.99.0' "$WORK/state/calls" +rm "$WORK/source/rust-toolchain.toml" + +# RustProxy-like rust-toolchain.toml (stable) also upgrades stale stable for FreeBSD. +reset_state +manifest 1.99 +cat > "$WORK/source/rust-toolchain.toml" <<'PIN' +[toolchain] +channel = "stable" +profile = "minimal" +components = ["rustfmt", "clippy"] +PIN +printf '1.98.1\n' > "$WORK/state/stable.version" +[[ $(FREEBSD_RUST_TOOLCHAIN=1.88.0 "$TOOL" "$WORK/source" freebsd) == stable ]] +grep -Fqx 'install stable' "$WORK/state/calls" +grep -Fqx 'target stable x86_64-unknown-freebsd' "$WORK/state/calls" +rm "$WORK/source/rust-toolchain.toml" + +# Workspace-inherited rust-version is resolved through TOML, not grep. +reset_state +cat > "$WORK/source/Cargo.toml" <<'MANIFEST' +[workspace.package] +rust-version = "1.99" +[package] +name = "rust-proxy" +version = "0.31.2" +rust-version = { workspace = true } +MANIFEST +printf '1.100.0\n' > "$WORK/state/stable.version" +[[ $($TOOL "$WORK/source" linux) == stable ]] + +# An unavailable upgrade must be a hard failure. +reset_state +manifest 1.99 +printf '1.98.1\n' > "$WORK/state/stable.version" +if MOCK_INSTALL_FAIL=1 "$TOOL" "$WORK/source" linux >/dev/null 2>&1; then + printf 'A failed rustup update should abort the build\n' >&2 + exit 1 +fi + +echo 'Rust toolchain selection: 8 offline scenarios OK'