#!/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"