Files
fips/testing/check-portable-atomics.py
ArjenandJohnathan Corgan de50a88b31 refactor(ble): move the Android radio bridge to io_radio under platform-neutral names
The Android backend does not drive a radio itself; it drives one an
embedder operates through a small command surface and a set of delivery
callbacks. Nothing in that machinery is Android-specific, and macOS's
CoreBluetooth has the same command-out, callback-in shape, so the code
moves to io_radio.rs under neutral names: BleRadio, BleRadioBridge,
BleRadioSlot, RadioIo, RadioStream, RadioAcceptor and RadioScanner.

io_android.rs keeps the old names as re-exports, so the embedder API is
unchanged, and the Android arm of DefaultBleTransport names RadioIo,
which is the same type as before. No behaviour changes.
2026-10-13 20:56:07 +00:00

208 lines
7.8 KiB
Python
Executable File

#!/usr/bin/env python3
"""Fail when non-test code under src/ or core/src/ uses the std 64-bit atomics.
`std::sync::atomic::AtomicU64` and `AtomicI64` exist only on targets with
64-bit atomics. The 32-bit MIPS targets the OpenWrt packages are meant to
cover (mips-unknown-linux-musl, mipsel-unknown-linux-musl) have none, so one
such use anywhere in the crate stops the whole crate building there. Code
that needs a 64-bit atomic uses `portable_atomic::AtomicU64` instead, which is
the std type where the target has one and a lock-based fallback where it does
not. No CI leg builds for MIPS yet, so without this check the first anyone
would hear of a new std use is a failed build on a router target.
What is flagged, read from the files committed at HEAD:
* a `use` tree rooted at `std` or `core` that reaches
`sync::atomic::AtomicU64` or `sync::atomic::AtomicI64`, in any form:
a plain path, a grouped `{...}` list over one or several lines, a nested
group such as `std::sync::{Arc, atomic::{AtomicU64, Ordering}}`, or a
rename with `as`;
* a glob import of `std::sync::atomic::*` or `core::sync::atomic::*`,
because it brings both types in unnamed;
* a path-qualified use such as `std::sync::atomic::AtomicU64::new(0)`, and
`atomic::AtomicU64` after `use std::sync::atomic;`, anywhere in the text;
* `sa::AtomicU64` where the file renames the module with
`use std::sync::atomic as sa;` or `use std::sync::{atomic::{self as sa}}`.
Scope is the crate sources, src/ and core/src/, minus files under a `tests/`
directory and files named `tests.rs` or `*_tests.rs`, which are only ever
built for the host. A `#[cfg(test)]` module inside an ordinary file is NOT
exempt: telling it apart needs a Rust parser, and holding test code in those
files to the same rule costs nothing today (no such module uses the std
types).
No file is exempt by name. `EXEMPT` holds any that must be, each with its
reason.
Known gaps, recorded rather than discovered: a std 64-bit atomic reached
through a re-export from another crate, or through a type alias defined
outside the crate sources, is not seen; nor is one written with a macro that
assembles the path from pieces.
Exit codes:
0 - no non-test file under src/ or core/src/ uses a std 64-bit atomic
1 - at least one does; every hit is printed
2 - the check could not look (not a git work tree, git failed, or src/
and core/src/ matched no Rust files); never a pass
"""
from __future__ import annotations
import re
import subprocess
import sys
EXEMPT: dict[str, str] = {}
WIDE = ("AtomicU64", "AtomicI64")
SRC_DIRS = ("src/", "core/src/")
USE_RE = re.compile(r"\buse\s+([^;]+);", re.S)
# Any `atomic::AtomicU64` whose `atomic` is not the tail of a longer name, so
# `portable_atomic::AtomicU64` is not matched and every std spelling is.
PATH_RE = re.compile(r"(?<![\w])atomic\s*::\s*(AtomicU64|AtomicI64)\b")
def git(*args: str) -> str:
"""Run a git command and return its stdout, exiting 2 if it fails."""
proc = subprocess.run(["git", *args], capture_output=True, text=True)
if proc.returncode != 0:
print(f"check-portable-atomics: git {' '.join(args)} failed: {proc.stderr.strip()}",
file=sys.stderr)
sys.exit(2)
return proc.stdout
def is_test_path(path: str) -> bool:
"""True for files only ever built as part of the test harness."""
parts = path.split("/")
name = parts[-1]
return "tests" in parts[:-1] or name == "tests.rs" or name.endswith("_tests.rs")
def split_top(text: str) -> list[str]:
"""Split a use-tree list on the commas that are not inside braces."""
items, depth, cur = [], 0, []
for ch in text:
if ch == "{":
depth += 1
elif ch == "}":
depth -= 1
if ch == "," and depth == 0:
items.append("".join(cur))
cur = []
else:
cur.append(ch)
items.append("".join(cur))
return [i.strip() for i in items if i.strip()]
def flatten(tree: str, prefix: tuple[str, ...] = ()) -> list[tuple[str, ...]]:
"""Expand a use tree into the full paths it imports.
A rename, written `name@alias` by the caller, stays on the last segment.
"""
tree = re.sub(r"\s+", "", tree)
brace = tree.find("{")
if brace == -1:
segs = tuple(s for s in tree.split("::") if s)
return [prefix + segs]
head = tuple(s for s in tree[:brace].split("::") if s)
inner = tree[brace + 1:tree.rfind("}")]
out = []
for item in split_top(inner):
if item == "self" or item.startswith("self@"):
alias = item[len("self"):]
out.append(prefix + head[:-1] + (head[-1] + alias,))
else:
out.extend(flatten(item, prefix + head))
return out
def use_hits(text: str) -> list[tuple[int, str]]:
"""Return (line, imported path) for each std/core wide-atomic import."""
hits = []
for m in USE_RE.finditer(text):
body = re.sub(r"\s+as\s+(\w+)", r"@\1", m.group(1))
line = text.count("\n", 0, m.start()) + 1
for path in flatten(body):
if not path:
continue
last, _, alias = path[-1].partition("@")
path = path[:-1] + (last,)
if path[0] not in ("std", "core") or path[1:3] != ("sync", "atomic"):
continue
if len(path) == 3 and alias:
hits.extend(alias_hits(text, alias, "::".join(path)))
elif len(path) >= 4 and (path[3] in WIDE or path[3] == "*"):
hits.append((line, "::".join(path[:4])))
return hits
def alias_hits(text: str, alias: str, module: str) -> list[tuple[int, str]]:
"""Return (line, use) for each wide atomic named through a module alias."""
hits = []
pat = re.compile(r"(?<![\w])" + re.escape(alias) + r"\s*::\s*(AtomicU64|AtomicI64)\b")
for m in pat.finditer(text):
line = text.count("\n", 0, m.start()) + 1
hits.append((line, f"{module}::{m.group(1)} as {alias}::{m.group(1)}"))
return hits
def path_hits(text: str) -> list[tuple[int, str]]:
"""Return (line, matched text) for each path-qualified wide atomic."""
hits = []
for m in PATH_RE.finditer(text):
line = text.count("\n", 0, m.start()) + 1
hits.append((line, re.sub(r"\s+", "", m.group(0))))
return hits
def main() -> int:
"""Scan the committed crate sources and report every std wide-atomic use."""
root = git("rev-parse", "--show-toplevel").strip()
if not root:
print("check-portable-atomics: empty work-tree root", file=sys.stderr)
return 2
files = [
f for f in git("-C", root, "ls-tree", "-r", "--name-only", "HEAD", "--", *SRC_DIRS).splitlines()
if f.endswith(".rs")
]
if not files:
print("check-portable-atomics: src/ and core/src/ matched no Rust files at HEAD",
file=sys.stderr)
return 2
scanned = 0
findings = []
for path in files:
if is_test_path(path) or path in EXEMPT:
continue
text = git("-C", root, "show", f"HEAD:{path}")
scanned += 1
seen = set()
for line, what in use_hits(text) + path_hits(text):
if (line, what) not in seen:
seen.add((line, what))
findings.append(f"{path}:{line}: {what}")
if scanned == 0:
print("check-portable-atomics: every file under src/ and core/src/ was excluded", file=sys.stderr)
return 2
if findings:
for f in findings:
print(f)
print("", file=sys.stderr)
print("check-portable-atomics: std 64-bit atomics do not exist on 32-bit MIPS, so the",
file=sys.stderr)
print("crate stops building there. Use portable_atomic::AtomicU64 (or AtomicI64).",
file=sys.stderr)
return 1
return 0
if __name__ == "__main__":
sys.exit(main())