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,7 @@ 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 ]
3841
3942 def __post_init__ (self ):
4043 rustc_output = check_output (["rustc" , f"+{ self .toolchain_name } " , "-vV" ])
@@ -66,6 +69,14 @@ def __post_init__(self):
6669 self .min_toolchain = Toolchain .NIGHTLY
6770
6871
72+ @dataclass
73+ class TargetResult :
74+ """Not all checks exit immediately, so failures are reported here."""
75+
76+ target : Target
77+ semver_ok : bool
78+
79+
6980FREEBSD_VERSIONS = [11 , 12 , 13 , 14 , 15 ]
7081
7182TARGETS = [
@@ -200,13 +211,13 @@ def __post_init__(self):
200211]
201212
202213
203- def eprint (* args , ** kw ):
214+ def eprint (* args , ** kw ) -> None :
204215 print (* args , file = sys .stderr , ** kw )
205216
206217
207- def xtrace (args : list [str ], / , env : Optional [dict [str , str ]]):
218+ def xtrace (args : Sequence [str | Path ], / , env : Optional [dict [str , str ]]) -> None :
208219 """Print commands before running them."""
209- astr = " " .join (args )
220+ astr = " " .join (str ( arg ) for arg in args )
210221 if env is None :
211222 eprint (f"+ { astr } " )
212223 else :
@@ -215,17 +226,25 @@ def xtrace(args: list[str], /, env: Optional[dict[str, str]]):
215226 eprint (f"+ { estr } { astr } " )
216227
217228
218- def check_output (args : list [str ], / , env : Optional [dict [str , str ]] = None ) -> str :
229+ def check_output (
230+ args : Sequence [str | Path ], / , env : Optional [dict [str , str ]] = None
231+ ) -> str :
219232 xtrace (args , env = env )
220233 return sp .check_output (args , env = env , encoding = "utf8" )
221234
222235
223- def run (args : list [str ], / , env : Optional [dict [str , str ]] = None ):
236+ def run (
237+ args : Sequence [str | Path ],
238+ / ,
239+ env : Optional [dict [str , str ]] = None ,
240+ check : bool = True ,
241+ ) -> sp .CompletedProcess :
224242 xtrace (args , env = env )
225- sp .run (args , env = env , check = True )
243+ return sp .run (args , env = env , check = check )
226244
227245
228- def check_dup_targets ():
246+ def check_dup_targets () -> None :
247+ """Ensure there are no duplicate targets in the list."""
229248 all = set ()
230249 duplicates = set ()
231250 for target in TARGETS :
@@ -235,7 +254,83 @@ def check_dup_targets():
235254 assert len (duplicates ) == 0 , f"duplicate targets: { duplicates } "
236255
237256
238- def test_target (cfg : Cfg , target : Target ):
257+ def do_semver_checks (cfg : Cfg , target : Target ) -> bool :
258+ tname = target .name
259+ if cfg .toolchain != Toolchain .STABLE :
260+ eprint ("Skipping semver checks" )
261+ return True
262+
263+ if not target .dist :
264+ eprint ("Skipping semver checks on non-dist target" )
265+ return True
266+
267+ # FIXME(semver): This is what we actually want to be doing on all targets, but
268+ # `--target` doesn't work right with semver-checks.
269+ if tname == cfg .host_target :
270+ eprint ("Running semver checks on host" )
271+ run (
272+ [
273+ "cargo" ,
274+ "semver-checks" ,
275+ "--only-explicit-features" ,
276+ "--features=std,extra_traits" ,
277+ "--release-type=patch" ,
278+ ],
279+ )
280+
281+ if cfg .baseline_crate_dir is None :
282+ eprint (
283+ "Non-host target: --baseline-crate-dir must be specified to \
284+ run semver-checks"
285+ )
286+ return True
287+
288+ # Since semver-checks doesn't work with `--target`, we build the json our self and
289+ # hand it over.
290+ eprint ("Running semver checks with cross compilation" )
291+
292+ env = os .environ .copy ()
293+ env .setdefault ("RUSTFLAGS" , "" )
294+ env .setdefault ("RUSTDOCFLAGS" , "" )
295+ # Needed for rustdoc json
296+ env ["RUSTC_BOOTSTRAP" ] = "1"
297+ # Unset everything that would cause us to get warnings for the baseline
298+ env ["RUSTFLAGS" ] += " -Awarnings"
299+ env ["RUSTDOCFLAGS" ] += " -Awarnings"
300+ env .pop ("LIBC_CI" , None )
301+
302+ cmd = ["cargo" , "rustdoc" , "--target" , tname ]
303+ rustdoc_args = ["--" , "-Zunstable-options" , "--output-format=json" ]
304+
305+ # Build the current crate and the baseline crate, which CI should have downloaded
306+ run ([* cmd , * rustdoc_args ], env = env )
307+ run (
308+ [* cmd , "--manifest-path" , cfg .baseline_crate_dir / "Cargo.toml" , * rustdoc_args ],
309+ env = env ,
310+ )
311+
312+ baseline = cfg .baseline_crate_dir / "target" / tname / "doc" / "libc.json"
313+ current = Path ("target" ) / tname / "doc" / "libc.json"
314+
315+ res = run (
316+ [
317+ "cargo" ,
318+ "semver-checks" ,
319+ "--baseline-rustdoc" ,
320+ baseline ,
321+ "--current-rustdoc" ,
322+ current ,
323+ # For now, everything is a patch
324+ "--release-type=patch" ,
325+ ],
326+ check = False ,
327+ )
328+
329+ return res .returncode == 0
330+
331+
332+ def test_target (cfg : Cfg , target : Target ) -> TargetResult :
333+ """Run tests for a single target."""
239334 start = time .time ()
240335 env = os .environ .copy ()
241336 env .setdefault ("RUSTFLAGS" , "" )
@@ -261,14 +356,15 @@ def test_target(cfg: Cfg, target: Target):
261356 if not target .dist :
262357 # If we can't download a `core`, we need to build it
263358 cmd += ["-Zbuild-std=core" ]
264- # FIXME: With `build-std` feature, `compiler_builtins` emits a lot of lint warnings.
359+ # FIXME: With `the build-std` feature, `compiler_builtins` emits a lot of
360+ # lint warnings.
265361 env ["RUSTFLAGS" ] += " -Aimproper_ctypes_definitions"
266362 else :
267363 run (["rustup" , "target" , "add" , tname , "--toolchain" , cfg .toolchain_name ])
268364
269365 # Test with expected combinations of features
270366 run (cmd , env = env )
271- run (cmd + [ "--features=extra_traits" ], env = env )
367+ run ([ * cmd , "--features=extra_traits" ], env = env )
272368
273369 # Check with different env for 64-bit time_t
274370 if target_os == "linux" and target_bits == "32" :
@@ -286,49 +382,39 @@ def test_target(cfg: Cfg, target: Target):
286382 run (cmd , env = env | {"RUST_LIBC_UNSTABLE_MUSL_V1_2_3" : "1" })
287383
288384 # Test again without default features, i.e. without `std`
289- run (cmd + [ "--no-default-features" ])
290- run (cmd + [ "--no-default-features" , "--features=extra_traits" ])
385+ run ([ * cmd , "--no-default-features" ])
386+ run ([ * cmd , "--no-default-features" , "--features=extra_traits" ])
291387
292388 # Ensure the crate will build when used as a dependency of `std`
293389 if cfg .nightly ():
294- run (cmd + [ "--no-default-features" , "--features=rustc-dep-of-std" ])
390+ run ([ * cmd , "--no-default-features" , "--features=rustc-dep-of-std" ])
295391
296392 # For freebsd targets, check with the different versions we support
297393 # if on nightly or stable
298394 if "freebsd" in tname and cfg .toolchain >= Toolchain .STABLE :
299395 for version in FREEBSD_VERSIONS :
300396 run (cmd , env = env | {"RUST_LIBC_UNSTABLE_FREEBSD_VERSION" : str (version )})
301397 run (
302- cmd + [ "--no-default-features" ],
398+ [ * cmd , "--no-default-features" ],
303399 env = env | {"RUST_LIBC_UNSTABLE_FREEBSD_VERSION" : str (version )},
304400 )
305401
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 :
321- eprint ("Skipping semver checks" )
402+ semver_ok = do_semver_checks (cfg , target )
322403
323404 elapsed = round (time .time () - start , 2 )
324405 eprint (f"Finished checking target { tname } in { elapsed } seconds" )
406+ return TargetResult (target = target , semver_ok = semver_ok )
325407
326408
327- def main ():
409+ def main () -> None :
328410 p = argparse .ArgumentParser ()
329411 p .add_argument ("--toolchain" , required = True , help = "Rust toolchain" )
330412 p .add_argument ("--only" , help = "only targets matching this regex" )
331413 p .add_argument ("--skip" , help = "skip targets matching this regex" )
414+ p .add_argument (
415+ "--baseline-crate-dir" ,
416+ help = "specify the directory of the crate to run semver checks against" ,
417+ )
332418 p .add_argument (
333419 "--half" ,
334420 type = int ,
@@ -337,7 +423,10 @@ def main():
337423 )
338424 args = p .parse_args ()
339425
340- cfg = Cfg (toolchain_name = args .toolchain )
426+ cfg = Cfg (
427+ toolchain_name = args .toolchain ,
428+ baseline_crate_dir = args .baseline_crate_dir and Path (args .baseline_crate_dir ),
429+ )
341430 eprint (f"Config: { cfg } " )
342431 eprint ("Python version: " , sys .version )
343432 check_dup_targets ()
@@ -373,16 +462,25 @@ def main():
373462 total = len (targets )
374463 eprint (f"Targets to run: { total } " )
375464 assert total > 0 , "some tests should be run"
465+ target_results : list [TargetResult ] = []
376466
377467 for i , target in enumerate (targets ):
378468 at = i + 1
379469 eprint (f"::group::Target: { target .name } ({ at } /{ total } )" )
380470 eprint (f"{ ESC_CYAN } Checking target { target } ({ at } /{ total } ){ ESC_END } " )
381- test_target (cfg , target )
471+ res = test_target (cfg , target )
472+ target_results .append (res )
382473 eprint ("::endgroup::" )
383474
384475 elapsed = round (time .time () - start , 2 )
385- eprint (f"Checked { total } targets in { elapsed } seconds" )
476+
477+ semver_failures = [t .target .name for t in target_results if not t .semver_ok ]
478+ if len (semver_failures ) != 0 :
479+ eprint (f"\n { ESC_YELLOW } Some targets had semver failures:{ ESC_END } " )
480+ for t in semver_failures :
481+ eprint (f"* { t } " )
482+
483+ eprint (f"\n Checked { total } targets in { elapsed } seconds" )
386484
387485
388486main ()
0 commit comments