From 20e573ccefbdeeab8015b0faa5dabed832201b7d Mon Sep 17 00:00:00 2001 From: Simone Date: Wed, 22 Jul 2026 14:08:40 +0200 Subject: [PATCH] fix(toolchain): chown /opt/rust after install, not only before The chown ran before rustup and cargo-audit were installed, so everything those created under RUSTUP_HOME/CARGO_HOME was left root-owned: only the two empty directories existing at chown time belonged to ci_build. Measured on the previous snapshot: 18350 root-owned paths, including cargo/registry. Every Rust build with external dependencies failed with "Permission denied (os error 13)". Re-assert ownership after the installs, and assert writability rather than mere existence -- test -x on the binaries could not catch this, so a broken template passed its own build and failed in consumers' jobs instead. Also wire the toolchain for non-login shells. The orchestrator runs build commands via paramiko exec_command + `sh -c`, which never sources /etc/profile.d, so profile.d alone was not enough: add RUSTUP_HOME/CARGO_HOME to /etc/environment (read by pam_env at SSH session open) and symlink the rustup proxies into /usr/local/bin. --- template/Install-CIToolchain-LinuxRust2404.sh | 490 ++++++++++++++++++ 1 file changed, 490 insertions(+) create mode 100644 template/Install-CIToolchain-LinuxRust2404.sh diff --git a/template/Install-CIToolchain-LinuxRust2404.sh b/template/Install-CIToolchain-LinuxRust2404.sh new file mode 100644 index 0000000..9f81d18 --- /dev/null +++ b/template/Install-CIToolchain-LinuxRust2404.sh @@ -0,0 +1,490 @@ +#!/usr/bin/env bash +# Install-CIToolchain-LinuxRust2404.sh +# Guest-side CI toolchain setup for the Ubuntu 24.04 LTS **Rust** build template +# (LinuxRust2404). Same base as Install-CIToolchain-Linux2404.sh, plus: +# - Tauri/webview GTK development libraries (Step 4d) +# - system-wide Rust toolchain via rustup in /opt/rust (Step 4e) +# Run as ci_build (passwordless sudo) via: sudo /tmp/Install-CIToolchain-LinuxRust2404.sh +# Called by: ci template prepare-rust (host-side) +# +# Usage: +# sudo /tmp/Install-CIToolchain-LinuxRust2404.sh [--skip-update] [--dotnet] [--no-mingw] +# [--rust-channel CHANNEL] [--no-cargo-audit] +# +# Options: +# --skip-update Skip apt update + upgrade (faster re-runs) +# --dotnet Install .NET SDK 10.0 (optional, adds ~500 MB) +# --no-mingw Skip mingw-w64 cross-compiler (installed by default) +# --rust-channel CH Rust toolchain channel to install (default: stable). +# Pin an exact version (e.g. 1.97.1) for a deterministic +# snapshot; "stable" is a moving target and rustup will +# re-download at runtime when a new stable ships. +# --no-cargo-audit Skip `cargo install cargo-audit --locked` (~2-3 min +# compile). The advisory DB is NEVER baked — cargo-audit +# fetches it at runtime on each job. +# +# Exit codes: +# 0 All steps passed, system ready for snapshot +# 1 One or more steps failed (output shows which step and why) + +set -euo pipefail + +# --- Option parsing --- +SKIP_UPDATE=0 +INSTALL_DOTNET=0 +INSTALL_MINGW=1 +RUST_CHANNEL="stable" +INSTALL_CARGO_AUDIT=1 + +while [[ $# -gt 0 ]]; do + case "$1" in + --skip-update) SKIP_UPDATE=1 ;; + --dotnet) INSTALL_DOTNET=1 ;; + --no-mingw) INSTALL_MINGW=0 ;; + --rust-channel) RUST_CHANNEL="${2:?--rust-channel requires a value}"; shift ;; + --no-cargo-audit) INSTALL_CARGO_AUDIT=0 ;; + *) echo "Unknown option: $1" >&2; exit 1 ;; + esac + shift +done + +# --- Assert helper --- +STEP=0 + +assert_step() { + local desc="$1" + shift + STEP=$((STEP + 1)) + if "$@"; then + echo " [OK] [Step ${STEP}] ${desc}" + else + echo " [FAIL] [Step ${STEP}] ${desc}" >&2 + exit 1 + fi +} + +# --- Step 1: apt update + upgrade --- +echo "" +echo "=== Step 1: apt update + upgrade ===" +# apt-get update always runs — package index must be fresh to locate packages. +# --skip-update only skips the (slow) full upgrade. +sudo DEBIAN_FRONTEND=noninteractive apt-get update -qq +assert_step "apt update completed" true +if [ "${SKIP_UPDATE:-0}" = "0" ]; then + sudo DEBIAN_FRONTEND=noninteractive apt-get upgrade -y -qq + assert_step "apt upgrade completed" true +else + echo " [SKIP] --skip-update flag set — upgrade skipped (index still refreshed)" +fi + +# --- Step 2: build essentials --- +echo "" +echo "=== Step 2: build essentials ===" +sudo DEBIAN_FRONTEND=noninteractive apt-get install -y -qq \ + build-essential gcc g++ clang \ + make cmake ninja-build \ + pkg-config autoconf automake libtool +assert_step "gcc available" command -v gcc +assert_step "g++ available" command -v g++ +assert_step "clang available" command -v clang +assert_step "cmake available" command -v cmake +assert_step "make available" command -v make + +# --- Step 3: Python --- +echo "" +echo "=== Step 3: Python ===" +sudo DEBIAN_FRONTEND=noninteractive apt-get install -y -qq python3 python3-pip python3-venv +sudo ln -sf /usr/bin/python3 /usr/local/bin/python +assert_step "python3 available" command -v python3 +assert_step "pip3 available" command -v pip3 + +# --- Step 4: Tier-1 toolchain --- +echo "" +echo "=== Step 4: Tier-1 toolchain (git, 7-zip, curl, wget) ===" +sudo DEBIAN_FRONTEND=noninteractive apt-get install -y -qq git p7zip-full curl wget ca-certificates +assert_step "git available" command -v git +assert_step "7z available" command -v 7z +assert_step "curl available" command -v curl + +# --- Step 4a: GTK/GObject + headless display (PyGObject GUI tests) --- +echo "" +echo "=== Step 4a: GTK4 / GObject introspection + Xvfb ===" +sudo DEBIAN_FRONTEND=noninteractive apt-get install -y -qq \ + python3-gi gir1.2-gtk-4.0 xvfb +assert_step "python3-gi importable" python3 -c "import gi" +assert_step "xvfb available" command -v Xvfb + +# --- Step 4b: .NET SDK (optional) --- +echo "" +echo "=== Step 4b: .NET SDK (optional) ===" +if [ "${INSTALL_DOTNET:-0}" = "1" ]; then + curl -fsSL https://dotnet.microsoft.com/download/dotnet/scripts/v1/dotnet-install.sh \ + | sudo bash -s -- --channel 10.0 --install-dir /usr/local/dotnet + sudo ln -sf /usr/local/dotnet/dotnet /usr/local/bin/dotnet + assert_step "dotnet available" command -v dotnet +else + echo " [SKIP] --dotnet not specified" +fi + +# --- Step 4c: mingw-w64 cross-compiler (default ON: required by ns7zip Linux build) --- +echo "" +echo "=== Step 4c: mingw-w64 cross-compiler ===" +if [ "$INSTALL_MINGW" = "1" ]; then + sudo DEBIAN_FRONTEND=noninteractive apt-get install -y -qq \ + binutils-mingw-w64-i686 binutils-mingw-w64-x86-64 \ + gcc-mingw-w64-i686 gcc-mingw-w64-x86-64 \ + g++-mingw-w64-i686 g++-mingw-w64-x86-64 + assert_step "i686-w64-mingw32-gcc available" command -v i686-w64-mingw32-gcc + assert_step "x86_64-w64-mingw32-gcc available" command -v x86_64-w64-mingw32-gcc + assert_step "i686-w64-mingw32-g++ available" command -v i686-w64-mingw32-g++ + assert_step "x86_64-w64-mingw32-g++ available" command -v x86_64-w64-mingw32-g++ +else + echo " [SKIP] --no-mingw passed, skipping mingw-w64" +fi + +# --- Step 4d: Tauri v2 / webview GTK development libraries --- +# Required to compile Tauri v2 apps (fury-gui): webkit2gtk-4.1 + GTK3 + +# appindicator + librsvg. These are the slow, stable apt deps that motivated +# baking a Rust template instead of provisioning per job. +echo "" +echo "=== Step 4d: Tauri/webview GTK development libraries ===" +sudo DEBIAN_FRONTEND=noninteractive apt-get install -y -qq \ + libwebkit2gtk-4.1-dev libgtk-3-dev libayatana-appindicator3-dev \ + librsvg2-dev +assert_step "webkit2gtk-4.1 pkg-config" pkg-config --exists webkit2gtk-4.1 +assert_step "gtk+-3.0 pkg-config" pkg-config --exists gtk+-3.0 +assert_step "ayatana-appindicator3 pkg-config" \ + pkg-config --exists ayatana-appindicator3-0.1 +assert_step "librsvg-2.0 pkg-config" pkg-config --exists librsvg-2.0 + +# --- Step 4e: system-wide Rust toolchain (rustup in /opt/rust) --- +# RUSTUP_HOME/CARGO_HOME live under /opt/rust so every user (ci_build and any +# future build user) shares one toolchain. Three independent exposure paths, +# because /etc/profile.d alone is NOT enough: the orchestrator runs commands +# via paramiko exec_command + `sh -c` (transport/ssh.py, commands/build.py), +# which is a non-login, non-interactive shell and never sources profile.d. +# 1. /etc/profile.d/rust.sh — interactive/login shells (humans, `ssh -t`) +# 2. /etc/environment — read by pam_env when sshd opens the session, +# so RUSTUP_HOME/CARGO_HOME reach non-login +# exec_command invocations +# 3. /usr/local/bin symlinks — puts the rustup proxies on the default PATH +# without rewriting PATH itself +# +# NOTE on freshness: the *binaries* (rustc/cargo/clippy/rustfmt/cargo-audit) +# are baked; the cargo-audit advisory DB is NOT — `cargo audit` fetches it at +# runtime. If --rust-channel is "stable" (moving target), rustup re-downloads +# the toolchain at runtime when a new stable ships; pin an exact version for +# a fully deterministic snapshot. +echo "" +echo "=== Step 4e: Rust toolchain (channel: ${RUST_CHANNEL}) ===" +export RUSTUP_HOME=/opt/rust/rustup +export CARGO_HOME=/opt/rust/cargo +sudo mkdir -p "$RUSTUP_HOME" "$CARGO_HOME" +sudo chown -R ci_build:ci_build /opt/rust + +curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs \ + | sh -s -- -y --profile minimal --no-modify-path --default-toolchain "$RUST_CHANNEL" + +"$CARGO_HOME/bin/rustup" component add rustfmt clippy + +if [ "$INSTALL_CARGO_AUDIT" = "1" ]; then + "$CARGO_HOME/bin/cargo" install cargo-audit --locked +else + echo " [SKIP] --no-cargo-audit passed, skipping cargo-audit install" +fi + +# Re-assert ownership AFTER the installs. This script runs as root (sudo), so +# everything rustup/cargo created above is root-owned; the chown before the +# install only covers the two empty directories. Without this, ci_build cannot +# write to RUSTUP_HOME (toolchain sync) or CARGO_HOME/registry (dependency +# download), and every Rust build with external deps fails with EACCES. +sudo chown -R ci_build:ci_build /opt/rust + +# Expose cargo/rustc on PATH for all users via login shells. +sudo tee /etc/profile.d/rust.sh > /dev/null << 'EORUST' +export RUSTUP_HOME=/opt/rust/rustup +export CARGO_HOME=/opt/rust/cargo +case ":$PATH:" in + *:/opt/rust/cargo/bin:*) ;; + *) export PATH="/opt/rust/cargo/bin:$PATH" ;; +esac +EORUST +sudo chmod 644 /etc/profile.d/rust.sh + +# (2) pam_env — reaches non-login SSH sessions, which is how the orchestrator +# actually runs build commands. Idempotent: drop any previous entries first. +sudo sed -i '/^RUSTUP_HOME=/d; /^CARGO_HOME=/d' /etc/environment +printf 'RUSTUP_HOME=%s\nCARGO_HOME=%s\n' "$RUSTUP_HOME" "$CARGO_HOME" \ + | sudo tee -a /etc/environment > /dev/null +sudo chmod 644 /etc/environment + +# (3) rustup proxies on the default PATH. /usr/local/bin is already in the +# stock non-login PATH, so no PATH rewriting is needed. +for proxy in cargo rustc rustup rustdoc rustfmt cargo-fmt cargo-clippy; do + sudo ln -sf "/opt/rust/cargo/bin/${proxy}" "/usr/local/bin/${proxy}" +done +if [ "$INSTALL_CARGO_AUDIT" = "1" ]; then + sudo ln -sf /opt/rust/cargo/bin/cargo-audit /usr/local/bin/cargo-audit +fi + +assert_step "cargo available" test -x /opt/rust/cargo/bin/cargo +assert_step "rustc available" test -x /opt/rust/cargo/bin/rustc +assert_step "rustfmt component" test -x /opt/rust/cargo/bin/cargo-fmt +assert_step "clippy component" test -x /opt/rust/cargo/bin/cargo-clippy +if [ "$INSTALL_CARGO_AUDIT" = "1" ]; then + assert_step "cargo-audit installed" test -x /opt/rust/cargo/bin/cargo-audit +fi +assert_step "/etc/profile.d/rust.sh exists" test -f /etc/profile.d/rust.sh +assert_step "/etc/environment has RUSTUP_HOME" \ + grep -q '^RUSTUP_HOME=/opt/rust/rustup$' /etc/environment +assert_step "/etc/environment has CARGO_HOME" \ + grep -q '^CARGO_HOME=/opt/rust/cargo$' /etc/environment +assert_step "cargo symlinked into /usr/local/bin" test -x /usr/local/bin/cargo +assert_step "rustc symlinked into /usr/local/bin" test -x /usr/local/bin/rustc +# The orchestrator runs build commands in a NON-login shell. Simulate that +# environment (stock PATH + pam_env vars only, no profile.d) and require the +# toolchain to resolve anyway. +assert_step "toolchain resolves in a non-login environment" \ + sudo -u ci_build env -i \ + PATH=/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin \ + RUSTUP_HOME=/opt/rust/rustup CARGO_HOME=/opt/rust/cargo \ + HOME=/home/ci_build \ + rustc --version +# Existence is not enough: the build user must be able to WRITE the toolchain +# tree, or the failure surfaces in consumers' jobs instead of here. +assert_step "ci_build owns /opt/rust" \ + test "$(stat -c '%U' /opt/rust)" = "ci_build" +assert_step "rustup home writable by ci_build" \ + sudo -u ci_build test -w /opt/rust/rustup +assert_step "cargo home writable by ci_build" \ + sudo -u ci_build test -w /opt/rust/cargo +assert_step "ci_build can create in rustup tmp" \ + sudo -u ci_build sh -c \ + 'mkdir -p /opt/rust/rustup/tmp && touch /opt/rust/rustup/tmp/.ci_probe && rm /opt/rust/rustup/tmp/.ci_probe' +assert_step "ci_build can create in cargo registry" \ + sudo -u ci_build sh -c \ + 'mkdir -p /opt/rust/cargo/registry && touch /opt/rust/cargo/registry/.ci_probe && rm /opt/rust/cargo/registry/.ci_probe' + +# --- Step 5: CI directories + sudo group --- +echo "" +echo "=== Step 5: CI directories ===" +sudo mkdir -p /opt/ci/{build,output,scripts,cache} +sudo chown -R ci_build:ci_build /opt/ci +# Ensure ci_build is in the sudo group (belt-and-suspenders alongside sudoers.d entry) +sudo usermod -aG sudo ci_build +assert_step "/opt/ci/build exists" test -d /opt/ci/build +assert_step "/opt/ci/output exists" test -d /opt/ci/output +assert_step "/opt/ci/scripts exists" test -d /opt/ci/scripts +assert_step "/opt/ci/cache exists" test -d /opt/ci/cache +assert_step "ci_build owns /opt/ci" test "$(stat -c '%U' /opt/ci)" = "ci_build" + +# --- Step 6: SSH hardening --- +echo "" +echo "=== Step 6: SSH hardening ===" +sudo sed -i 's/^#*PasswordAuthentication.*/PasswordAuthentication no/' /etc/ssh/sshd_config +sudo sed -i 's/^#*PubkeyAuthentication.*/PubkeyAuthentication yes/' /etc/ssh/sshd_config +sudo systemctl restart ssh +assert_step "PasswordAuthentication no in sshd_config" grep -q "^PasswordAuthentication no" /etc/ssh/sshd_config +assert_step "sshd service active" systemctl is-active ssh + +# --- Step 7: disable swap --- +echo "" +echo "=== Step 7: disable swap ===" +sudo swapoff -a +sudo sed -i '/ swap / s/^/#/' /etc/fstab +assert_step "no active swap" bash -c '[ -z "$(swapon --show)" ]' + +# --- Step 7a: MAC-agnostic netplan for linked clones --- +# Cloud-init generates /etc/netplan/50-cloud-init.yaml with the template's MAC +# pinned via `match: macaddress: ...`. When VMware creates a linked clone with +# a new MAC, that match fails and the clone has no network. We add a second +# netplan file that matches any en* interface by name, so any clone (any MAC) +# gets DHCP. The two files are merged; if 50-cloud-init also matches, fine — +# we just guarantee DHCP works regardless. +echo "" +echo "=== Step 7a: MAC-agnostic netplan for clones ===" +sudo tee /etc/netplan/99-ci-dhcp-allnics.yaml > /dev/null << 'EONETPLAN' +network: + version: 2 + ethernets: + ci-all-en: + match: + name: "en*" + dhcp4: true + dhcp6: false +EONETPLAN +sudo chmod 600 /etc/netplan/99-ci-dhcp-allnics.yaml +sudo chown root:root /etc/netplan/99-ci-dhcp-allnics.yaml +sudo netplan generate +assert_step "99-ci-dhcp-allnics.yaml exists" test -f /etc/netplan/99-ci-dhcp-allnics.yaml + +# --- Step 7b: CI IP reporter via VMware guestinfo --- +# Supports two modes: +# Static (ip_pool): reads guestinfo.ip-assignment injected by the host +# orchestrator, applies static NIC config, reports back. +# DHCP fallback: waits for DHCP address, reports via guestinfo.ci-ip. +# Content embedded inline — this script runs inside the VM where the host-side +# template/guest-setup/linux/ directory is not present. +echo "" +echo "=== Step 7b: CI IP reporter (with static IP support) ===" +sudo tee /usr/local/bin/ci-report-ip.sh > /dev/null << 'EOREPORTIP' +#!/bin/bash +# CI VM IP reporter — called by ci-report-ip.service at boot. +# +# Supports two modes: +# Static (ip_pool): reads guestinfo.ip-assignment injected by the host +# orchestrator, applies static NIC config, reports back. +# DHCP fallback: waits for DHCP address, reports via guestinfo.ci-ip. +# +# The VMCI channel (vmware-rpctool) does not require TCP/IP to be initialised — +# it communicates via the VMware virtual hardware interface. This means the +# static IP can be configured before network-online.target, so by the time SSH +# starts listening the NIC already has the correct address. + +RPCTOOL=/usr/bin/vmware-rpctool +[ -x "$RPCTOOL" ] || exit 0 + +# ── Check for pre-assigned IP from orchestrator ─────────────────────────────── +assigned_ip=$("$RPCTOOL" "info-get guestinfo.ip-assignment" 2>/dev/null | tr -d '[:space:]') + +if [[ "$assigned_ip" =~ ^[0-9]+\.[0-9]+\.[0-9]+\.[0-9]+$ ]]; then + netmask=$("$RPCTOOL" "info-get guestinfo.netmask" 2>/dev/null | tr -d '[:space:]') + gateway=$("$RPCTOOL" "info-get guestinfo.gateway" 2>/dev/null | tr -d '[:space:]') + + # Resolve primary NIC (first non-loopback). + iface=$(ip -o link show | awk -F': ' '$2 != "lo" {print $2; exit}') + if [ -z "$iface" ]; then + echo "ci-report-ip: no non-loopback interface found" | systemd-cat -t ci-report-ip + exit 1 + fi + + # Compute prefix length from netmask (default /24). + prefix=24 + if [ -n "$netmask" ]; then + prefix=$(python3 -c \ + "import ipaddress; print(ipaddress.IPv4Network('0.0.0.0/$netmask',strict=False).prefixlen)" \ + 2>/dev/null) || prefix=24 + fi + + # Flush any existing config (DHCP or stale static) and apply. + ip addr flush dev "$iface" 2>/dev/null || true + ip addr add "${assigned_ip}/${prefix}" dev "$iface" + ip link set "$iface" up + + if [[ "$gateway" =~ ^[0-9]+\.[0-9]+\.[0-9]+\.[0-9]+$ ]]; then + ip route add default via "$gateway" dev "$iface" 2>/dev/null || true + fi + + # Report IP back to host via guestinfo.ci-ip. + "$RPCTOOL" "info-set guestinfo.ci-ip $assigned_ip" 2>/dev/null + echo "ci-report-ip: applied static IP $assigned_ip/$prefix via guestinfo" \ + | systemd-cat -t ci-report-ip + exit 0 +fi + +# ── Fallback: DHCP — wait for IP and report ─────────────────────────────────── +# Retries for up to 60 s to tolerate open-vm-tools re-init after UUID change +# (linked clones get a new UUID — vmware-rpctool may fail briefly on first boot). +for i in $(seq 1 30); do + IP=$(ip -4 route get 1.0.0.0 2>/dev/null \ + | awk '{for(i=1;i<=NF;i++) if($i=="src") print $(i+1)}' \ + | head -1) + if [ -n "$IP" ]; then + if "$RPCTOOL" "info-set guestinfo.ci-ip $IP" 2>/dev/null; then + echo "ci-report-ip: set guestinfo.ci-ip=$IP via DHCP (attempt $i)" \ + | systemd-cat -t ci-report-ip + exit 0 + fi + fi + sleep 2 +done + +echo "ci-report-ip: failed to set guestinfo.ci-ip after 30 attempts" \ + | systemd-cat -t ci-report-ip +exit 1 +EOREPORTIP +sudo chmod +x /usr/local/bin/ci-report-ip.sh + +sudo tee /etc/systemd/system/ci-report-ip.service > /dev/null << 'EOSERVICE' +[Unit] +Description=Report CI VM IP to VMware host via guestinfo +# Run after open-vm-tools so vmware-rpctool is available. +# Do NOT add network-online.target — we want to run BEFORE the network stack +# settles so static IP is applied before SSH/other services start. +After=open-vm-tools.service +Wants=open-vm-tools.service + +[Service] +Type=oneshot +ExecStart=/usr/local/bin/ci-report-ip.sh +RemainAfterExit=yes +TimeoutStartSec=120 + +[Install] +WantedBy=multi-user.target +EOSERVICE + +sudo systemctl daemon-reload +sudo systemctl enable ci-report-ip.service +assert_step "ci-report-ip.service enabled" systemctl is-enabled ci-report-ip.service + +# --- Step 8: disable apt auto-update --- +echo "" +echo "=== Step 8: disable apt auto-update ===" +sudo systemctl mask apt-daily.service apt-daily-upgrade.service \ + apt-daily.timer apt-daily-upgrade.timer 2>/dev/null || true +sudo DEBIAN_FRONTEND=noninteractive apt-get remove -y unattended-upgrades 2>/dev/null || true +assert_step "apt-daily.timer masked or inactive" bash -c \ + 'systemctl is-enabled apt-daily.timer 2>/dev/null | grep -qE "masked|disabled" || true' + +# --- Step 9: cleanup pre-snapshot --- +echo "" +echo "=== Step 9: cleanup pre-snapshot ===" +sudo apt-get clean +sudo rm -rf /var/lib/apt/lists/* +sudo journalctl --vacuum-time=1d 2>/dev/null || true +sudo rm -rf /tmp/* /var/tmp/* 2>/dev/null || true +history -c 2>/dev/null || true +cat /dev/null > ~/.bash_history 2>/dev/null || true +assert_step "apt cache cleaned" bash -c '[ -z "$(ls /var/cache/apt/archives/*.deb 2>/dev/null)" ]' + +# --- Final Gate: pre-snapshot validation --- +echo "" +echo "=== Final Gate: pre-snapshot validation ===" + +for tool in gcc g++ clang cmake make git 7z curl python3 pip3; do + assert_step "${tool} in PATH" command -v "${tool}" +done + +# Rust toolchain (binaries baked under /opt/rust/cargo/bin). +for tool in cargo rustc rustfmt cargo-clippy cargo-fmt; do + assert_step "${tool} baked" test -x "/opt/rust/cargo/bin/${tool}" +done +if [ "$INSTALL_CARGO_AUDIT" = "1" ]; then + assert_step "cargo-audit baked" test -x /opt/rust/cargo/bin/cargo-audit +fi +assert_step "profile.d rust.sh" test -f /etc/profile.d/rust.sh +assert_step "ci_build owns /opt/rust" test "$(stat -c '%U' /opt/rust)" = "ci_build" +assert_step "rustup home writable by ci_build" sudo -u ci_build test -w /opt/rust/rustup +assert_step "cargo home writable by ci_build" sudo -u ci_build test -w /opt/rust/cargo + +for d in /opt/ci/build /opt/ci/output /opt/ci/scripts /opt/ci/cache; do + assert_step "${d} exists" test -d "${d}" +done +assert_step "ci_build owns /opt/ci" test "$(stat -c '%U' /opt/ci)" = "ci_build" + +assert_step "no active swap" bash -c '[ -z "$(swapon --show)" ]' + +assert_step "sshd active" systemctl is-active ssh +assert_step "PasswordAuthentication no" grep -q "^PasswordAuthentication no" /etc/ssh/sshd_config + +assert_step "root fs >= 38 GB" bash -c \ + 'df -BG / | awk '\''NR==2{gsub("G","",$2); if ($2+0 < 38) {print "FAIL: root fs too small ("$2"G)"; exit 1}}'\''' + +assert_step "/tmp writable" bash -c 'touch /tmp/.ci_tmpcheck && rm /tmp/.ci_tmpcheck' +assert_step "ci-report-ip.service enabled" systemctl is-enabled ci-report-ip.service + +echo "" +echo "=== Install-CIToolchain-LinuxRust2404.sh complete ===" +echo " All steps passed. VM is ready for BaseClean-LinuxRust snapshot." +echo " Run: ci template prepare-rust (host-side) to shutdown + snapshot."