Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
10 changes: 4 additions & 6 deletions .github/workflows/research-book-pages.yml
Original file line number Diff line number Diff line change
@@ -1,10 +1,10 @@
name: Rheon research book and Pages
on:
pull_request:
paths: ['browser/viewer/**', 'browser/solver/**', 'tests/forces_contract.rs', 'tests/simulation_contract.rs', 'docs/education/**', 'docs/research-book/companion/**', 'docs/research-book/compressible/**', 'docs/research-book/chapters/**', 'docs/research-book/engineering/**', 'docs/research-book/implementation/**', 'docs/research-book/sources/**', 'docs/research-book/appendices/**', 'docs/research-book/figures/**', 'docs/research-book/expansion/**', 'proofs/**', 'src/**', 'examples/aligned_strain.rs', 'tests/aligned_strain_contract.rs', 'tools/aligned_strain_oracle.py', 'tools/test_aligned_strain_oracle.py', 'tools/qualify_aligned_strain.py', 'tools/test_qualify_aligned_strain.py', 'rust-toolchain.toml', 'examples/static_obstacle.rs', 'examples/obstacle_flow.rs', 'tests/obstacle_flow_contract.rs', 'Cargo.*', 'evidence/bounded-physics-contracts/**', '.github/workflows/research-book-pages.yml']
paths: ['browser/viewer/**', 'browser/solver/**', 'tests/forces_contract.rs', 'tests/simulation_contract.rs', 'docs/education/**', 'docs/research-book/companion/**', 'docs/research-book/compressible/**', 'docs/research-book/chapters/**', 'docs/research-book/engineering/**', 'docs/research-book/implementation/**', 'docs/research-book/sources/**', 'docs/research-book/appendices/**', 'docs/research-book/figures/**', 'docs/research-book/expansion/**', 'proofs/**', 'src/**', 'examples/aligned_strain.rs', 'tests/aligned_strain_contract.rs', 'tools/aligned_strain_oracle.py', 'tools/test_aligned_strain_oracle.py', 'tools/qualify_aligned_strain.py', 'tools/test_qualify_aligned_strain.py', 'rust-toolchain.toml', 'tools/setup_ci_rust.py', 'tools/test_setup_ci_rust.py', 'examples/static_obstacle.rs', 'examples/obstacle_flow.rs', 'tests/obstacle_flow_contract.rs', 'Cargo.*', 'evidence/bounded-physics-contracts/**', '.github/workflows/research-book-pages.yml']
push:
branches: [main]
paths: ['browser/viewer/**', 'browser/solver/**', 'tests/forces_contract.rs', 'tests/simulation_contract.rs', 'docs/education/**', 'docs/research-book/companion/**', 'docs/research-book/compressible/**', 'docs/research-book/chapters/**', 'docs/research-book/engineering/**', 'docs/research-book/implementation/**', 'docs/research-book/sources/**', 'docs/research-book/appendices/**', 'docs/research-book/figures/**', 'docs/research-book/expansion/**', 'proofs/**', 'src/**', 'examples/aligned_strain.rs', 'tests/aligned_strain_contract.rs', 'tools/aligned_strain_oracle.py', 'tools/test_aligned_strain_oracle.py', 'tools/qualify_aligned_strain.py', 'tools/test_qualify_aligned_strain.py', 'rust-toolchain.toml', 'examples/static_obstacle.rs', 'examples/obstacle_flow.rs', 'tests/obstacle_flow_contract.rs', 'Cargo.*', 'evidence/bounded-physics-contracts/**', '.github/workflows/research-book-pages.yml']
paths: ['browser/viewer/**', 'browser/solver/**', 'tests/forces_contract.rs', 'tests/simulation_contract.rs', 'docs/education/**', 'docs/research-book/companion/**', 'docs/research-book/compressible/**', 'docs/research-book/chapters/**', 'docs/research-book/engineering/**', 'docs/research-book/implementation/**', 'docs/research-book/sources/**', 'docs/research-book/appendices/**', 'docs/research-book/figures/**', 'docs/research-book/expansion/**', 'proofs/**', 'src/**', 'examples/aligned_strain.rs', 'tests/aligned_strain_contract.rs', 'tools/aligned_strain_oracle.py', 'tools/test_aligned_strain_oracle.py', 'tools/qualify_aligned_strain.py', 'tools/test_qualify_aligned_strain.py', 'rust-toolchain.toml', 'tools/setup_ci_rust.py', 'tools/test_setup_ci_rust.py', 'examples/static_obstacle.rs', 'examples/obstacle_flow.rs', 'tests/obstacle_flow_contract.rs', 'Cargo.*', 'evidence/bounded-physics-contracts/**', '.github/workflows/research-book-pages.yml']
workflow_dispatch:
permissions:
contents: read
Expand Down Expand Up @@ -145,9 +145,8 @@ jobs:
echo "RHEON_STRAIN=$RUNNER_TEMP/rheon-aligned-strain-records" >> "$GITHUB_ENV"
echo "RHEON_STRAIN_QUALIFICATION=$RUNNER_TEMP/rheon-aligned-strain-qualification" >> "$GITHUB_ENV"
echo "CARGO_TARGET_DIR=$RUNNER_TEMP/rheon-geometry-target" >> "$GITHUB_ENV"
echo "CARGO_HOME=$RUNNER_TEMP/rheon-cargo" >> "$GITHUB_ENV"
echo "$RUNNER_TEMP/rheon-cargo/bin" >> "$GITHUB_PATH"
echo "RUSTUP_HOME=$RUNNER_TEMP/rheon-rustup" >> "$GITHUB_ENV"
- name: Install isolated official pinned Rust toolchain
run: python3 tools/setup_ci_rust.py
- uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020
with:
node-version: '24'
Expand Down Expand Up @@ -202,7 +201,6 @@ jobs:
python3 -O -m unittest discover -s docs/education -p test_compressible_lab.py
- name: Construct and qualify only static geometry controls
run: |
rustup toolchain install 1.92.0 --profile minimal --no-self-update
mkdir -p "$RHEON_OBSTACLE"
cargo +1.92.0 build --locked --no-default-features --example static_obstacle
"$CARGO_TARGET_DIR/debug/examples/static_obstacle" "$RHEON_OBSTACLE/records.json"
Expand Down
18 changes: 10 additions & 8 deletions .github/workflows/rust-rheon.yml
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,8 @@ on:
- 'Cargo.toml'
- 'Cargo.lock'
- 'rust-toolchain.toml'
- 'tools/setup_ci_rust.py'
- 'tools/test_setup_ci_rust.py'
- 'src/**'
- 'tests/**'
- 'examples/**'
Expand Down Expand Up @@ -36,6 +38,8 @@ on:
- 'Cargo.toml'
- 'Cargo.lock'
- 'rust-toolchain.toml'
- 'tools/setup_ci_rust.py'
- 'tools/test_setup_ci_rust.py'
- 'src/**'
- 'tests/**'
- 'examples/**'
Expand Down Expand Up @@ -86,10 +90,7 @@ jobs:
with:
persist-credentials: false
- name: Select official pinned Rust toolchain
run: |
rustup toolchain install 1.92.0 --profile minimal --component rustfmt --component clippy
rustc --version --verbose
cargo --version
run: python3 tools/setup_ci_rust.py
- name: Strict lints for selected features
run: |
cargo clippy --locked ${{ matrix.flags }} --all-targets -- -D warnings
Expand All @@ -113,11 +114,12 @@ jobs:
run: |
python3 -m venv "$RUNNER_TEMP/rheon-tests"
"$RUNNER_TEMP/rheon-tests/bin/python" -m pip install --index-url https://pypi.org/simple --only-binary=:all: Pillow==12.3.0 numpy==2.3.5
- name: Select official pinned Rust toolchain
- name: Check isolated Rust bootstrap contracts
run: |
rustup toolchain install 1.92.0 --profile minimal --component rustfmt --component clippy
rustc --version --verbose
cargo --version
python3 -m unittest discover -s tools -p test_setup_ci_rust.py
python3 -O -m unittest discover -s tools -p test_setup_ci_rust.py
- name: Select official pinned Rust toolchain
run: python3 tools/setup_ci_rust.py
- name: Formatting
run: |
cargo fmt --all --check
Expand Down
149 changes: 149 additions & 0 deletions tools/setup_ci_rust.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,149 @@
#!/usr/bin/env python3
"""Install the repository's pinned Rust into fresh GitHub-job-owned homes.

Do not repair, copy, or invoke the runner image's Rust installation. A failed
setup publishes no usable tool environment; its disposable directory remains
under RUNNER_TEMP for diagnosis and runner cleanup. This is Ubuntu x86-64 CI
setup, not a hermetic build or a reproducibility claim.
"""
import hashlib
import os
from pathlib import Path
import platform
import re
import subprocess
import sys
import tempfile
import tomllib
import urllib.request


HOST = "x86_64-unknown-linux-gnu"
RUSTUP_VERSION = "1.28.2"
RUSTUP_INIT_URL = (
f"https://static.rust-lang.org/rustup/archive/{RUSTUP_VERSION}/{HOST}/rustup-init"
)
# Official adjacent .sha256, verified 2026-10-16; pin bootstrap bytes as well as
# the repository's rust-toolchain.toml version/components.
RUSTUP_INIT_SHA256 = "20a06e644b0d9bd2fbdbfd52d42540bdde820ea7df86e92e533c073da0cdd43c"
MAX_BOOTSTRAP_BYTES = 32 * 1024 * 1024
DIST_SERVER = "https://static.rust-lang.org"
UPDATE_ROOT = "https://static.rust-lang.org/rustup"


def download_bootstrap(destination):
with urllib.request.urlopen(RUSTUP_INIT_URL, timeout=60) as response:
data = response.read(MAX_BOOTSTRAP_BYTES + 1)
if len(data) > MAX_BOOTSTRAP_BYTES:
raise ValueError("rustup bootstrap exceeds download limit")
if hashlib.sha256(data).hexdigest() != RUSTUP_INIT_SHA256:
raise ValueError("rustup bootstrap SHA-256 mismatch")
destination.write_bytes(data)
destination.chmod(0o700)


def run(command, environment, capture=False):
result = subprocess.run(
[str(arg) for arg in command], env=environment, check=True,
capture_output=capture, text=True, timeout=900,
)
if capture:
print(result.stdout, end="", flush=True)
return result.stdout
return ""


def setup(repository, runner_temp, github_env, github_path):
if platform.system() != "Linux" or platform.machine() != "x86_64":
raise ValueError("CI Rust setup supports Linux x86-64 only")
configuration = tomllib.loads((repository / "rust-toolchain.toml").read_text())
toolchain = configuration["toolchain"]
channel = toolchain["channel"]
if (
not isinstance(channel, str)
or re.fullmatch(r"[0-9]+\.[0-9]+\.[0-9]+", channel) is None
or toolchain.get("profile") != "minimal"
or toolchain.get("components") != ["rustfmt", "clippy"]
or toolchain.keys() != {"channel", "profile", "components"}
):
raise ValueError("expected a pinned minimal Rust toolchain with rustfmt and clippy")
if not runner_temp.is_absolute() or not runner_temp.is_dir():
raise ValueError("RUNNER_TEMP must name an existing absolute directory")
# mkdtemp exclusively creates this job's root. Never reuse a predictable
# preexisting home or import runner toolchains/cache into it.
root = Path(tempfile.mkdtemp(prefix="rheon-ci-rust-", dir=runner_temp))
cargo_home = root / "cargo"
rustup_home = root / "rustup"
cargo_home.mkdir()
rustup_home.mkdir()
selected = f"{channel}-{HOST}"
environment = {
key: value for key, value in os.environ.items()
if not key.startswith(("CARGO", "RUST"))
}
environment.update({
"CARGO_HOME": str(cargo_home),
"RUSTUP_HOME": str(rustup_home),
"RUSTUP_TOOLCHAIN": selected,
"RUSTUP_DIST_SERVER": DIST_SERVER,
"RUSTUP_UPDATE_ROOT": UPDATE_ROOT,
# Download/install must not discover preinstalled runner Rust proxies.
"PATH": "/usr/bin:/bin",
})
bootstrap = root / "rustup-init"
print(f"Install official rustup {RUSTUP_VERSION} into {root}", flush=True)
download_bootstrap(bootstrap)
run([
bootstrap, "-y", "--no-modify-path", "--default-host", HOST,
"--default-toolchain", "none", "--profile", "minimal",
], environment)
rustup = cargo_home / "bin/rustup"
run([
rustup, "toolchain", "install", selected, "--profile", "minimal",
"--component", "rustfmt", "--component", "clippy", "--no-self-update",
], environment)
installed = set(run([
rustup, "component", "list", "--installed", "--toolchain", selected,
], environment, capture=True).splitlines())
required = {f"{component}-{HOST}" for component in ("rustc", "cargo", "rust-std", "rustfmt", "clippy")}
if not required <= installed:
raise ValueError("installed Rust toolchain lacks required components")
# Use the newly installed proxies explicitly, including their +toolchain
# support needed by existing book build commands.
binaries = cargo_home / "bin"
rustc = run([binaries / "rustc", "--version", "--verbose"], environment, capture=True)
cargo = run([binaries / "cargo", "--version"], environment, capture=True)
if f"release: {channel}" not in rustc.splitlines() or not cargo.startswith(f"cargo {channel} "):
raise ValueError("installed Rust/Cargo version differs from repository pin")
run([binaries / "rustfmt", "--version"], environment, capture=True)
run([binaries / "cargo-clippy", "--version"], environment, capture=True)
published = {key: environment[key] for key in (
"CARGO_HOME", "RUSTUP_HOME", "RUSTUP_TOOLCHAIN", "RUSTUP_DIST_SERVER", "RUSTUP_UPDATE_ROOT",
)}
# Export only after download, installation, versions, and components pass.
with github_env.open("a", encoding="utf-8") as output:
for key, value in published.items():
if "\n" in value or "\r" in value:
raise ValueError("invalid CI tool path")
output.write(f"{key}={value}\n")
with github_path.open("a", encoding="utf-8") as output:
output.write(f"{binaries}\n")
return published


def main():
try:
setup(
Path(__file__).resolve().parent.parent,
Path(os.environ["RUNNER_TEMP"]),
Path(os.environ["GITHUB_ENV"]),
Path(os.environ["GITHUB_PATH"]),
)
except (OSError, ValueError, KeyError, subprocess.SubprocessError) as error:
print(f"CI Rust setup failed: {error}", file=sys.stderr)
return 1
return 0


if __name__ == "__main__":
sys.exit(main())
Loading
Loading