99import time
1010from dataclasses import dataclass, field
1111from enum import Enum, IntEnum
12- from typing import Optional
12+ from pathlib import Path
13+ from typing import Optional, Sequence
1314
1415
16+ ESC_YELLOW = "\033[1;33m"
1517ESC_CYAN = "\033[1;36m"
1618ESC_END = "\033[0m"
1719
@@ -35,6 +37,8 @@ class Cfg:
3537 toolchain: Toolchain = field(init=False)
3638 host_target: str = field(init=False)
3739 os_: Os = field(init=False)
40+ baseline_crate_dir: Optional[Path]
41+ skip_semver: bool
3842
3943 def __post_init__(self):
4044 rustc_output = check_output(["rustc", f"+{self.toolchain_name}", "-vV"])
@@ -66,6 +70,14 @@ def __post_init__(self):
6670 self.min_toolchain = Toolchain.NIGHTLY
6771
6872
73+ @dataclass
74+ class TargetResult:
75+ """Not all checks exit immediately, so failures are reported here."""
76+
77+ target: Target
78+ semver_ok: bool
79+
80+
6981FREEBSD_VERSIONS = [11, 12, 13, 14, 15]
7082
7183TARGETS = [
@@ -200,13 +212,13 @@ def __post_init__(self):
200212]
201213
202214
203- def eprint(*args, **kw):
215+ def eprint(*args, **kw) -> None :
204216 print(*args, file=sys.stderr, **kw)
205217
206218
207- def xtrace(args: list [str], / , env: Optional[dict[str, str]]):
219+ def xtrace(args: Sequence [str | Path ], * , env: Optional[dict[str, str]]) -> None :
208220 """Print commands before running them."""
209- astr = " ".join(args)
221+ astr = " ".join(str(arg) for arg in args)
210222 if env is None:
211223 eprint(f"+ {astr}")
212224 else:
@@ -215,17 +227,25 @@ def xtrace(args: list[str], /, env: Optional[dict[str, str]]):
215227 eprint(f"+ {estr} {astr}")
216228
217229
218- def check_output(args: list[str], /, env: Optional[dict[str, str]] = None) -> str:
230+ def check_output(
231+ args: Sequence[str | Path], *, env: Optional[dict[str, str]] = None
232+ ) -> str:
219233 xtrace(args, env=env)
220234 return sp.check_output(args, env=env, encoding="utf8")
221235
222236
223- def run(args: list[str], /, env: Optional[dict[str, str]] = None):
237+ def run(
238+ args: Sequence[str | Path],
239+ *,
240+ env: Optional[dict[str, str]] = None,
241+ check: bool = True,
242+ ) -> sp.CompletedProcess:
224243 xtrace(args, env=env)
225- sp.run(args, env=env, check=True )
244+ return sp.run(args, env=env, check=check )
226245
227246
228- def check_dup_targets():
247+ def check_dup_targets() -> None:
248+ """Ensure there are no duplicate targets in the list."""
229249 all = set()
230250 duplicates = set()
231251 for target in TARGETS:
@@ -235,7 +255,106 @@ def check_dup_targets():
235255 assert len(duplicates) == 0, f"duplicate targets: {duplicates}"
236256
237257
238- def test_target(cfg: Cfg, target: Target):
258+ def do_semver_checks(cfg: Cfg, target: Target) -> bool:
259+ """Run cargo semver-checks for a target."""
260+ tname = target.name
261+ if cfg.toolchain != Toolchain.STABLE:
262+ eprint("Skipping semver checks (only supported on stable)")
263+ return True
264+
265+ if not target.dist:
266+ eprint("Skipping semver checks on non-dist target")
267+ return True
268+
269+ if tname == cfg.host_target:
270+ # FIXME(semver): This is what we actually want to be doing on all targets, but
271+ # `--target` doesn't work right with semver-checks.
272+ eprint("Running semver checks on host")
273+ # NOTE: this is the only check which actually fails CI if it doesn't succeed,
274+ # since it is the only check we can control lints for (via the
275+ # package.metadata table).
276+ #
277+ # We may need to play around with this a bit.
278+ run(
279+ [
280+ "cargo",
281+ "semver-checks",
282+ "--only-explicit-features",
283+ "--features=std,extra_traits",
284+ "--release-type=patch",
285+ ],
286+ check=True,
287+ )
288+ # Don't return here so we still get the same rustdoc-json-base tests even while
289+ # running on the host.
290+
291+ if cfg.baseline_crate_dir is None:
292+ eprint(
293+ "Non-host target: --baseline-crate-dir must be specified to \
294+ run semver-checks"
295+ )
296+ sys.exit(1)
297+
298+ # Since semver-checks doesn't work with `--target`, we build the json ourself and
299+ # hand it over.
300+ eprint("Running semver checks with cross compilation")
301+
302+ # Set the bootstrap hack (for rustdoc json), allow warnings, and get rid of LIBC_CI
303+ # (which sets `deny(warnings)`).
304+ env = os.environ.copy()
305+ env.setdefault("RUSTFLAGS", "")
306+ env["RUSTFLAGS"] += " -Awarnings"
307+ env["RUSTC_BOOTSTRAP"] = "1"
308+ env.pop("LIBC_CI", None)
309+
310+ cmd = ["cargo", "rustdoc", "--target", tname]
311+ # Take the flags from:
312+ # https://github.com/obi1kenobi/cargo-semver-checks/blob/030af2032e93a64a6a40c4deaa0f57f262042426/src/data_generation/generate.rs#L241-L297
313+ rustdoc_args = [
314+ "--",
315+ "-Zunstable-options",
316+ "--document-private-items",
317+ "--document-hidden-items",
318+ "--output-format=json",
319+ "--cap-lints=allow",
320+ ]
321+
322+ # Build the current crate and the baseline crate, which CI should have downloaded
323+ run([*cmd, *rustdoc_args], env=env)
324+ run(
325+ [*cmd, "--manifest-path", cfg.baseline_crate_dir / "Cargo.toml", *rustdoc_args],
326+ env=env,
327+ )
328+
329+ baseline = cfg.baseline_crate_dir / "target" / tname / "doc" / "libc.json"
330+ current = Path("target") / tname / "doc" / "libc.json"
331+
332+ # NOTE: We can't configure lints when using the rustoc input :(. For this reason,
333+ # we don't check for failure output status since there is no way to override false
334+ # positives.
335+ #
336+ # See: https://github.com/obi1kenobi/cargo-semver-checks/issues/827
337+ res = run(
338+ [
339+ "cargo",
340+ "semver-checks",
341+ "--baseline-rustdoc",
342+ baseline,
343+ "--current-rustdoc",
344+ current,
345+ # For now, everything is a patch
346+ "--release-type=patch",
347+ ],
348+ check=False,
349+ )
350+
351+ # If this job failed, we can't fail CI because it may have been a false positive.
352+ # But at least we can make an explicit note of it.
353+ return res.returncode == 0
354+
355+
356+ def test_target(cfg: Cfg, target: Target) -> TargetResult:
357+ """Run tests for a single target."""
239358 start = time.time()
240359 env = os.environ.copy()
241360 env.setdefault("RUSTFLAGS", "")
@@ -261,14 +380,15 @@ def test_target(cfg: Cfg, target: Target):
261380 if not target.dist:
262381 # If we can't download a `core`, we need to build it
263382 cmd += ["-Zbuild-std=core"]
264- # FIXME: With `build-std` feature, `compiler_builtins` emits a lot of lint warnings.
383+ # FIXME: With `the build-std` feature, `compiler_builtins` emits a lot of
384+ # lint warnings.
265385 env["RUSTFLAGS"] += " -Aimproper_ctypes_definitions"
266386 else:
267387 run(["rustup", "target", "add", tname, "--toolchain", cfg.toolchain_name])
268388
269389 # Test with expected combinations of features
270390 run(cmd, env=env)
271- run(cmd + [ "--features=extra_traits"], env=env)
391+ run([* cmd, "--features=extra_traits"], env=env)
272392
273393 # Check with different env for 64-bit time_t
274394 if target_os == "linux" and target_bits == "32":
@@ -286,49 +406,44 @@ def test_target(cfg: Cfg, target: Target):
286406 run(cmd, env=env | {"RUST_LIBC_UNSTABLE_MUSL_V1_2_3": "1"})
287407
288408 # Test again without default features, i.e. without `std`
289- run(cmd + [ "--no-default-features"])
290- run(cmd + [ "--no-default-features", "--features=extra_traits"])
409+ run([* cmd, "--no-default-features"])
410+ run([* cmd, "--no-default-features", "--features=extra_traits"])
291411
292412 # Ensure the crate will build when used as a dependency of `std`
293413 if cfg.nightly():
294- run(cmd + [ "--no-default-features", "--features=rustc-dep-of-std"])
414+ run([* cmd, "--no-default-features", "--features=rustc-dep-of-std"])
295415
296416 # For freebsd targets, check with the different versions we support
297417 # if on nightly or stable
298418 if "freebsd" in tname and cfg.toolchain >= Toolchain.STABLE:
299419 for version in FREEBSD_VERSIONS:
300420 run(cmd, env=env | {"RUST_LIBC_UNSTABLE_FREEBSD_VERSION": str(version)})
301421 run(
302- cmd + [ "--no-default-features"],
422+ [* cmd, "--no-default-features"],
303423 env=env | {"RUST_LIBC_UNSTABLE_FREEBSD_VERSION": str(version)},
304424 )
305425
306- is_stable = cfg.toolchain == Toolchain.STABLE
307- # FIXME(semver): can't pass `--target` to `cargo-semver-checks` so we restrict to
308- # the host target
309- is_host = tname == cfg.host_target
310- if is_stable and is_host:
311- eprint("Running semver checks")
312- run(
313- [
314- "cargo",
315- "semver-checks",
316- "--only-explicit-features",
317- "--features=std,extra_traits",
318- ]
319- )
320- else:
426+ if cfg.skip_semver:
321427 eprint("Skipping semver checks")
428+ semver_ok = True
429+ else:
430+ semver_ok = do_semver_checks(cfg, target)
322431
323432 elapsed = round(time.time() - start, 2)
324433 eprint(f"Finished checking target {tname} in {elapsed} seconds")
434+ return TargetResult(target=target, semver_ok=semver_ok)
325435
326436
327- def main():
437+ def main() -> None :
328438 p = argparse.ArgumentParser()
329439 p.add_argument("--toolchain", required=True, help="Rust toolchain")
330440 p.add_argument("--only", help="only targets matching this regex")
331441 p.add_argument("--skip", help="skip targets matching this regex")
442+ p.add_argument("--skip-semver", help="don't run semver checks")
443+ p.add_argument(
444+ "--baseline-crate-dir",
445+ help="specify the directory of the crate to run semver checks against",
446+ )
332447 p.add_argument(
333448 "--half",
334449 type=int,
@@ -337,7 +452,11 @@ def main():
337452 )
338453 args = p.parse_args()
339454
340- cfg = Cfg(toolchain_name=args.toolchain)
455+ cfg = Cfg(
456+ toolchain_name=args.toolchain,
457+ baseline_crate_dir=args.baseline_crate_dir and Path(args.baseline_crate_dir),
458+ skip_semver=args.skip_semver,
459+ )
341460 eprint(f"Config: {cfg}")
342461 eprint("Python version: ", sys.version)
343462 check_dup_targets()
@@ -373,16 +492,25 @@ def main():
373492 total = len(targets)
374493 eprint(f"Targets to run: {total}")
375494 assert total > 0, "some tests should be run"
495+ target_results: list[TargetResult] = []
376496
377497 for i, target in enumerate(targets):
378498 at = i + 1
379499 eprint(f"::group::Target: {target.name} ({at}/{total})")
380500 eprint(f"{ESC_CYAN}Checking target {target} ({at}/{total}){ESC_END}")
381- test_target(cfg, target)
501+ res = test_target(cfg, target)
502+ target_results.append(res)
382503 eprint("::endgroup::")
383504
384505 elapsed = round(time.time() - start, 2)
385- eprint(f"Checked {total} targets in {elapsed} seconds")
506+
507+ semver_failures = [t.target.name for t in target_results if not t.semver_ok]
508+ if len(semver_failures) != 0:
509+ eprint(f"\n{ESC_YELLOW}Some targets had semver failures:{ESC_END}")
510+ for t in semver_failures:
511+ eprint(f"* {t}")
512+
513+ eprint(f"\nChecked {total} targets in {elapsed} seconds")
386514
387515
388516main()
0 commit comments