import argparse
import os
import platform
import re
import subprocess as sp
import sys
import time
from dataclasses import dataclass, field
from enum import Enum, IntEnum
from pathlib import Path
from typing import Optional, Sequence
ESC_YELLOW = "\033[1;33m"
ESC_CYAN = "\033[1;36m"
ESC_END = "\033[0m"
class Os(Enum):
LINUX = "Linux"
WINDOWS = "Windows"
DARWIN = "Darwin"
class Toolchain(IntEnum):
OTHER = 0
STABLE = 1
BETA = 2
NIGHTLY = 3
@dataclass
class Cfg:
toolchain_name: str
toolchain: Toolchain = field(init=False)
host_target: str = field(init=False)
os_: Os = field(init=False)
baseline_crate_dir: Optional[Path]
skip_semver: bool
def __post_init__(self):
rustc_output = check_output(["rustc", f"+{self.toolchain_name}", "-vV"])
self.host_target = re.findall(r"host: (.*)", rustc_output)[0]
if "nightly" in self.toolchain_name:
self.toolchain = Toolchain.NIGHTLY
elif "beta" in self.toolchain_name:
self.toolchain = Toolchain.BETA
elif "stable" in self.toolchain_name:
self.toolchain = Toolchain.STABLE
else:
self.toolchain = Toolchain.OTHER
self.os_ = Os(platform.system())
eprint(f"Testing Rust {self.toolchain_name} on {self.os_}")
def nightly(self) -> bool:
return self.toolchain == Toolchain.NIGHTLY
@dataclass
class Target:
name: str
dist: bool = True
min_toolchain: Toolchain = Toolchain.OTHER
def __post_init__(self):
if not self.dist:
self.min_toolchain = Toolchain.NIGHTLY
@dataclass
class TargetResult:
"""Not all checks exit immediately, so failures are reported here."""
target: Target
semver_ok: bool
FREEBSD_VERSIONS = [11, 12, 13, 14, 15]
TARGETS = [
Target("aarch64-apple-darwin"),
Target("aarch64-pc-windows-msvc"),
Target("aarch64-unknown-linux-gnu"),
Target("i686-pc-windows-msvc"),
Target("i686-unknown-linux-gnu"),
Target("x86_64-pc-windows-gnu"),
Target("x86_64-pc-windows-msvc"),
Target("x86_64-unknown-linux-gnu"),
Target("aarch64-pc-windows-gnullvm", min_toolchain=Toolchain.STABLE),
Target("aarch64-unknown-linux-musl"),
Target("aarch64-unknown-linux-ohos", min_toolchain=Toolchain.STABLE),
Target("arm-unknown-linux-gnueabi"),
Target("arm-unknown-linux-gnueabihf"),
Target("armv7-unknown-linux-gnueabihf"),
Target("armv7-unknown-linux-ohos", min_toolchain=Toolchain.STABLE),
Target("i686-pc-windows-gnu"),
Target("loongarch64-unknown-linux-gnu", min_toolchain=Toolchain.STABLE),
Target("loongarch64-unknown-linux-musl", min_toolchain=Toolchain.STABLE),
Target("powerpc-unknown-linux-gnu"),
Target("powerpc64-unknown-linux-gnu"),
Target("powerpc64le-unknown-linux-gnu"),
Target("powerpc64le-unknown-linux-musl", min_toolchain=Toolchain.STABLE),
Target("riscv64gc-unknown-linux-gnu"),
Target("s390x-unknown-linux-gnu"),
Target("sparcv9-sun-solaris"),
Target("x86_64-apple-darwin"),
Target("x86_64-pc-solaris"),
Target("x86_64-pc-windows-gnullvm", min_toolchain=Toolchain.STABLE),
Target("x86_64-unknown-freebsd"),
Target("x86_64-unknown-illumos"),
Target("x86_64-unknown-linux-musl"),
Target("x86_64-unknown-linux-ohos", min_toolchain=Toolchain.STABLE),
Target("x86_64-unknown-netbsd"),
Target("aarch64-apple-ios"),
Target("aarch64-linux-android"),
Target("aarch64-unknown-fuchsia", min_toolchain=Toolchain.STABLE),
Target("arm-linux-androideabi"),
Target("arm-unknown-linux-musleabi"),
Target("arm-unknown-linux-musleabihf"),
Target("armv5te-unknown-linux-gnueabi"),
Target("armv5te-unknown-linux-musleabi"),
Target("armv7-linux-androideabi"),
Target("armv7-unknown-linux-musleabihf"),
Target("i586-unknown-linux-gnu"),
Target("i586-unknown-linux-musl"),
Target("i686-linux-android"),
Target("i686-unknown-freebsd"),
Target("i686-unknown-linux-musl"),
Target("sparc64-unknown-linux-gnu"),
Target("wasm32-unknown-emscripten"),
Target("wasm32-unknown-unknown"),
Target("wasm32-wasip1", min_toolchain=Toolchain.STABLE),
Target("wasm32-wasip2", min_toolchain=Toolchain.STABLE),
Target("x86_64-fortanix-unknown-sgx"),
Target("x86_64-linux-android"),
Target("x86_64-unknown-fuchsia", min_toolchain=Toolchain.STABLE),
Target("x86_64-unknown-linux-gnux32"),
Target("x86_64-unknown-redox"),
Target("aarch64-unknown-freebsd", dist=False),
Target("aarch64-unknown-hermit", dist=False),
Target("aarch64-unknown-illumos", dist=False),
Target("aarch64-unknown-netbsd", dist=False),
Target("aarch64-unknown-openbsd", dist=False),
Target("aarch64-wrs-vxworks", dist=False),
Target("armebv7r-none-eabihf", dist=False),
Target("armv7-rtems-eabihf", dist=False),
Target("armv7-wrs-vxworks-eabihf", dist=False),
Target("armv7r-none-eabihf", dist=False),
Target("armv7s-apple-ios", dist=False),
Target("i386-apple-ios", dist=False),
Target("i686-apple-darwin", dist=False),
Target("i686-unknown-haiku", dist=False),
Target("i686-unknown-hurd-gnu", dist=False),
Target("i686-unknown-netbsd", dist=False),
Target("i686-unknown-openbsd", dist=False),
Target("i686-wrs-vxworks", dist=False),
Target("mips-unknown-linux-gnu", dist=False),
Target("mips-unknown-linux-musl", dist=False),
Target("mips64-unknown-linux-gnuabi64", dist=False),
Target("mips64-unknown-linux-muslabi64", dist=False),
Target("mips64el-unknown-linux-gnuabi64", dist=False),
Target("mips64el-unknown-linux-muslabi64", dist=False),
Target("mipsel-unknown-linux-gnu", dist=False),
Target("mipsel-unknown-linux-musl", dist=False),
Target("nvptx64-nvidia-cuda", dist=False),
Target("powerpc-unknown-linux-gnuspe", dist=False),
Target("powerpc-unknown-netbsd", dist=False),
Target("powerpc-wrs-vxworks", dist=False),
Target("powerpc-wrs-vxworks-spe", dist=False),
Target("powerpc64-ibm-aix", dist=False),
Target("powerpc64-unknown-freebsd", dist=False),
Target("powerpc64-wrs-vxworks", dist=False),
Target("riscv32-wrs-vxworks", dist=False),
Target("riscv32gc-unknown-linux-gnu", dist=False),
Target("riscv32i-unknown-none-elf", dist=False),
Target("riscv32imac-unknown-none-elf", dist=False),
Target("riscv32imc-unknown-none-elf", dist=False),
Target("riscv64-wrs-vxworks", dist=False),
Target("riscv64a23-unknown-linux-gnu", dist=False),
Target("riscv64gc-unknown-freebsd", dist=False),
Target("riscv64gc-unknown-hermit", dist=False),
Target("riscv64gc-unknown-linux-musl", dist=False),
Target("riscv64gc-unknown-none-elf", dist=False),
Target("riscv64imac-unknown-none-elf", dist=False),
Target("s390x-unknown-linux-musl", dist=False),
Target("sparc-unknown-linux-gnu", dist=False),
Target("sparc64-unknown-netbsd", dist=False),
Target("thumbv7em-none-eabihf", dist=False),
Target("thumbv7m-none-eabi", dist=False),
Target("thumbv7neon-linux-androideabi", dist=False),
Target("thumbv7neon-unknown-linux-gnueabihf", dist=False),
Target("thumbv8m.main-none-eabi", dist=False),
Target("x86_64-unknown-dragonfly", dist=False),
Target("x86_64-unknown-haiku", dist=False),
Target("x86_64-unknown-hermit", dist=False),
Target("x86_64-unknown-l4re-uclibc", dist=False),
Target("x86_64-unknown-openbsd", dist=False),
Target("x86_64-wrs-vxworks", dist=False),
]
def eprint(*args, **kw) -> None:
print(*args, file=sys.stderr, **kw)
def xtrace(args: Sequence[str | Path], *, env: Optional[dict[str, str]]) -> None:
"""Print commands before running them."""
astr = " ".join(str(arg) for arg in args)
if env is None:
eprint(f"+ {astr}")
else:
envdiff = set(env.items()) - set(os.environ.items())
estr = " ".join(f"{k}='{v}'" for (k, v) in envdiff)
eprint(f"+ {estr} {astr}")
def check_output(
args: Sequence[str | Path], *, env: Optional[dict[str, str]] = None
) -> str:
xtrace(args, env=env)
return sp.check_output(args, env=env, encoding="utf8")
def run(
args: Sequence[str | Path],
*,
env: Optional[dict[str, str]] = None,
check: bool = True,
) -> sp.CompletedProcess:
xtrace(args, env=env)
return sp.run(args, env=env, check=check)
def check_dup_targets() -> None:
"""Ensure there are no duplicate targets in the list."""
all = set()
duplicates = set()
for target in TARGETS:
if target.name in all:
duplicates.add(target.name)
all.add(target.name)
assert len(duplicates) == 0, f"duplicate targets: {duplicates}"
def do_semver_checks(cfg: Cfg, target: Target) -> bool:
"""Run cargo semver-checks for a target."""
tname = target.name
if cfg.toolchain != Toolchain.STABLE:
eprint("Skipping semver checks (only supported on stable)")
return True
if not target.dist:
eprint("Skipping semver checks on non-dist target")
return True
if tname == cfg.host_target:
eprint("Running semver checks on host")
run(
[
"cargo",
"semver-checks",
"--only-explicit-features",
"--features=std,extra_traits",
"--release-type=patch",
],
check=True,
)
if cfg.baseline_crate_dir is None:
eprint(
"Non-host target: --baseline-crate-dir must be specified to \
run semver-checks"
)
sys.exit(1)
eprint("Running semver checks with cross compilation")
env = os.environ.copy()
env.setdefault("RUSTFLAGS", "")
env["RUSTFLAGS"] += " -Awarnings"
env["RUSTC_BOOTSTRAP"] = "1"
env.pop("LIBC_CI", None)
cmd = ["cargo", "rustdoc", "--target", tname]
rustdoc_args = [
"--",
"-Zunstable-options",
"--document-private-items",
"--document-hidden-items",
"--output-format=json",
"--cap-lints=allow",
]
run([*cmd, *rustdoc_args], env=env)
run(
[*cmd, "--manifest-path", cfg.baseline_crate_dir / "Cargo.toml", *rustdoc_args],
env=env,
)
baseline = cfg.baseline_crate_dir / "target" / tname / "doc" / "libc.json"
current = Path("target") / tname / "doc" / "libc.json"
res = run(
[
"cargo",
"semver-checks",
"--baseline-rustdoc",
baseline,
"--current-rustdoc",
current,
"--release-type=patch",
],
check=False,
)
return res.returncode == 0
def test_target(cfg: Cfg, target: Target) -> TargetResult:
"""Run tests for a single target."""
start = time.time()
env = os.environ.copy()
env.setdefault("RUSTFLAGS", "")
tname = target.name
target_cfg = check_output(["rustc", "--print=cfg", "--target", tname])
target_env = re.findall(r'target_env="(.*)"', target_cfg)
target_os = re.findall(r'target_os="(.*)"', target_cfg)
target_bits = re.findall(r'target_pointer_width="(.*)"', target_cfg)[0]
assert target_bits in ["32", "64"]
eprint(f"env {target_env}, os {target_os}, bits {target_bits}")
if target.dist:
action = "build"
else:
action = "check"
cmd = ["cargo", f"+{cfg.toolchain_name}", action, "--target", tname]
if not target.dist:
cmd += ["-Zbuild-std=core"]
env["RUSTFLAGS"] += " -Aimproper_ctypes_definitions"
else:
run(["rustup", "target", "add", tname, "--toolchain", cfg.toolchain_name])
run(cmd, env=env)
run([*cmd, "--features=extra_traits"], env=env)
if "gnu" in target_env and target_bits == "32":
run(cmd, env=env | {"RUST_LIBC_UNSTABLE_GNU_FILE_OFFSET_BITS": "64"})
run(cmd, env=env | {"RUST_LIBC_UNSTABLE_GNU_TIME_BITS": "64"})
if "musl" in target_env:
run(cmd, env=env | {"RUST_LIBC_UNSTABLE_MUSL_V1_2_3": "1"})
run([*cmd, "--no-default-features"])
run([*cmd, "--no-default-features", "--features=extra_traits"])
if cfg.nightly():
run([*cmd, "--no-default-features", "--features=rustc-dep-of-std"])
if "freebsd" in tname and cfg.toolchain >= Toolchain.STABLE:
for version in FREEBSD_VERSIONS:
run(cmd, env=env | {"RUST_LIBC_UNSTABLE_FREEBSD_VERSION": str(version)})
run(
[*cmd, "--no-default-features"],
env=env | {"RUST_LIBC_UNSTABLE_FREEBSD_VERSION": str(version)},
)
if cfg.skip_semver:
eprint("Skipping semver checks")
semver_ok = True
else:
semver_ok = do_semver_checks(cfg, target)
elapsed = round(time.time() - start, 2)
eprint(f"Finished checking target {tname} in {elapsed} seconds")
return TargetResult(target=target, semver_ok=semver_ok)
def main() -> None:
p = argparse.ArgumentParser()
p.add_argument("--toolchain", required=True, help="Rust toolchain")
p.add_argument("--only", help="only targets matching this regex")
p.add_argument("--skip", help="skip targets matching this regex")
p.add_argument("--skip-semver", help="don't run semver checks")
p.add_argument(
"--baseline-crate-dir",
help="specify the directory of the crate to run semver checks against",
)
p.add_argument(
"--half",
type=int,
choices=[1, 2],
help="specify 1 or 2 to run half of the targets",
)
args = p.parse_args()
cfg = Cfg(
toolchain_name=args.toolchain,
baseline_crate_dir=args.baseline_crate_dir and Path(args.baseline_crate_dir),
skip_semver=args.skip_semver,
)
eprint(f"Config: {cfg}")
eprint("Python version: ", sys.version)
check_dup_targets()
start = time.time()
if cfg.nightly():
run(["rustup", "component", "add", "rust-src"])
targets = TARGETS
eprint(f"Total checked targets across platforms: {len(targets)}")
if not cfg.nightly():
targets = [t for t in targets if t.dist]
targets = [t for t in targets if cfg.toolchain >= t.min_toolchain]
eprint(f"Targets checkable with this toolchain: {len(targets)}")
if args.only:
targets = [t for t in targets if re.match(args.only, t.name)]
if args.skip:
targets = [t for t in targets if not re.match(args.skip, t.name)]
if args.half == 1:
targets = targets[0::2]
elif args.half == 2:
targets = targets[1::2]
total = len(targets)
eprint(f"Targets to run: {total}")
assert total > 0, "some tests should be run"
target_results: list[TargetResult] = []
for i, target in enumerate(targets):
at = i + 1
eprint(f"::group::Target: {target.name} ({at}/{total})")
eprint(f"{ESC_CYAN}Checking target {target} ({at}/{total}){ESC_END}")
res = test_target(cfg, target)
target_results.append(res)
eprint("::endgroup::")
elapsed = round(time.time() - start, 2)
semver_failures = [t.target.name for t in target_results if not t.semver_ok]
if len(semver_failures) != 0:
eprint(f"\n{ESC_YELLOW}Some targets had semver failures:{ESC_END}")
for t in semver_failures:
eprint(f"* {t}")
eprint(f"\nChecked {total} targets in {elapsed} seconds")
main()