Skip to content

Commit 5f74e88

Browse files
committed
build: fix native CI provisioning and platform test lints
Route CI through the shared pinned installers, install NASM for Windows assembly, and omit tools that the CI checks do not use. Retain complete native and portable test coverage, with script regressions on Linux x86-64. Preserve backend fixture bytes with explicit byte extraction and strict slice bounds, and document the existing unsafe test-helper requirements.
1 parent 516027c commit 5f74e88

27 files changed

Lines changed: 339 additions & 97 deletions

.config/tooling.toml

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -32,13 +32,22 @@ powershell = "7.6.5"
3232
rustup = "1.29.1"
3333
python = "3.14.7"
3434
zig = "0.16.0"
35+
nasm = "3.02"
3536

3637
[linux]
3738
ubuntu = "26.04"
3839
codename = "resolute"
3940
snapshot = "20260906T054640Z"
4041
packages = ["build-essential", "ca-certificates", "curl", "git", "jq", "pkgconf", "python3", "python3-venv", "unzip", "xz-utils", "zstd", "ninja-build", "libssl-dev", "perl", "m4", "shellcheck"]
4142

43+
[ci]
44+
cargo = ["just", "cargo-nextest", "cargo-deny", "cargo-audit"]
45+
46+
[linux-ci]
47+
ubuntu = "24.04"
48+
codename = "noble"
49+
packages = ["build-essential", "ca-certificates", "curl", "git", "jq", "pkgconf", "python3", "cmake", "clang", "libclang-dev", "libssl-dev", "perl"]
50+
4251
[windows]
4352
visual-studio = "18.9.2"
4453
build-version = "18.9.12120.119"
@@ -166,6 +175,10 @@ cargo = ["cargo-audit", "cargo-deny", "cargo-nextest", "cargo-mutants", "just",
166175
url = "https://static.rust-lang.org/rustup/archive/1.29.1/x86_64-pc-windows-msvc/rustup-init.exe"
167176
sha256 = "6f4bef66261261fcb43131be8720bab817d403a09edec7455c371974b90bdb7e"
168177

178+
[x86_64-win.assets.nasm]
179+
url = "https://www.nasm.us/pub/nasm/releasebuilds/3.02/win64/nasm-3.02-win64.zip"
180+
sha256 = "161d0bfaff53c2f9e9f3e69fd0672323ebabafd1268976a5cec11be92a19aee7"
181+
169182
[x86_64-win.assets.cargo-binstall]
170183
url = "https://github.com/cargo-bins/cargo-binstall/releases/download/v1.23.0/cargo-binstall-x86_64-pc-windows-msvc.zip"
171184
sha256 = "f4641479477aca40387e88297e3813fab8e44a8d21f25faa44e0ff33e2bc1726"

.github/workflows/ci.yml

Lines changed: 7 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -48,27 +48,15 @@ jobs:
4848
- name: Install tooling
4949
env:
5050
PLATFORM: ${{ matrix.platform }}
51+
run: scripts/tooling/"$PLATFORM.sh" --ci
52+
- name: Test script orchestration
53+
if: matrix.platform == 'x86_64-linux'
5154
run: |
52-
sudo apt-get update
53-
sudo apt-get install -y build-essential ca-certificates python3 cmake clang libclang-dev libssl-dev pkg-config jq
54-
installer_dir="$(mktemp -d)"
55-
installer="$installer_dir/rustup-init"
56-
python3 scripts/tooling/catalog.py download "$PLATFORM" rustup "$installer"
57-
chmod +x "$installer"
58-
"$installer" -y --no-modify-path --default-toolchain none
59-
rm "$installer"
60-
rmdir "$installer_dir"
61-
export PATH="$HOME/.cargo/bin:$PATH"
62-
host="$(python3 scripts/tooling/catalog.py get "$PLATFORM" rust-host)"
63-
python3 scripts/lib/toolchain.py --install "$host"
64-
export RUSTUP_TOOLCHAIN
65-
RUSTUP_TOOLCHAIN="$(python3 scripts/lib/toolchain.py --target "$host")"
66-
for tool in just cargo-nextest cargo-deny cargo-audit; do
67-
cargo install "$tool" --version "$(python3 scripts/tooling/catalog.py get cargo "$tool")" --locked
68-
done
55+
source "$HOME/.local/share/rscrypto-tooling/environment.sh"
56+
just test-scripts
6957
- name: Check and test
7058
run: |
71-
export PATH="$HOME/.cargo/bin:$PATH"
59+
source "$HOME/.local/share/rscrypto-tooling/environment.sh"
7260
just ci-check
7361
just test --all
7462
just test --all --portable
@@ -85,7 +73,7 @@ jobs:
8573
- name: Install tooling, check, and test
8674
shell: powershell
8775
run: |
88-
./scripts/tooling/x86_64-win.ps1
76+
./scripts/tooling/x86_64-win.ps1 -Ci
8977
just ci-check
9078
if ($LASTEXITCODE -ne 0) { exit $LASTEXITCODE }
9179
just test --all

justfile

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -124,6 +124,7 @@ test-scripts:
124124
@scripts/lib/python.sh scripts/test/test_runner_test.py
125125
@scripts/lib/python.sh scripts/test/just_arguments_test.py
126126
@scripts/lib/python.sh scripts/tooling/toolchain_test.py
127+
@scripts/lib/python.sh scripts/tooling/install_test.py
127128
@scripts/lib/python.sh scripts/test/fuzz_features_test.py
128129
@scripts/lib/python.sh scripts/check/check_runner_test.py
129130
@scripts/lib/python.sh scripts/test/fuzz_runner_test.py

scripts/README.md

Lines changed: 19 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -138,7 +138,25 @@ The installers use sudo when needed. Windows uses the corresponding
138138
`aarch64-win.ps1` or `x86_64-win.ps1` in an elevated PowerShell session.
139139
macOS tools remain locally managed.
140140

141-
All profiles install the prerequisites for `just ci-check`, `just test`, and
141+
CI calls these same installers with `--ci` on Linux or `-Ci` on Windows.
142+
The catalog's `ci` section selects the Cargo tools needed by `just ci-check`,
143+
`just test --all`, and `just test --all --portable`. Both test commands include
144+
doctests. This mode omits Cargo Rail because `--all` bypasses affected-work
145+
planning; use the full installer for ordinary `just test` and benchmark work.
146+
Linux CI uses the catalog's `linux-ci` Ubuntu release and packages from the
147+
same archive snapshot as development provisioning. It installs prebuilt Cargo
148+
tools on x86-64 and ARM64, and builds them from source on the other architectures.
149+
CI does not install optional profiling, mutation, or live-fuzzing tools or alter
150+
shell startup files. These jobs validate CI provisioning, not the full optional
151+
development toolset.
152+
153+
After Linux installation, source
154+
`$HOME/.local/share/rscrypto-tooling/environment.sh` in each new CI step.
155+
Windows CI runs installation and validation in one PowerShell step to retain
156+
the MSVC/SDK environment. Windows x86-64 installs catalog-pinned NASM for native
157+
dependency assembly in both modes.
158+
159+
All full profiles install the prerequisites for `just ci-check`, `just test`, and
142160
Criterion `just bench`. RISC-V, Z, and POWER install pinned Cargo tools from
143161
source and use snapshot-pinned native CMake/Clang. They do not install cross
144162
targets, Miri, browsers, or profiling tools. The shared selector in

scripts/tooling/aarch64-win.ps1

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
1+
param([switch]$Ci)
12
$ErrorActionPreference = 'Stop'
2-
if ($args.Count -ne 0) { throw 'Usage: scripts/tooling/aarch64-win.ps1' }
3-
& "$PSScriptRoot/windows.ps1" -Platform aarch64-win
3+
if ($args.Count -ne 0) { throw 'Usage: scripts/tooling/aarch64-win.ps1 [-Ci]' }
4+
& "$PSScriptRoot/windows.ps1" -Platform aarch64-win -Ci:$Ci

scripts/tooling/catalog.py

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -89,6 +89,11 @@ def install_archive(name, asset, prefix):
8989

9090

9191
def validate(data):
92+
required_ci = {'just', 'cargo-nextest', 'cargo-deny', 'cargo-audit'}
93+
if not required_ci <= set(data['ci']['cargo']):
94+
raise ValueError('CI: missing check/test tools')
95+
if any(tool not in data['cargo'] for tool in data['ci']['cargo']):
96+
raise ValueError('CI: missing Cargo tool version')
9297
for platform in PLATFORMS:
9398
config = data[platform]
9499
if 'miri' in config['components']:
@@ -102,6 +107,8 @@ def validate(data):
102107
assets = config['assets']
103108
if 'rustup' not in assets:
104109
raise ValueError(f'{platform}: missing native rustup archive')
110+
if platform == 'x86_64-win' and 'nasm' not in assets:
111+
raise ValueError(f'{platform}: missing NASM for native dependency assembly')
105112
if platform not in NATIVE_SOURCE_PLATFORMS and not {'cargo-rail', 'cargo-binstall', 'cmake', 'llvm'} <= assets.keys():
106113
raise ValueError(f'{platform}: missing native tool archives')
107114
for name, asset in assets.items():

scripts/tooling/install_test.py

Lines changed: 111 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,111 @@
1+
#!/usr/bin/env python3
2+
"""Exercise Linux provisioning with substitute host and installation commands."""
3+
import json
4+
import os
5+
from pathlib import Path
6+
import shutil
7+
import subprocess
8+
import sys
9+
import tempfile
10+
import tomllib
11+
import unittest
12+
13+
ROOT = Path(__file__).resolve().parents[2]
14+
BASH = shutil.which('bash')
15+
CATALOG = tomllib.loads((ROOT / '.config/tooling.toml').read_text())
16+
17+
STUB = r'''import json, os, pathlib, subprocess, sys
18+
name = pathlib.Path(sys.argv[0]).name
19+
args = sys.argv[1:]
20+
with open(os.environ['INSTALL_LOG'], 'a') as log:
21+
log.write(json.dumps([name, *args]) + '\n')
22+
if name == 'uname':
23+
print('Linux' if args == ['-s'] else os.environ['INSTALL_ARCH'])
24+
elif name == 'id':
25+
print('0')
26+
elif name == 'apt-cache':
27+
print(args[-1] + ' | 1.0 | snapshot')
28+
elif name == 'apt-get':
29+
if os.environ.get('INSTALL_FAIL_APT'):
30+
sys.exit(42)
31+
elif name == 'python3':
32+
script = pathlib.Path(args[0]).name
33+
if script == 'catalog.py' and args[1] == 'download':
34+
pathlib.Path(args[-1]).write_text('#!/bin/sh\nexit 0\n')
35+
elif script == 'catalog.py' and args[1] == 'install-archive':
36+
directory = pathlib.Path(args[-1]) / args[-2]
37+
directory.mkdir(parents=True, exist_ok=True)
38+
executable = directory / 'cargo-binstall'
39+
executable.write_text('#!/bin/sh\nexit 0\n')
40+
executable.chmod(0o755)
41+
print(directory)
42+
elif script == 'toolchain.py' and '--install' in args:
43+
pass
44+
else:
45+
sys.exit(subprocess.run([sys.executable, *args]).returncode)
46+
'''
47+
48+
49+
class LinuxInstall(unittest.TestCase):
50+
def provision(self, platform, fail=False):
51+
temporary = tempfile.TemporaryDirectory(prefix='rscrypto installer ')
52+
self.addCleanup(temporary.cleanup)
53+
root = Path(temporary.name)
54+
binaries = root / 'bin'
55+
binaries.mkdir()
56+
for name in ('uname', 'id', 'apt-get', 'apt-cache', 'cargo', 'clang', 'cmake', 'python3'):
57+
script = binaries / name
58+
script.write_text('#!' + sys.executable + '\n' + STUB)
59+
script.chmod(0o755)
60+
bash_env = root / 'bash-env'
61+
bash_env.write_text('''source() {
62+
if [[ "$1" == /etc/os-release ]]; then
63+
ID=ubuntu
64+
VERSION_ID=24.04
65+
PRETTY_NAME=fixture
66+
else
67+
builtin source "$@"
68+
fi
69+
}
70+
''')
71+
arch = platform.removesuffix('-linux').replace('powerpc64le', 'ppc64le')
72+
env = {**os.environ, 'HOME': str(root), 'CARGO_HOME': str(root / 'custom cargo'),
73+
'PATH': str(binaries) + os.pathsep + os.environ['PATH'],
74+
'BASH_ENV': str(bash_env), 'INSTALL_ARCH': arch,
75+
'INSTALL_LOG': str(root / 'commands.jsonl')}
76+
if fail:
77+
env['INSTALL_FAIL_APT'] = '1'
78+
result = subprocess.run([BASH, str(ROOT / 'scripts/tooling/linux.sh'), platform, '--ci'],
79+
env=env, capture_output=True, text=True)
80+
calls = [json.loads(line) for line in (root / 'commands.jsonl').read_text().splitlines()]
81+
return result, calls, root
82+
83+
def test_ci_profiles_install_only_required_tools(self):
84+
for platform in ('x86_64-linux', 'aarch64-linux', 's390x-linux', 'powerpc64le-linux', 'riscv64-linux'):
85+
with self.subTest(platform=platform):
86+
result, calls, root = self.provision(platform)
87+
self.assertEqual(result.returncode, 0, result.stdout + result.stderr)
88+
installs = [c for c in calls if c[0] == 'cargo' and ('install' in c or 'binstall' in c)]
89+
self.assertEqual(len(installs), len(CATALOG['ci']['cargo']))
90+
binary = platform in ('x86_64-linux', 'aarch64-linux')
91+
for command, tool in zip(installs, CATALOG['ci']['cargo']):
92+
self.assertIn('binstall' if binary else 'install', command)
93+
self.assertEqual(command[-1], f"{tool}@{CATALOG['cargo'][tool]}" if binary else tool)
94+
self.assertIn('--locked', command)
95+
self.assertIn(CATALOG[platform]['rust-host'], command)
96+
apt = next(c for c in calls if c[0] == 'apt-get' and '--allow-downgrades' in c)
97+
self.assertEqual([a for a in apt if a.endswith('=1.0')],
98+
[p + '=1.0' for p in CATALOG['linux-ci']['packages']])
99+
environment = (root / '.local/share/rscrypto-tooling/environment.sh').read_text()
100+
self.assertIn('custom\\ cargo/bin', environment)
101+
self.assertFalse((root / '.bashrc').exists())
102+
self.assertFalse((root / '.profile').exists())
103+
104+
def test_package_failure_stops_before_rust_installation(self):
105+
result, calls, _ = self.provision('x86_64-linux', fail=True)
106+
self.assertEqual(result.returncode, 42)
107+
self.assertFalse(any(c[0] == 'cargo' or 'download' in c for c in calls))
108+
109+
110+
if __name__ == '__main__':
111+
unittest.main()

scripts/tooling/linux.sh

Lines changed: 44 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,9 @@ export PYTHONDONTWRITEBYTECODE=1
55

66
platform="${1:?native platform is required}"
77
shift
8-
[[ "$#" -eq 0 ]] || { echo "usage: scripts/tooling/$platform.sh" >&2; exit 64; }
8+
ci=false
9+
if [[ "${1:-}" == --ci ]]; then ci=true; shift; fi
10+
[[ "$#" -eq 0 ]] || { echo "usage: scripts/tooling/$platform.sh [--ci]" >&2; exit 64; }
911
machine="${platform%-linux}"
1012
[[ "$machine" != powerpc64le ]] || machine=ppc64le
1113
case "$platform" in
@@ -21,9 +23,11 @@ cd "$REPO_ROOT"
2123
# Ubuntu Server supplies Python; install it from the selected archive if absent.
2224
# Read only the two bootstrap strings before the full TOML reader is available.
2325
catalog="$REPO_ROOT/.config/tooling.toml"
24-
bootstrap_value() { sed -n '/^\[linux\]$/,/^\[/s/^'"$1"' = "\([^"]*\)"$/\1/p' "$catalog"; }
26+
linux_section=linux
27+
[[ "$ci" == false ]] || linux_section=linux-ci
28+
bootstrap_value() { sed -n '/^\['"${2:-$linux_section}"'\]$/,/^\[/s/^'"$1"' = "\([^"]*\)"$/\1/p' "$catalog"; }
2529
ubuntu="$(bootstrap_value ubuntu)"
26-
snapshot="$(bootstrap_value snapshot)"
30+
snapshot="$(bootstrap_value snapshot linux)"
2731
# shellcheck source=/dev/null
2832
source /etc/os-release
2933
[[ "$ID" == ubuntu && "$VERSION_ID" == "$ubuntu" ]] || {
@@ -53,9 +57,11 @@ apt=("${sudo_cmd[@]}" env DEBIAN_FRONTEND=noninteractive apt-get "${apt_options[
5357
"${apt[@]}" update
5458
catalog_get() { python3 "$SCRIPT_DIR/catalog.py" get "$@"; }
5559
python3 "$SCRIPT_DIR/catalog.py" validate
56-
mapfile -t packages < <(catalog_get linux packages)
57-
mapfile -t native_packages < <(catalog_get "$platform" packages)
58-
packages+=("${native_packages[@]}")
60+
mapfile -t packages < <(catalog_get "$linux_section" packages)
61+
if [[ "$ci" == false ]]; then
62+
mapfile -t native_packages < <(catalog_get "$platform" packages)
63+
packages+=("${native_packages[@]}")
64+
fi
5965
# Exact candidates come from the selected snapshot, including repeat installations.
6066
pinned_packages=()
6167
for package in "${packages[@]}"; do
@@ -66,28 +72,41 @@ done
6672
"${apt[@]}" install -y --allow-downgrades "${pinned_packages[@]}"
6773

6874
prefix="$HOME/.local/share/rscrypto-tooling"
69-
mkdir -p "$prefix/bin"
75+
mkdir -p "$prefix"
7076
python3 "$SCRIPT_DIR/catalog.py" download "$platform" rustup "$temporary/rustup-init"
7177
chmod +x "$temporary/rustup-init"
7278
host="$(catalog_get "$platform" rust-host)"
7379
channel="$(python3 "$SCRIPT_DIR/../lib/toolchain.py")"
7480
"$temporary/rustup-init" -y --no-modify-path --default-host "$host" --default-toolchain none
75-
export PATH="$HOME/.cargo/bin:$PATH"
76-
mapfile -t components < <(catalog_get "$platform" components)
81+
cargo_bin="${CARGO_HOME:-$HOME/.cargo}/bin"
82+
export PATH="$cargo_bin:$PATH"
83+
components=()
84+
if [[ "$ci" == false ]]; then mapfile -t components < <(catalog_get "$platform" components); fi
7785
component_args=()
7886
for component in "${components[@]}"; do component_args+=(--component "$component"); done
7987
python3 "$SCRIPT_DIR/../lib/toolchain.py" --install "$host" "${component_args[@]}"
8088
export RUSTUP_TOOLCHAIN="$channel"
8189
# Archive tools retain their complete directory layouts, including LLVM and Zig libraries.
82-
python3 "$SCRIPT_DIR/catalog.py" install-archives "$platform" "$prefix" > "$temporary/archives"
90+
if [[ "$ci" == true ]]; then
91+
: > "$temporary/archives"
92+
if [[ "$platform" == aarch64-linux || "$platform" == x86_64-linux ]]; then
93+
directory="$(python3 "$SCRIPT_DIR/catalog.py" install-archive "$platform" cargo-binstall "$prefix")"
94+
printf 'cargo-binstall\t%s\n' "$directory" > "$temporary/archives"
95+
fi
96+
else
97+
python3 "$SCRIPT_DIR/catalog.py" install-archives "$platform" "$prefix" > "$temporary/archives"
98+
fi
8399
tool_paths=()
84100
while IFS=$'\t' read -r name directory; do
85101
if [[ -d "$directory/bin" ]]; then tool_paths+=("$directory/bin"); else tool_paths+=("$directory"); fi
86102
if [[ "$name" == llvm ]]; then export LIBCLANG_PATH="$directory/lib"; fi
87103
done < "$temporary/archives"
88-
PATH="$(IFS=:; echo "${tool_paths[*]}"):$prefix/bin:$HOME/.cargo/bin:$PATH"
89-
export PATH
90-
mapfile -t cargo_tools < <(catalog_get "$platform" cargo)
104+
tool_paths+=("$cargo_bin")
105+
path_prefix="$(IFS=:; echo "${tool_paths[*]}")"
106+
export PATH="$path_prefix:$PATH"
107+
tool_section="$platform"
108+
[[ "$ci" == false ]] || tool_section=ci
109+
mapfile -t cargo_tools < <(catalog_get "$tool_section" cargo)
91110
for tool in "${cargo_tools[@]}"; do
92111
version="$(catalog_get cargo "$tool")"
93112
# Cargo's install registry verifies exact installed package versions on reruns.
@@ -99,23 +118,25 @@ for tool in "${cargo_tools[@]}"; do
99118
cargo +"$channel" install --locked --target "$(catalog_get "$platform" rust-host)" --version "$version" "$tool"
100119
fi
101120
done
102-
if [[ "$platform" != aarch64-linux && "$platform" != x86_64-linux ]]; then
121+
if [[ "$ci" == false && "$platform" != aarch64-linux && "$platform" != x86_64-linux ]]; then
103122
env -u RUSTC_WRAPPER -u CARGO_ENCODED_RUSTFLAGS \
104123
cargo +"$channel" install --locked --target "$(catalog_get "$platform" rust-host)" --version "$(catalog_get versions cargo-rail)" cargo-rail
105124
fi
106125

107126
# Persistent paths are shared by interactive shells and non-interactive Bash recipes.
108127
environment="$prefix/environment.sh"
109128
{
110-
printf "export PATH=%q:\"\$PATH\"\n" "$(IFS=:; echo "${tool_paths[*]}"):$prefix/bin:$HOME/.cargo/bin"
129+
printf "export PATH=%q:\"\$PATH\"\n" "$path_prefix"
111130
if [[ -n "${LIBCLANG_PATH:-}" ]]; then printf 'export LIBCLANG_PATH=%q\n' "$LIBCLANG_PATH"; fi
112131
} > "$environment"
113-
for startup in "$HOME/.profile" "$HOME/.bashrc"; do
114-
line="source \"$environment\""
115-
touch "$startup"
116-
grep -Fxq "$line" "$startup" || printf '\n%s\n' "$line" >> "$startup"
117-
done
118-
if [[ "$platform" == aarch64-linux || "$platform" == x86_64-linux ]]; then
132+
if [[ "$ci" == false ]]; then
133+
for startup in "$HOME/.profile" "$HOME/.bashrc"; do
134+
line="source \"$environment\""
135+
touch "$startup"
136+
grep -Fxq "$line" "$startup" || printf '\n%s\n' "$line" >> "$startup"
137+
done
138+
fi
139+
if [[ "$ci" == false && ( "$platform" == aarch64-linux || "$platform" == x86_64-linux ) ]]; then
119140
# Linux profiling is available to the runner account, including non-root perf/samply.
120141
"${sudo_cmd[@]}" tee /etc/sysctl.d/99-rscrypto-profiling.conf >/dev/null <<'CONF'
121142
kernel.perf_event_paranoid = -1
@@ -132,6 +153,6 @@ samply --version
132153
fi
133154
clang --version
134155
cmake --version
135-
cargo rail --version
156+
if [[ "$ci" == false ]]; then cargo rail --version; fi
136157
cargo nextest --version
137-
printf 'Installed %s tooling. New shells load %s.\n' "$platform" "$environment"
158+
printf 'Installed %s tooling. Load with: source "%s"\n' "$platform" "$environment"

0 commit comments

Comments
 (0)