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"\n Checked { total } targets in { elapsed } seconds" )
386514
387515
388516main ()
0 commit comments