Skip to content
Open
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
6 changes: 6 additions & 0 deletions .gitattributes
Original file line number Diff line number Diff line change
@@ -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
12 changes: 11 additions & 1 deletion .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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() }}
Expand Down
26 changes: 26 additions & 0 deletions .github/workflows/roll-pinned-toolchain-versions.yml
Original file line number Diff line number Diff line change
Expand Up @@ -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 \
Expand All @@ -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'

Expand Down
232 changes: 232 additions & 0 deletions ci/check_tools.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,232 @@
#!/usr/bin/env bash
#
# Copyright 2026 The Fuchsia Authors
#
# Licensed under a BSD-style license <LICENSE-BSD>, Apache License, Version 2.0
# <LICENSE-APACHE or https://www.apache.org/licenses/LICENSE-2.0>, or the MIT
# license <LICENSE-MIT or https://opensource.org/licenses/MIT>, 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
18 changes: 12 additions & 6 deletions githooks/pre-push
Original file line number Diff line number Diff line change
Expand Up @@ -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() {
Expand All @@ -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
Expand All @@ -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=$!
Expand All @@ -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"
Expand Down
13 changes: 8 additions & 5 deletions githooks/test_pre_push.py
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand Down Expand Up @@ -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)
Expand Down Expand Up @@ -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)
Expand Down
Loading
Loading