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,107 @@ 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:
337+ # https://github.com/obi1kenobi/cargo-semver-checks/issues/827
338+ res = run (
339+ [
340+ "cargo" ,
341+ "semver-checks" ,
342+ "--baseline-rustdoc" ,
343+ baseline ,
344+ "--current-rustdoc" ,
345+ current ,
346+ # For now, everything is a patch
347+ "--release-type=patch" ,
348+ ],
349+ check = False ,
350+ )
351+
352+ # If this job failed, we can't fail CI because it may have been a false positive.
353+ # But at least we can make an explicit note of it.
354+ return res .returncode == 0
355+
356+
357+ def test_target (cfg : Cfg , target : Target ) -> TargetResult :
358+ """Run tests for a single target."""
239359 start = time .time ()
240360 env = os .environ .copy ()
241361 env .setdefault ("RUSTFLAGS" , "" )
@@ -261,14 +381,15 @@ def test_target(cfg: Cfg, target: Target):
261381 if not target .dist :
262382 # If we can't download a `core`, we need to build it
263383 cmd += ["-Zbuild-std=core" ]
264- # FIXME: With `build-std` feature, `compiler_builtins` emits a lot of lint warnings.
384+ # FIXME: With `the build-std` feature, `compiler_builtins` emits a lot of
385+ # lint warnings.
265386 env ["RUSTFLAGS" ] += " -Aimproper_ctypes_definitions"
266387 else :
267388 run (["rustup" , "target" , "add" , tname , "--toolchain" , cfg .toolchain_name ])
268389
269390 # Test with expected combinations of features
270391 run (cmd , env = env )
271- run (cmd + [ "--features=extra_traits" ], env = env )
392+ run ([ * cmd , "--features=extra_traits" ], env = env )
272393
273394 # Check with different env for 64-bit time_t
274395 if target_os == "linux" and target_bits == "32" :
@@ -286,49 +407,44 @@ def test_target(cfg: Cfg, target: Target):
286407 run (cmd , env = env | {"RUST_LIBC_UNSTABLE_MUSL_V1_2_3" : "1" })
287408
288409 # Test again without default features, i.e. without `std`
289- run (cmd + [ "--no-default-features" ])
290- run (cmd + [ "--no-default-features" , "--features=extra_traits" ])
410+ run ([ * cmd , "--no-default-features" ])
411+ run ([ * cmd , "--no-default-features" , "--features=extra_traits" ])
291412
292413 # Ensure the crate will build when used as a dependency of `std`
293414 if cfg .nightly ():
294- run (cmd + [ "--no-default-features" , "--features=rustc-dep-of-std" ])
415+ run ([ * cmd , "--no-default-features" , "--features=rustc-dep-of-std" ])
295416
296417 # For freebsd targets, check with the different versions we support
297418 # if on nightly or stable
298419 if "freebsd" in tname and cfg .toolchain >= Toolchain .STABLE :
299420 for version in FREEBSD_VERSIONS :
300421 run (cmd , env = env | {"RUST_LIBC_UNSTABLE_FREEBSD_VERSION" : str (version )})
301422 run (
302- cmd + [ "--no-default-features" ],
423+ [ * cmd , "--no-default-features" ],
303424 env = env | {"RUST_LIBC_UNSTABLE_FREEBSD_VERSION" : str (version )},
304425 )
305426
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 :
427+ if cfg .skip_semver :
321428 eprint ("Skipping semver checks" )
429+ semver_ok = True
430+ else :
431+ semver_ok = do_semver_checks (cfg , target )
322432
323433 elapsed = round (time .time () - start , 2 )
324434 eprint (f"Finished checking target { tname } in { elapsed } seconds" )
435+ return TargetResult (target = target , semver_ok = semver_ok )
325436
326437
327- def main ():
438+ def main () -> None :
328439 p = argparse .ArgumentParser ()
329440 p .add_argument ("--toolchain" , required = True , help = "Rust toolchain" )
330441 p .add_argument ("--only" , help = "only targets matching this regex" )
331442 p .add_argument ("--skip" , help = "skip targets matching this regex" )
443+ p .add_argument ("--skip-semver" , help = "don't run semver checks" )
444+ p .add_argument (
445+ "--baseline-crate-dir" ,
446+ help = "specify the directory of the crate to run semver checks against" ,
447+ )
332448 p .add_argument (
333449 "--half" ,
334450 type = int ,
@@ -337,7 +453,11 @@ def main():
337453 )
338454 args = p .parse_args ()
339455
340- cfg = Cfg (toolchain_name = args .toolchain )
456+ cfg = Cfg (
457+ toolchain_name = args .toolchain ,
458+ baseline_crate_dir = args .baseline_crate_dir and Path (args .baseline_crate_dir ),
459+ skip_semver = args .skip_semver ,
460+ )
341461 eprint (f"Config: { cfg } " )
342462 eprint ("Python version: " , sys .version )
343463 check_dup_targets ()
@@ -373,16 +493,25 @@ def main():
373493 total = len (targets )
374494 eprint (f"Targets to run: { total } " )
375495 assert total > 0 , "some tests should be run"
496+ target_results : list [TargetResult ] = []
376497
377498 for i , target in enumerate (targets ):
378499 at = i + 1
379500 eprint (f"::group::Target: { target .name } ({ at } /{ total } )" )
380501 eprint (f"{ ESC_CYAN } Checking target { target } ({ at } /{ total } ){ ESC_END } " )
381- test_target (cfg , target )
502+ res = test_target (cfg , target )
503+ target_results .append (res )
382504 eprint ("::endgroup::" )
383505
384506 elapsed = round (time .time () - start , 2 )
385- eprint (f"Checked { total } targets in { elapsed } seconds" )
507+
508+ semver_failures = [t .target .name for t in target_results if not t .semver_ok ]
509+ if len (semver_failures ) != 0 :
510+ eprint (f"\n { ESC_YELLOW } Some targets had semver failures:{ ESC_END } " )
511+ for t in semver_failures :
512+ eprint (f"* { t } " )
513+
514+ eprint (f"\n Checked { total } targets in { elapsed } seconds" )
386515
387516
388517main ()
0 commit comments