From a7df7b52dc83d24f090aacb681665980b3c64f7b Mon Sep 17 00:00:00 2001 From: joshlf's Agent Date: Mon, 24 Aug 2026 22:37:12 +0000 Subject: [PATCH] [ci] Pin and test repository tools Build repository tools with the exact stable compiler recorded in tools/rust-toolchain.toml and with the checked-in lockfile. Pass the pin to Cargo explicitly and export it for descendant Cargo processes, so neither RUSTUP_TOOLCHAIN nor a persisted rustup directory override can silently select another compiler. Share the Unix bootstrap recognizer between the wrapper and its validation check. Scope each exact assignment to its exact TOML table. Treat only canonical bare table headers as declarations. Reject quoted, mixed, spaced, commented, and array-table aliases of the same semantic path without confusing one quoted component whose literal name contains dots. Validate the fixed table and key inputs before constructing the recognizer. Accept LF and well-formed CRLF, but reject alternate assignments, noncanonical or duplicate tables, multiline strings, NUL bytes, bare carriage returns, unterminated lines, unreadable input, and missing or duplicate declarations. Keep the independent Windows recognizer coordinated with the Unix contract. Require the complete three-line semantic file shape, reject command-interpreter metacharacters before parsing, and avoid CALL's second expansion pass. A future file-format change must update both recognizers deliberately. Run README generation from the tools workspace with the same compiler pin and locked dependencies. Export the pin to its nested cargo-readme process, and report a failed child command before printing any generated framing so redirected output cannot look like a partial regeneration. Document the working and source directories together. Update the tools compiler in the weekly stable roller so the tool and library build paths cannot drift accidentally. Add stable to the serial pre-push bootstrap when the tools check first makes it necessary. Run the complete tools workspace test suite in CI and before each push. Validate that the tools compiler still matches the stable CI compiler, and exercise hostile parser inputs and nested command failure. Tests: ./githooks/pre-push Tests: hostile RUSTUP_TOOLCHAIN ./ci/check_tools.sh Tests: hostile RUSTUP_TOOLCHAIN ./zerocopy/ci/check_readme.sh gherrit-pr-id: Gvc5xhb5dgtwxc6zn6aycox7svxcbv3by --- .gitattributes | 6 + .github/workflows/ci.yml | 12 +- .../roll-pinned-toolchain-versions.yml | 26 ++ ci/check_tools.sh | 232 ++++++++++++++++++ githooks/pre-push | 18 +- githooks/test_pre_push.py | 13 +- tools/cargo.sh | 47 ++++ tools/generate-readme/src/main.rs | 62 ++++- tools/rust-toolchain.toml | 23 ++ tools/toolchain.sh | 187 ++++++++++++++ zerocopy/AGENTS.md | 3 +- zerocopy/cargo.sh | 16 +- zerocopy/ci/check_readme.sh | 22 +- zerocopy/src/lib.rs | 3 +- zerocopy/win-cargo.bat | 128 ++++++++-- 15 files changed, 752 insertions(+), 46 deletions(-) create mode 100644 .gitattributes create mode 100755 ci/check_tools.sh create mode 100755 tools/cargo.sh create mode 100644 tools/rust-toolchain.toml create mode 100644 tools/toolchain.sh diff --git a/.gitattributes b/.gitattributes new file mode 100644 index 0000000000..4021947934 --- /dev/null +++ b/.gitattributes @@ -0,0 +1,6 @@ +# Shell entry points must remain LF so they can run before Rust tooling exists. +# The shared bootstrap parser still accepts well-formed CRLF in the two TOML +# files needed by an existing Windows worktree. Keep these rules coordinated +# with `tools/toolchain.sh` and `ci/check_tools.sh`. +*.sh text eol=lf +githooks/pre-push text eol=lf diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 3c5d70bf7c..cee70f0857 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -824,6 +824,16 @@ jobs: - name: Check Rust formatting run: ./ci/check_fmt.sh + check_tools: + runs-on: ubuntu-latest + name: Check repository tools + steps: + - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 + with: + persist-credentials: false + - name: Test repository tools + run: ./ci/check_tools.sh + check_stale_stderr: runs-on: ubuntu-latest name: Check stale stderr files @@ -1048,7 +1058,7 @@ jobs: # success in branch protection, so this aggregation must fail closed. if: ${{ always() }} runs-on: ubuntu-latest - needs: [build_test, miri, codegen, coverage, kani, check_be_aarch64, check_avr_atmega, check_fmt, check_actions, check_readme, check_versions, check_msrv_is_minimal, check_stale_stderr, check-all-toolchains-tested, check-job-dependencies, check-todo, run-git-hooks, zizmor, build_docker_env] + needs: [build_test, miri, codegen, coverage, kani, check_be_aarch64, check_avr_atmega, check_fmt, check_tools, check_actions, check_readme, check_versions, check_msrv_is_minimal, check_stale_stderr, check-all-toolchains-tested, check-job-dependencies, check-todo, run-git-hooks, zizmor, build_docker_env] steps: - name: Reject workflow cancellation if: ${{ cancelled() }} diff --git a/.github/workflows/roll-pinned-toolchain-versions.yml b/.github/workflows/roll-pinned-toolchain-versions.yml index 0a795c44dd..fb89495933 100644 --- a/.github/workflows/roll-pinned-toolchain-versions.yml +++ b/.github/workflows/roll-pinned-toolchain-versions.yml @@ -114,6 +114,31 @@ jobs: ./tools/update-expected-test-output.sh } + function update-tools-toolchain { + VERSION="$1" + + # `zerocopy/cargo.sh` builds repository tools from the `tools` + # directory, so rustup selects this pin before cargo-zerocopy can + # read the library's Cargo metadata. Keep this explicit copy + # coordinated with the stable pin above. This explicit update and + # the exact-match checks make a future file reorganization fail + # visibly instead of silently leaving the tools compiler behind. + REGEX='^channel = "[0-9]+\.[0-9]+\.[0-9]+"$' + MATCH_COUNT="$( + grep -Ec "$REGEX" tools/rust-toolchain.toml || true + )" + if [ "$MATCH_COUNT" -ne 1 ]; then + echo "Expected exactly one line matching '$REGEX' in" \ + "tools/rust-toolchain.toml; found $MATCH_COUNT" >&2 + exit 1 + fi + sed -i -E \ + "s/$REGEX/channel = \"$VERSION\"/" \ + tools/rust-toolchain.toml + grep -Fx "channel = \"$VERSION\"" \ + tools/rust-toolchain.toml >/dev/null + } + if [ "$TOOLCHAIN" == stable ]; then STABLE_VERSION="$( rustc +stable --version --verbose \ @@ -123,6 +148,7 @@ jobs: echo "Could not determine the latest stable rustc version" >&2 exit 1 fi + update-tools-toolchain "$STABLE_VERSION" update-pinned-version stable "$STABLE_VERSION" \ '--features __internal_use_only_features_that_work_on_stable' diff --git a/ci/check_tools.sh b/ci/check_tools.sh new file mode 100755 index 0000000000..640948a13a --- /dev/null +++ b/ci/check_tools.sh @@ -0,0 +1,232 @@ +#!/usr/bin/env bash +# +# Copyright 2026 The Fuchsia Authors +# +# Licensed under a BSD-style license , Apache License, Version 2.0 +# , or the MIT +# license , at your option. +# This file may not be copied, modified, or distributed except according to +# those terms. + +set -eo pipefail +cd "$(dirname "$0")/../tools" + +# shellcheck source=../tools/toolchain.sh +source ./toolchain.sh + +# The stable-toolchain roller intentionally updates these two files together. +# Check the copy here as well so a hand edit cannot make local tool builds use +# a different compiler from the stable CI lane. Require one exact declaration +# in each file; a future reorganization must update this check explicitly. +# `tools/cargo.sh` and `zerocopy/win-cargo.bat` also parse the tools channel so +# they can defeat persisted rustup directory overrides. The Unix scripts share +# `tools/toolchain.sh`; keep its exact format coordinated with the independent +# batch parser. A pre-existing Windows worktree may still contain CRLF TOML, so +# the bootstrap parser accepts well-formed CRLF but rejects bare CR. +# +# Store the parser output before comparing the two pins. Do not use `mapfile`: +# macOS still ships Bash 3.2, which does not provide that builtin. +if ! TOOLS_CHANNEL="$( + zc_read_exact_rust_version_assignment rust-toolchain.toml toolchain channel +)"; then + echo "Expected one canonical [toolchain] channel" \ + "in tools/rust-toolchain.toml" >&2 + exit 1 +fi +if ! STABLE_CHANNEL="$( + zc_read_exact_rust_version_assignment \ + ../zerocopy/Cargo.toml package.metadata.ci pinned-stable +)"; then + echo "Expected one canonical [package.metadata.ci] pinned-stable" \ + "in zerocopy/Cargo.toml" >&2 + exit 1 +fi +if [[ -z "$TOOLS_CHANNEL" || "$TOOLS_CHANNEL" == *$'\n'* || \ + -z "$STABLE_CHANNEL" || "$STABLE_CHANNEL" == *$'\n'* ]]; then + echo "Expected one tools channel and one Zerocopy stable channel" >&2 + exit 1 +fi +if [[ "$TOOLS_CHANNEL" != "$STABLE_CHANNEL" ]]; then + echo "Tools compiler $TOOLS_CHANNEL does not match Zerocopy stable" \ + "compiler $STABLE_CHANNEL" >&2 + exit 1 +fi + +# Exercise the bootstrap parser independently of this checkout. Existing +# Git-for-Windows worktrees can retain CRLF after pulling `.gitattributes`, but +# every table and assignment choice is otherwise a strict coordination +# contract. Keep these hostile fixtures synchronized with `tools/toolchain.sh` +# and the independent live-file parser in `zerocopy/win-cargo.bat`. +# +# The parser opens its input twice so it can detect NUL before Bash `read` +# discards that byte. Materialize each fixture instead of piping through +# /dev/stdin; production callers likewise provide ordinary repository files. +TOOLS_CHANNEL_FIXTURE_DIR="$(mktemp -d)" +TOOLS_CHANNEL_FIXTURE="$TOOLS_CHANNEL_FIXTURE_DIR/rust-toolchain.toml" +cleanup_tools_channel_fixture() { + rm -f "$TOOLS_CHANNEL_FIXTURE" + rmdir "$TOOLS_CHANNEL_FIXTURE_DIR" +} +trap cleanup_tools_channel_fixture EXIT + +write_tools_channel_fixture() { + printf '%s' "$1" > "$TOOLS_CHANNEL_FIXTURE" +} + +write_tools_channel_fixture $'# channel = "ignored"\n[toolchain]\nchannel = "1.2.3"\n' +if ! PARSED_LF_CHANNEL="$( + zc_read_exact_rust_version_assignment \ + "$TOOLS_CHANNEL_FIXTURE" toolchain channel +)" || [[ "$PARSED_LF_CHANNEL" != "1.2.3" ]]; then + echo "Tools channel parser rejected a canonical LF declaration" >&2 + exit 1 +fi +write_tools_channel_fixture \ + $'[toolchain]\r\nchannel = "1.2.3"\r\nnote = "write \\"unsafe\\""\r\nprofile = "minimal"\r\n' +if ! PARSED_CRLF_CHANNEL="$( + zc_read_exact_rust_version_assignment \ + "$TOOLS_CHANNEL_FIXTURE" toolchain channel +)" || [[ "$PARSED_CRLF_CHANNEL" != "1.2.3" ]]; then + echo "Tools channel parser rejected a canonical CRLF declaration" >&2 + exit 1 +fi + +CANONICAL_TOOLCHAIN=$'[toolchain]\nchannel = "1.2.3"\n' + +# A quoted key containing a literal dot is one TOML component, not a dotted +# path. Keep this accepted fixture coordinated with the segment-aware alias +# check in `tools/toolchain.sh`; flattening quotes before comparison would +# incorrectly reject this unrelated table. +write_tools_channel_fixture \ + "${CANONICAL_TOOLCHAIN}"$'["toolchain.unrelated"]\nnote = "accepted"\n' +if ! PARSED_LITERAL_DOT_CHANNEL="$( + zc_read_exact_rust_version_assignment \ + "$TOOLS_CHANNEL_FIXTURE" toolchain channel +)" || [[ "$PARSED_LITERAL_DOT_CHANNEL" != "1.2.3" ]]; then + echo "Tools channel parser confused a literal dot with a dotted key" >&2 + exit 1 +fi + +expect_tools_channel_rejected() { + local description="$1" + local source="$2" + write_tools_channel_fixture "$source" + if zc_read_exact_rust_version_assignment \ + "$TOOLS_CHANNEL_FIXTURE" toolchain channel >/dev/null; then + echo "Tools channel parser accepted $description" >&2 + return 1 + fi +} + +expect_tools_channel_rejected "an assignment without its table" \ + $'channel = "1.2.3"\n' +expect_tools_channel_rejected "an assignment in the wrong table" \ + $'[other]\nchannel = "1.2.3"\n' +expect_tools_channel_rejected "a noncanonical table header" \ + $'[ toolchain ]\nchannel = "1.2.3"\n' +expect_tools_channel_rejected "a duplicate table" \ + "${CANONICAL_TOOLCHAIN}"$'[toolchain]\n' +expect_tools_channel_rejected "a duplicate assignment" \ + "${CANONICAL_TOOLCHAIN}"$'channel = "4.5.6"\n' +expect_tools_channel_rejected "a compact assignment" \ + "${CANONICAL_TOOLCHAIN}"$'channel="4.5.6"\n' +expect_tools_channel_rejected "a leading-space assignment" \ + "${CANONICAL_TOOLCHAIN}"$' channel = "4.5.6"\n' +expect_tools_channel_rejected "a trailing-space assignment" \ + "${CANONICAL_TOOLCHAIN}"$'channel = "4.5.6" \n' +expect_tools_channel_rejected "an assignment with an inline comment" \ + "${CANONICAL_TOOLCHAIN}"$'channel = "4.5.6" # duplicate authority\n' +expect_tools_channel_rejected "a nonnumeric channel" \ + "${CANONICAL_TOOLCHAIN}"$'channel = "nightly"\n' +expect_tools_channel_rejected "a quoted channel key" \ + "${CANONICAL_TOOLCHAIN}"$'"channel" = "4.5.6"\n' +expect_tools_channel_rejected "a channel key with a short Unicode escape" \ + "${CANONICAL_TOOLCHAIN}"$'"\\u0063hannel" = "4.5.6"\n' +expect_tools_channel_rejected "a channel key with a long Unicode escape" \ + "${CANONICAL_TOOLCHAIN}"$'"\\U00000063hannel" = "4.5.6"\n' +expect_tools_channel_rejected "an escaped duplicate table" \ + "${CANONICAL_TOOLCHAIN}"$'["tool\\u0063hain"]\n' +expect_tools_channel_rejected "a dotted channel assignment" \ + "${CANONICAL_TOOLCHAIN}"$'[other]\ntoolchain.channel = "4.5.6"\n' +expect_tools_channel_rejected "an inline-table channel assignment" \ + "${CANONICAL_TOOLCHAIN}"$'other = {channel = "4.5.6"}\n' +expect_tools_channel_rejected "a duplicate noncanonical table" \ + "${CANONICAL_TOOLCHAIN}"$'[ toolchain ]\n' +expect_tools_channel_rejected "a duplicate double-quoted table" \ + "${CANONICAL_TOOLCHAIN}"$'["toolchain"]\n' +expect_tools_channel_rejected "a duplicate single-quoted table" \ + "${CANONICAL_TOOLCHAIN}"$"['toolchain']\n" +expect_tools_channel_rejected "a conflicting quoted array table" \ + "${CANONICAL_TOOLCHAIN}"$'[["toolchain"]]\n' +expect_tools_channel_rejected "an assignment after leaving its table" \ + "${CANONICAL_TOOLCHAIN}"$'[other]\nchannel = "4.5.6"\n' +expect_tools_channel_rejected "an assignment inside a multiline string" \ + $'[toolchain]\nnote = """\nchannel = "9.9.9"\n"""\n' +expect_tools_channel_rejected "a table inside a multiline literal string" \ + $'[other]\nnote = \047\047\047\n[toolchain]\nchannel = "9.9.9"\n\047\047\047\n' +expect_tools_channel_rejected "an unterminated bare CR" \ + "${CANONICAL_TOOLCHAIN}"$'profile = "minimal"\r' +expect_tools_channel_rejected "an embedded bare CR" \ + "${CANONICAL_TOOLCHAIN}"$'profile = "mini\rmal"\n' +expect_tools_channel_rejected "an unterminated declaration" \ + "${CANONICAL_TOOLCHAIN}"$'profile = "minimal"' + +# Exercise a dotted expected path independently. TOML permits a different key +# spelling for every component, so a mixed quoted/bare duplicate must not +# evade the same one-table contract. Conversely, quotes can make a dot literal +# inside one component; those unrelated paths must remain accepted. +CANONICAL_CI_METADATA=$'[package.metadata.ci]\npinned-stable = "1.2.3"\n' +write_tools_channel_fixture \ + "${CANONICAL_CI_METADATA}"$'[ "package" . metadata . \047ci\047 ] # duplicate\n' +if zc_read_exact_rust_version_assignment \ + "$TOOLS_CHANNEL_FIXTURE" package.metadata.ci pinned-stable \ + >/dev/null; then + echo "Tools channel parser accepted a mixed-quoted duplicate table" >&2 + exit 1 +fi +write_tools_channel_fixture \ + "${CANONICAL_CI_METADATA}"$'["package.metadata.ci"]\n[package."metadata.ci"]\n[target.\047cfg(any())\047.dependencies]\n' +if ! PARSED_LITERAL_DOT_METADATA="$( + zc_read_exact_rust_version_assignment \ + "$TOOLS_CHANNEL_FIXTURE" package.metadata.ci pinned-stable +)" || [[ "$PARSED_LITERAL_DOT_METADATA" != "1.2.3" ]]; then + echo "Tools channel parser confused quoted dotted components with a path" >&2 + exit 1 +fi + +printf '[toolchain]\nchannel = "1.2' > "$TOOLS_CHANNEL_FIXTURE" +printf '\0' >> "$TOOLS_CHANNEL_FIXTURE" +printf '.3"\n' >> "$TOOLS_CHANNEL_FIXTURE" +if zc_read_exact_rust_version_assignment \ + "$TOOLS_CHANNEL_FIXTURE" toolchain channel >/dev/null; then + echo "Tools channel parser accepted a NUL byte" >&2 + exit 1 +fi + +if zc_read_exact_rust_version_assignment \ + /dev/null/missing toolchain channel \ + >/dev/null 2>&1; then + echo "Tools channel parser accepted an input it could not open" >&2 + exit 1 +fi + +# RUSTUP_TOOLCHAIN overrides tools/rust-toolchain.toml. Use an intentionally +# invalid value to prove that the Unix wrapper's explicit pin defeats this +# ambient override while it builds cargo-zerocopy, then reports the configured +# stable version. +WRAPPER_STABLE="$( + RUSTUP_TOOLCHAIN=zerocopy-ci-intentionally-invalid \ + ../zerocopy/cargo.sh --version stable +)" +if [[ "$WRAPPER_STABLE" != "$STABLE_CHANNEL" ]]; then + echo "cargo.sh resolved stable to $WRAPPER_STABLE, expected $STABLE_CHANNEL" \ + >&2 + exit 1 +fi + +# Prove that the shared Unix wrapper defeats an ambient rustup override. Keep +# the lockfile read-only: repository tools are part of CI's trusted setup, so +# an unreviewed dependency change must never be created as a side effect of +# testing them. +RUSTUP_TOOLCHAIN=zerocopy-ci-intentionally-invalid \ + ./cargo.sh test --locked --workspace diff --git a/githooks/pre-push b/githooks/pre-push index 886dad249b..6c4445bae3 100755 --- a/githooks/pre-push +++ b/githooks/pre-push @@ -50,15 +50,18 @@ for lockfile in "${LOCKFILES[@]}"; do LOCKFILE_HASHES_BEFORE+=("$(lockfile_hash "$lockfile")") done -# check_fmt.sh uses cargo-zerocopy's pinned nightly toolchain. On a fresh -# runner, it previously reached rustup concurrently with other checks. Rustup -# shares download and rollback paths across toolchains, so one installation -# could remove another installation's partial file. Bootstrap nightly before -# retaining parallelism between the checks themselves. +# check_fmt.sh uses cargo-zerocopy's pinned nightly toolchain. check_tools.sh +# uses the exact stable compiler pinned in tools/rust-toolchain.toml. On a fresh +# runner, these checks previously reached rustup concurrently. Rustup shares +# download and rollback paths across toolchains, so one installation could +# remove another installation's partial file. Bootstrap both toolchains in +# sequence before retaining parallelism between the checks themselves. # # Keep this list coordinated with the backgrounded scripts below. If another # check starts using a different cargo-zerocopy descriptor, initialize it here -# before that check may run in parallel. +# before that check may run in parallel. The stable bootstrap also installs the +# exact compiler pinned by tools/rust-toolchain.toml: zerocopy/cargo.sh builds +# cargo-zerocopy with that pin, and CI requires it to equal `+stable`. CHECKS_FAILED=0 TOOLCHAINS_READY=1 bootstrap_toolchain() { @@ -80,6 +83,7 @@ bootstrap_toolchain() { TOOLCHAINS_READY=0 } +bootstrap_toolchain stable bootstrap_toolchain nightly # Forego redirecting stdout to /dev/null on check_fmt.sh because the output from @@ -106,6 +110,7 @@ if [[ "$TOOLCHAINS_READY" -eq 1 ]]; then ./ci/check_actions.sh & ACTIONS_PID=$! ./ci/check_fmt.sh & FMT_PID=$! ./ci/check_job_dependencies.sh >/dev/null & JOB_DEPS_PID=$! + ./ci/check_tools.sh & TOOLS_PID=$! ./zerocopy/ci/check_all_toolchains_tested.sh >/dev/null & TOOLCHAINS_PID=$! ./zerocopy/ci/check_readme.sh >/dev/null & README_PID=$! ./zerocopy/ci/check_stale_stderr.sh >/dev/null & STALE_STDERR_PID=$! @@ -119,6 +124,7 @@ if [[ "$TOOLCHAINS_READY" -eq 1 ]]; then wait_for_check "ci/check_actions.sh" "$ACTIONS_PID" wait_for_check "ci/check_fmt.sh" "$FMT_PID" wait_for_check "ci/check_job_dependencies.sh" "$JOB_DEPS_PID" + wait_for_check "ci/check_tools.sh" "$TOOLS_PID" wait_for_check \ "zerocopy/ci/check_all_toolchains_tested.sh" "$TOOLCHAINS_PID" wait_for_check "zerocopy/ci/check_readme.sh" "$README_PID" diff --git a/githooks/test_pre_push.py b/githooks/test_pre_push.py index f26fc44512..994e2e64fa 100755 --- a/githooks/test_pre_push.py +++ b/githooks/test_pre_push.py @@ -31,6 +31,9 @@ "ci/check_actions.sh", "ci/check_fmt.sh", "ci/check_job_dependencies.sh", + # Keep this fixture coordinated with the explicit fan-out in pre-push. + # Omitting a real check here would leave its child handling untested. + "ci/check_tools.sh", "zerocopy/ci/check_all_toolchains_tested.sh", "zerocopy/ci/check_readme.sh", "zerocopy/ci/check_stale_stderr.sh", @@ -213,7 +216,7 @@ def test_bootstraps_serially_and_runs_every_check(self): self.assertEqual(result.returncode, 0, result.stderr) self.assertEqual( self.cargo_invocations(), - ["+nightly --version"], + ["+stable --version", "+nightly --version"], ) for check in _CHECKS: self.assertTrue(self.marker_for(check).is_file(), check) @@ -246,19 +249,19 @@ def test_can_run_from_a_repository_subdirectory(self): def test_bootstrap_failure_still_checks_lockfiles(self): lockfile = _LOCKFILES[-1] result = self.repository.run_hook( - FAIL_CARGO_INVOCATION="+nightly --version", - MUTATING_CARGO_INVOCATION="+nightly --version", + FAIL_CARGO_INVOCATION="+stable --version", + MUTATING_CARGO_INVOCATION="+stable --version", MUTATE_LOCKFILE=str(self.repository.path / lockfile), ) self.assertNotEqual(result.returncode, 0) self.assertIn( - "zerocopy/cargo.sh +nightly --version failed with status 23", + "zerocopy/cargo.sh +stable --version failed with status 23", result.stderr, ) self.assertIn(f"{lockfile} was modified", result.stderr) self.assertEqual( self.cargo_invocations(), - ["+nightly --version"], + ["+stable --version", "+nightly --version"], ) for check in _CHECKS: self.assertFalse(self.marker_for(check).exists(), check) diff --git a/tools/cargo.sh b/tools/cargo.sh new file mode 100755 index 0000000000..bd5ee32d12 --- /dev/null +++ b/tools/cargo.sh @@ -0,0 +1,47 @@ +#!/usr/bin/env bash +# +# Copyright 2026 The Fuchsia Authors +# +# Licensed under a BSD-style license , Apache License, Version 2.0 +# , or the MIT +# license , at your option. +# This file may not be copied, modified, or distributed except according to +# those terms. + +set -eo pipefail + +TOOLS_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" >/dev/null 2>&1 && pwd)" + +# shellcheck source=toolchain.sh +source "$TOOLS_DIR/toolchain.sh" + +# Do not rely on rustup's directory lookup here. RUSTUP_TOOLCHAIN and a +# persisted `rustup override set` both take precedence over a toolchain file, +# while an explicit `+` takes precedence over those overrides. Keep +# this deliberately narrow parser coordinated with rust-toolchain.toml, +# toolchain.sh, and ../ci/check_tools.sh. A changed or duplicate declaration +# must fail closed instead of silently selecting another compiler. The shared +# parser accepts a pre-existing Windows worktree whose TOML still uses CRLF. +if ! TOOLS_TOOLCHAIN="$( + zc_read_exact_rust_version_assignment \ + "$TOOLS_DIR/rust-toolchain.toml" toolchain channel +)"; then + echo "Expected one canonical [toolchain] channel" \ + "in tools/rust-toolchain.toml" >&2 + exit 1 +fi +if [[ -z "$TOOLS_TOOLCHAIN" || "$TOOLS_TOOLCHAIN" == *$'\n'* ]]; then + echo "Expected one exact channel in tools/rust-toolchain.toml" >&2 + exit 1 +fi + +# Establish the tools workspace as Cargo's discovery directory. This keeps +# every caller on the checked-in tools lockfile and Cargo configuration unless +# the caller explicitly supplies different command-line options. +# +# Cargo subcommands can launch Cargo again. Export the same validated pin so a +# child process cannot fall back to the caller's RUSTUP_TOOLCHAIN or a persisted +# directory override after the explicit outer `+` has done its job. +export RUSTUP_TOOLCHAIN="$TOOLS_TOOLCHAIN" +cd "$TOOLS_DIR" +exec cargo "+$TOOLS_TOOLCHAIN" "$@" diff --git a/tools/generate-readme/src/main.rs b/tools/generate-readme/src/main.rs index 3d4efe0c8e..9c8715f5e8 100644 --- a/tools/generate-readme/src/main.rs +++ b/tools/generate-readme/src/main.rs @@ -6,7 +6,11 @@ // This file may not be copied, modified, or distributed except according to // those terms. -use std::{env, path::PathBuf, process::Command}; +use std::{ + env, io, + path::PathBuf, + process::{Command, Output}, +}; use regex::{Captures, Regex}; @@ -30,6 +34,19 @@ const DISCLAIMER_FOOTER: &str = "\ Disclaimer: Zerocopy is not an officially supported Google product.\ "; +fn checked_output(command: &mut Command) -> io::Result { + let output = command.output()?; + if output.status.success() { + Ok(output) + } else { + Err(io::Error::other(format!( + "child command failed with {}:\n{}", + output.status, + String::from_utf8_lossy(&output.stderr) + ))) + } +} + fn main() { let cwd = env::current_dir().unwrap(); let readme_dir = env::var_os("ZEROCOPY_README_DIR").map(PathBuf::from).unwrap_or_else(|| { @@ -42,11 +59,14 @@ fn main() { // This uses the `cargo readme` tool, which you can install via `cargo install // cargo-readme --version 3.2.0`. - let output = Command::new("cargo") - .current_dir(readme_dir) - .args(["readme", "--no-license"]) - .output() - .unwrap(); + // The tools wrapper exports its checked compiler pin for this nested Cargo + // invocation. Propagate failure before printing any generated framing: a + // caller may redirect stdout directly onto README.md, so partial output + // would turn a tool failure into a destructive-looking regeneration. + let output = checked_output( + Command::new("cargo").current_dir(readme_dir).args(["readme", "--no-license"]), + ) + .unwrap(); let readme = String::from_utf8(output.stdout).unwrap(); @@ -62,3 +82,33 @@ fn main() { println!("{}\n\n{}\n{}", COPYRIGHT_HEADER, body, DISCLAIMER_FOOTER); } + +#[cfg(test)] +mod tests { + use std::process::Command; + + use super::checked_output; + + #[cfg(any(unix, windows))] + #[test] + fn rejects_child_failure_without_returning_partial_stdout() { + #[cfg(unix)] + let mut command = { + let mut command = Command::new("sh"); + command.args(["-c", "printf partial-output; printf child-diagnostic >&2; exit 7"]); + command + }; + #[cfg(windows)] + let mut command = { + let mut command = Command::new("cmd"); + command + .args(["/C", "&2 echo child-diagnostic & exit /b 7"]); + command + }; + + let error = checked_output(&mut command).unwrap_err().to_string(); + assert!(error.contains("child command failed with")); + assert!(error.contains("child-diagnostic")); + assert!(!error.contains("partial-output")); + } +} diff --git a/tools/rust-toolchain.toml b/tools/rust-toolchain.toml new file mode 100644 index 0000000000..74f3f02dd8 --- /dev/null +++ b/tools/rust-toolchain.toml @@ -0,0 +1,23 @@ +# Copyright 2026 The Fuchsia Authors +# +# Licensed under a BSD-style license , Apache License, Version 2.0 +# , or the MIT +# license , at your option. +# This file may not be copied, modified, or distributed except according to +# those terms. + +# This pin selects the compiler used to build repository tools. It does not set +# Zerocopy's minimum supported Rust version. The stable-toolchain roller updates +# this value together with `package.metadata.ci.pinned-stable` in +# `zerocopy/Cargo.toml` so the two build paths do not drift accidentally. +# +# The shared Unix tools wrapper (`cargo.sh`) and the Windows cargo-zerocopy +# wrapper (`../zerocopy/win-cargo.bat`) parse the channel line so they can pass +# an explicit `+` to cargo. `../ci/check_tools.sh` validates the same +# line. Keep its key, spacing, quotes, and numeric version format coordinated +# with those parsers; changing the format should make them fail. The batch +# bootstrap additionally recognizes these three semantic lines as one ordered +# unit, so adding a field or changing `profile` must update win-cargo.bat too. +[toolchain] +channel = "1.93.1" +profile = "minimal" diff --git a/tools/toolchain.sh b/tools/toolchain.sh new file mode 100644 index 0000000000..fd8d593b3b --- /dev/null +++ b/tools/toolchain.sh @@ -0,0 +1,187 @@ +#!/usr/bin/env bash +# +# Copyright 2026 The Fuchsia Authors +# +# Licensed under a BSD-style license , Apache License, Version 2.0 +# , or the MIT +# license , at your option. +# This file may not be copied, modified, or distributed except according to +# those terms. + +# Shared bootstrap parser for one exact Rust-version assignment in one exact +# TOML table. This code runs before the typed Rust audit can normalize +# repository text, so it accepts LF and well-formed CRLF directly. A carriage +# return in any other position, including an unterminated final line, fails +# closed. +# +# Keep the two callers (`cargo.sh` and `../ci/check_tools.sh`) coordinated with +# the independent parser in `../zerocopy/win-cargo.bat`. The table and key +# arguments are fixed repository constants, not user input. This is +# intentionally a narrow recognizer rather than a partial TOML parser: any +# alternate spelling of the target assignment must fail and force the +# bootstrap contract to be updated deliberately. +zc_read_exact_rust_version_assignment() { + if [[ $# -ne 3 ]]; then + return 2 + fi + + local path="$1" + local expected_table="$2" + local key="$3" + local line="" + local normalized + local expected_header="[$expected_table]" + local expected_table_path_ere="" + local expected_table_header_ere + local expected_array_table_header_ere + local separator="" + local part + local -a expected_table_parts + local expected_table_count=0 + local in_expected_table=0 + local assignment_count=0 + local prefix="$key = \"" + local suffix='"' + local assignment_ere="(^|[.{,[:space:]])[\"']?$key[\"']?[[:space:]]*=" + local unicode_escape_ere='\\(u[[:xdigit:]]{4}|U[[:xdigit:]]{8})' + local version + local selected_version="" + local rust_version_ere='^[0-9]+\.[0-9]+\.[0-9]+$' + + # These values are fixed repository constants, but they are interpolated + # into regular expressions below. Reject an accidental future caller which + # would give punctuation regex meaning instead of silently widening the + # bootstrap grammar. + [[ "$expected_table" =~ ^[A-Za-z0-9_-]+(\.[A-Za-z0-9_-]+)*$ ]] || return 2 + [[ "$key" =~ ^[A-Za-z0-9_-]+$ ]] || return 2 + + # TOML permits each component of a dotted key to be bare, basic quoted, or + # literal quoted. Build the exact semantic spelling of the expected path so + # quoted aliases are rejected without confusing a literal dot inside one + # quoted component with a separator. For example, `["package".metadata.ci]` + # aliases `[package.metadata.ci]`, while `["package.metadata.ci"]` names one + # unrelated component and remains valid. Unicode escapes are handled by the + # global fail-closed check below. + IFS='.' read -r -a expected_table_parts <<< "$expected_table" + for part in "${expected_table_parts[@]}"; do + expected_table_path_ere+="$separator($part|\"$part\"|'$part')" + separator='[[:space:]]*\.[[:space:]]*' + done + expected_table_header_ere="^[[:space:]]*\\[[[:space:]]*${expected_table_path_ere}[[:space:]]*\\][[:space:]]*(#.*)?$" + expected_array_table_header_ere="^[[:space:]]*\\[[[:space:]]*\\[[[:space:]]*${expected_table_path_ere}[[:space:]]*\\][[:space:]]*\\][[:space:]]*(#.*)?$" + + # Reject an unavailable input before entering the loop so callers receive a + # useful status rather than an empty, apparently successful result. The + # conditional around the loop also preserves a redirection failure if the + # file becomes unavailable between this check and opening it. + [[ -r "$path" ]] || return 1 + + # Bash `read` silently removes NUL bytes. Detect one before the line parser + # can turn a noncanonical file into canonical-looking text. All callers pass + # reopenable repository or fixture paths: this preflight and the loop below + # deliberately open the file independently. + if IFS= read -r -d '' normalized < "$path"; then + return 1 + fi + if ! while true; do + line="" + if IFS= read -r line; then + : + elif [[ -n "$line" ]]; then + # `read` saw bytes without a terminating LF. In particular, do not + # mistake a final bare CR for the end of a CRLF record. + return 1 + else + break + fi + + case "$line" in + *$'\r') + normalized="${line%$'\r'}" + # Removing the one CR which belongs to CRLF must leave no other CR. + [[ "$normalized" != *$'\r'* ]] || return 1 + ;; + *$'\r'*) + return 1 + ;; + *) + normalized="$line" + ;; + esac + + if [[ -z "$normalized" || "$normalized" =~ ^[[:space:]]*# ]]; then + continue + fi + + # A line-oriented recognizer cannot distinguish apparent tables or + # assignments inside TOML multiline strings. Neither coordinated input + # needs multiline strings today, so reject both delimiter forms. If one is + # introduced later, replace this recognizer with a parser which tracks TOML + # string state rather than letting embedded text gain bootstrap authority. + if [[ "$normalized" == *'"""'* || "$normalized" == *"'''"* ]]; then + return 1 + fi + + # TOML basic quoted keys and table names decode Unicode escapes before + # comparing identifiers. For example, `"\u0063hannel"` is another + # spelling of `channel`, but the deliberately textual checks below cannot + # recognize that semantic duplicate. Decoding TOML escapes here would turn + # this bootstrap recognizer into a partial parser, so fail closed on either + # Unicode-escape form anywhere in a semantic line. Ordinary escapes such as + # `\"` remain accepted in unrelated values. If a coordinated input ever + # needs a Unicode escape, replace this recognizer with a parser and update + # the hostile fixtures in `../ci/check_tools.sh` at the same time. + if [[ "$normalized" =~ $unicode_escape_ere ]]; then + return 1 + fi + + # A table header with any alternate spacing is deliberately not the + # expected canonical header. It still leaves the expected table so an + # exact-looking assignment beneath another table cannot be accepted. + # + # Quoted TOML keys are another spelling of a bare key. For example, + # `["toolchain"]` and `['toolchain']` both reopen `[toolchain]`. Array + # tables receive the same treatment because defining `[[toolchain]]` + # alongside `[toolchain]` is also a semantic collision. Keep these checks + # coordinated with the hostile fixtures in `../ci/check_tools.sh`. + if [[ "$normalized" =~ ^[[:space:]]*\[ ]]; then + if [[ "$normalized" == "$expected_header" ]]; then + expected_table_count=$((expected_table_count + 1)) + [[ $expected_table_count -eq 1 ]] || return 1 + in_expected_table=1 + else + if [[ "$normalized" =~ $expected_table_header_ere || \ + "$normalized" =~ $expected_array_table_header_ere ]]; then + return 1 + fi + in_expected_table=0 + fi + continue + fi + + if [[ "$normalized" == "$prefix"*"$suffix" ]]; then + version="${normalized#"$prefix"}" + version="${version%"$suffix"}" + [[ $in_expected_table -eq 1 ]] || return 1 + [[ "$version" =~ $rust_version_ere ]] || return 1 + assignment_count=$((assignment_count + 1)) + [[ $assignment_count -eq 1 ]] || return 1 + selected_version="$version" + continue + fi + + # Catch compact, spaced, quoted, dotted, and inline-table spellings of the + # same key. + # False positives are intentionally fail-closed: if a future TOML value + # needs to contain assignment-like text, update this bootstrap grammar and + # its hostile fixtures together. + if [[ "$normalized" =~ $assignment_ere ]]; then + return 1 + fi + done < "$path"; then + return 1 + fi + + [[ $expected_table_count -eq 1 && $assignment_count -eq 1 ]] || return 1 + printf '%s' "$selected_version" +} diff --git a/zerocopy/AGENTS.md b/zerocopy/AGENTS.md index dcc4b7d4ad..f8a5d09009 100644 --- a/zerocopy/AGENTS.md +++ b/zerocopy/AGENTS.md @@ -27,7 +27,8 @@ you **MUST** also read [agent_docs/reviewing.md](./agent_docs/reviewing.md). - **README Generation:** **DON'T** edit `README.md` directly. It is generated from `src/lib.rs`. Edit the top-level doc comment in `src/lib.rs` instead. - **To regenerate:** - `(cd .. && cargo -q run --manifest-path tools/Cargo.toml -p generate-readme) > README.md` + `ZEROCOPY_README_DIR=../zerocopy ../tools/cargo.sh -q run \ + --locked -p generate-readme > README.md` - **TODOs:** **DON'T** use `TODO` comments unless you explicitly intend to block diff --git a/zerocopy/cargo.sh b/zerocopy/cargo.sh index 82bacd0b38..62a347340e 100755 --- a/zerocopy/cargo.sh +++ b/zerocopy/cargo.sh @@ -13,13 +13,15 @@ set -eo pipefail ZEROCOPY_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" >/dev/null 2>&1 && pwd)" REPO_DIR="$(dirname "$ZEROCOPY_DIR")" -# Build `cargo-zerocopy` without any RUSTFLAGS or CARGO_TARGET_DIR set in the -# environment. Build it from the repository root so that Zerocopy's vendoring -# config does not apply to the unvendored tools workspace. -( - cd "$REPO_DIR" - env -u RUSTFLAGS -u CARGO_TARGET_DIR cargo +stable build --manifest-path tools/cargo-zerocopy/Cargo.toml -p cargo-zerocopy -q -) +# Build `cargo-zerocopy` without any RUSTFLAGS, CARGO_TARGET_DIR, or +# RUSTUP_TOOLCHAIN set in the environment. `tools/cargo.sh` supplies the exact +# compiler pin; clearing the variables also keeps their other effects out of +# the build. That wrapper establishes `tools` as Cargo's discovery directory, +# outside Zerocopy's vendored configuration. `--locked` prevents this bootstrap +# step from silently changing the tools dependency graph. +env -u RUSTFLAGS -u CARGO_TARGET_DIR -u RUSTUP_TOOLCHAIN \ + "$REPO_DIR/tools/cargo.sh" build --locked --manifest-path Cargo.toml \ + -p cargo-zerocopy -q cd "$ZEROCOPY_DIR" exec "$REPO_DIR/tools/target/debug/cargo-zerocopy" "$@" diff --git a/zerocopy/ci/check_readme.sh b/zerocopy/ci/check_readme.sh index 40989c49b4..aec6a5471b 100755 --- a/zerocopy/ci/check_readme.sh +++ b/zerocopy/ci/check_readme.sh @@ -11,13 +11,25 @@ set -eo pipefail cd "$(dirname "$0")/.." +# Exercise the tools wrapper with an override which rustup cannot resolve. The +# wrapper must replace it with the checked tools pin for both its outer Cargo +# process and the generator's nested `cargo readme`; otherwise this check fails +# before comparing generated output. +export RUSTUP_TOOLCHAIN=zerocopy-ci-intentionally-invalid + # Install again in case the installation failed during the # `generate_cache` step. We treat that step as best-effort and # suppress all errors from it. -( - cd .. - cargo install -q cargo-readme --version 3.2.0 -) +../tools/cargo.sh install -q cargo-readme --version 3.2.0 --locked -diff <(cd .. && cargo -q run --manifest-path tools/Cargo.toml -p generate-readme) README.md >&2 +# `tools/cargo.sh` pins the compiler explicitly, defeating both +# RUSTUP_TOOLCHAIN and rustup directory overrides, and establishes the tools +# workspace as Cargo's discovery directory. Keep this invocation identical to +# the regeneration commands in `../src/lib.rs` and `../AGENTS.md`. The +# generator needs its source directory explicitly because its repository-layout +# auto-detection intentionally starts from the tools workspace. +diff <( + ZEROCOPY_README_DIR=../zerocopy \ + ../tools/cargo.sh -q run --locked -p generate-readme +) README.md >&2 exit $? diff --git a/zerocopy/src/lib.rs b/zerocopy/src/lib.rs index b5ecbb58a8..4061f5926c 100644 --- a/zerocopy/src/lib.rs +++ b/zerocopy/src/lib.rs @@ -12,7 +12,8 @@ // After updating the following doc comment, make sure to run the following // command to update `README.md` based on its contents: // -// (cd .. && cargo -q run --manifest-path tools/Cargo.toml -p generate-readme) > README.md +// ZEROCOPY_README_DIR=../zerocopy ../tools/cargo.sh -q run --locked \ +// -p generate-readme > README.md //! ***Fast, safe, compile error. Pick two.*** diff --git a/zerocopy/win-cargo.bat b/zerocopy/win-cargo.bat index dfacf455ac..5228d7b25e 100644 --- a/zerocopy/win-cargo.bat +++ b/zerocopy/win-cargo.bat @@ -6,22 +6,122 @@ @rem This file may not be copied, modified, or distributed except according to @rem those terms. -@rem Build `cargo-zerocopy` without any RUSTFLAGS set in the environment. -@rem Build from the repository root so that Zerocopy's vendoring config does -@rem not apply to the unvendored tools workspace. -@set SCRIPT_DIR=%~dp0 -@set TEMP_RUSTFLAGS=%RUSTFLAGS% -@set RUSTFLAGS= -@pushd "%SCRIPT_DIR%.." -@cargo +stable build --manifest-path tools\Cargo.toml -p cargo-zerocopy -q -@set CARGO_ZEROCOPY_BUILD_STATUS=%ERRORLEVEL% +@echo off +@rem Parse the tools compiler and pass it to cargo explicitly. An explicit +@rem `+` takes precedence over both RUSTUP_TOOLCHAIN and a persisted +@rem `rustup override set` in this checkout. This bootstrap parser requires the +@rem complete non-comment file shape: [toolchain], one canonical numeric +@rem channel, then profile = "minimal". A future semantic field must update this +@rem parser deliberately. Keep it coordinated with tools\rust-toolchain.toml, +@rem tools\toolchain.sh, tools\cargo.sh, and ci\check_tools.sh. +@rem Resolve the checkout path before enabling delayed expansion. A checkout +@rem path may legally contain `!`, which delayed expansion would remove while +@rem expanding `%~dp0`. The parser enables it only while already inside tools. +@setlocal EnableExtensions DisableDelayedExpansion +@rem A caller can define an ordinary ERRORLEVEL variable which shadows cmd's +@rem dynamic status value during `%ERRORLEVEL%` and `!ERRORLEVEL!` expansion. +@rem Clear only this local copy so status capture remains trustworthy; the +@rem final ENDLOCAL restores any value the caller supplied. +@set "ERRORLEVEL=" +@pushd "%~dp0..\tools" +@if errorlevel 1 exit /b 1 +@set "TOOLS_TOOLCHAIN_FILE_VALID=1" +@rem FOR /F collapses whitespace before the token checks below. Reject every +@rem noncanonical space and tab in a semantic line first. The last /C pattern +@rem contains one literal tab. +@findstr /V /B /C:"#" "rust-toolchain.toml" | findstr /R /C:"^ " /C:" $" /C:" " /C:" " >nul +@if not errorlevel 1 @set "TOOLS_TOOLCHAIN_FILE_VALID=0" +@rem Delayed expansion makes a single FOR block possible without CALL. Reject +@rem command-interpreter metacharacters before enabling it so a malformed +@rem semantic line can only make validation fail, never change the commands +@rem which validate it. The doubled percent denotes one literal character +@rem after cmd.exe parses this line. `|` is also the FOR /F end-of-line marker +@rem below, so rejecting it prevents a semantic line from being skipped. +@findstr /V /B /C:"#" "rust-toolchain.toml" | ^ + findstr /L /C:"!" /C:"&" /C:"|" /C:"^" /C:"%%" /C:"<" /C:">" ^ + /C:"(" /C:")" >nul +@if not errorlevel 1 @set "TOOLS_TOOLCHAIN_FILE_VALID=0" +@if not "%TOOLS_TOOLCHAIN_FILE_VALID%"=="1" ( + @echo Expected one canonical [toolchain] channel ^ + in tools\rust-toolchain.toml >&2 + @popd + @exit /b 1 +) + +@setlocal EnableDelayedExpansion +@set "TOOLS_TOOLCHAIN=" +@set "TOOLS_TOOLCHAIN_LINE_COUNT=0" +@set "TOOLS_TOOLCHAIN_CHANNEL_RAW=" +@set "TOOLS_TOOLCHAIN_PROFILE_RAW=" +@for /f "eol=| tokens=1,2,3,4" %%A in ('findstr /V /B /C:"#" "rust-toolchain.toml"') do ( + @set /a TOOLS_TOOLCHAIN_LINE_COUNT+=1 >nul + @if "!TOOLS_TOOLCHAIN_LINE_COUNT!"=="1" ( + @if not "%%A"=="[toolchain]" @set "TOOLS_TOOLCHAIN_FILE_VALID=0" + @if not "%%B"=="" @set "TOOLS_TOOLCHAIN_FILE_VALID=0" + ) else if "!TOOLS_TOOLCHAIN_LINE_COUNT!"=="2" ( + @if not "%%A"=="channel" @set "TOOLS_TOOLCHAIN_FILE_VALID=0" + @if not "%%B"=="=" @set "TOOLS_TOOLCHAIN_FILE_VALID=0" + @if not "%%D"=="" @set "TOOLS_TOOLCHAIN_FILE_VALID=0" + @set "TOOLS_TOOLCHAIN=%%~C" + @set "TOOLS_TOOLCHAIN_CHANNEL_RAW=%%C" + ) else if "!TOOLS_TOOLCHAIN_LINE_COUNT!"=="3" ( + @if not "%%A"=="profile" @set "TOOLS_TOOLCHAIN_FILE_VALID=0" + @if not "%%B"=="=" @set "TOOLS_TOOLCHAIN_FILE_VALID=0" + @if not "%%~C"=="minimal" @set "TOOLS_TOOLCHAIN_FILE_VALID=0" + @if not "%%D"=="" @set "TOOLS_TOOLCHAIN_FILE_VALID=0" + @set "TOOLS_TOOLCHAIN_PROFILE_RAW=%%C" + ) else ( + @set "TOOLS_TOOLCHAIN_FILE_VALID=0" + ) +) +@if not "!TOOLS_TOOLCHAIN_LINE_COUNT!"=="3" @set "TOOLS_TOOLCHAIN_FILE_VALID=0" + +@rem `%%~C` deliberately removes quotes when recording the channel. Compare +@rem the raw token separately so alternate or missing quotes cannot pass. A +@rem sentinel before each operand also makes an empty token safe. +@set TOOLS_TOOLCHAIN_EXPECTED_CHANNEL_RAW="!TOOLS_TOOLCHAIN!" +@if not x!TOOLS_TOOLCHAIN_CHANNEL_RAW!==x!TOOLS_TOOLCHAIN_EXPECTED_CHANNEL_RAW! ^ + @set "TOOLS_TOOLCHAIN_FILE_VALID=0" +@if not x!TOOLS_TOOLCHAIN_PROFILE_RAW!==x"minimal" ^ + @set "TOOLS_TOOLCHAIN_FILE_VALID=0" + +@rem Require exactly three nonempty decimal components. FOR /F collapses +@rem repeated delimiters, so reconstructing the version also rejects leading, +@rem trailing, and repeated dots. Initialize the result to failure because an +@rem empty value causes FOR /F to execute no iterations. +@set "TOOLS_TOOLCHAIN_VERSION_VALID=0" +@for /f "eol=| tokens=1-4 delims=." %%V in ("!TOOLS_TOOLCHAIN!") do ( + @if not "%%V"=="" if not "%%W"=="" if not "%%X"=="" ^ + if "%%Y"=="" if "!TOOLS_TOOLCHAIN!"=="%%V.%%W.%%X" ( + @set "TOOLS_TOOLCHAIN_VERSION_VALID=1" + @for /f "eol=| delims=0123456789" %%N in ("%%V%%W%%X") do ^ + @set "TOOLS_TOOLCHAIN_VERSION_VALID=0" + ) +) +@if not "!TOOLS_TOOLCHAIN_VERSION_VALID!"=="1" @set "TOOLS_TOOLCHAIN_FILE_VALID=0" +@if not "!TOOLS_TOOLCHAIN_FILE_VALID!"=="1" ( + @echo Expected one canonical [toolchain] channel ^ + in tools\rust-toolchain.toml >&2 + @popd + @exit /b 1 +) + +@rem Build `cargo-zerocopy` without output overrides from the environment. The +@rem explicit toolchain pins the outer Cargo invocation; export that same pin +@rem so nested Cargo or build-script children cannot recover the caller's +@rem override. Endlocal restores all three variables before cargo-zerocopy. +@set "RUSTFLAGS=" +@set "CARGO_TARGET_DIR=" +@set "RUSTUP_TOOLCHAIN=!TOOLS_TOOLCHAIN!" +@cargo +!TOOLS_TOOLCHAIN! build --locked --manifest-path Cargo.toml -p cargo-zerocopy -q +@set "CARGO_ZEROCOPY_BUILD_STATUS=!ERRORLEVEL!" @popd -@set RUSTFLAGS=%TEMP_RUSTFLAGS% -@set TEMP_RUSTFLAGS= +@endlocal & @set "CARGO_ZEROCOPY_BUILD_STATUS=%CARGO_ZEROCOPY_BUILD_STATUS%" @if not "%CARGO_ZEROCOPY_BUILD_STATUS%"=="0" exit /b %CARGO_ZEROCOPY_BUILD_STATUS% @rem Thin wrapper around the `cargo-zerocopy` binary in `tools/cargo-zerocopy` -@pushd "%SCRIPT_DIR%" +@pushd "%~dp0" +@if errorlevel 1 exit /b 1 @..\tools\target\debug\cargo-zerocopy %* -@set CARGO_ZEROCOPY_STATUS=%ERRORLEVEL% +@set "CARGO_ZEROCOPY_STATUS=%ERRORLEVEL%" @popd -@exit /b %CARGO_ZEROCOPY_STATUS% +@endlocal & @exit /b %CARGO_ZEROCOPY_STATUS%