Skip to content

Commit 8a6762f

Browse files
committed
ci: Run semver checks even on non-host targets
Add a version of semver checks that run on non-host targets by building rustdoc JSON output and passing that to `cargo-semver-checks`. Unfortunately this doesn't have a way to suppress false positives, so we need to leave the checks as optional rather than enforced for now (i.e. the exit code isn't checked).
1 parent c100954 commit 8a6762f

File tree

4 files changed

+205
-34
lines changed

4 files changed

+205
-34
lines changed

.github/workflows/ci.yaml

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -75,6 +75,10 @@ jobs:
7575
uses: taiki-e/install-action@cargo-semver-checks
7676
if: matrix.toolchain == 'stable'
7777

78+
- name: Retrieve semver baseline
79+
if: matrix.toolchain == 'stable'
80+
run: ./ci/prep-semver-baseline.sh
81+
7882
# FIXME(ci): These `du` statements are temporary for debugging cache
7983
- name: Target size before restoring cache
8084
run: du -sh target | sort -k 2 || true
@@ -91,6 +95,7 @@ jobs:
9195
[ "${{ matrix.toolchain }}" = "1.63.0" ] && export RUSTFLAGS=""
9296
python3 ci/verify-build.py \
9397
--toolchain "$TOOLCHAIN" \
98+
${BASELINE_CRATE_DIR:+"--baseline-crate-dir" "$BASELINE_CRATE_DIR"} \
9499
${{ matrix.only && format('--only "{0}"', matrix.only) }} \
95100
${{ matrix.half && format('--half "{0}"', matrix.half) }}
96101
- name: Target size after job completion

Cargo.toml

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -182,3 +182,5 @@ used_underscore_binding = "allow"
182182
[package.metadata.cargo-semver-checks.lints]
183183
# Alignment is an internal detail that users must not rely upon
184184
repr_align_removed = "warn"
185+
# We deprecate things all the time
186+
global_value_marked_deprecated = "warn"

ci/prep-semver-baseline.sh

Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,36 @@
1+
#!/bin/bash
2+
# Download a baseline crate to run semver checks against
3+
4+
set -euxo pipefail
5+
6+
# Retrieve the index for libc
7+
index=$(curl -L https://index.crates.io/li/bc/libc)
8+
9+
# Regex for versions matching what we want to check against
10+
if [ "${GITHUB_BASE_REF:-}" = "libc-0.2" ]; then
11+
pat="^0.2"
12+
elif [ "${GITHUB_BASE_REF:-}" = "main" ]; then
13+
pat="^1.0"
14+
else
15+
echo "GITHUB_BASE_REFmust be set to either 'libc-0.2' or 'main'"
16+
exit 1
17+
fi
18+
19+
# Find the most recent version matching a pattern.
20+
version=$(
21+
echo "$index" |
22+
jq -er --slurp --arg pat "$pat" '
23+
map(select(.vers | test($pat)))
24+
| last
25+
| debug("version:", .)
26+
| .vers
27+
'
28+
)
29+
30+
libc_cache="${XDG_CACHE_DIR:-$HOME/.cache}/libc-ci/"
31+
mkdir -p "$libc_cache"
32+
33+
curl -L "https://static.crates.io/crates/libc/libc-$version.crate" | tar xzf - -C "$libc_cache"
34+
crate_dir="$libc_cache/libc-$version"
35+
36+
echo "BASELINE_CRATE_DIR=$crate_dir" >> "$GITHUB_ENV"

ci/verify-build.py

Lines changed: 162 additions & 34 deletions
Original file line numberDiff line numberDiff line change
@@ -9,9 +9,11 @@
99
import time
1010
from dataclasses import dataclass, field
1111
from 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"
1517
ESC_CYAN = "\033[1;36m"
1618
ESC_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+
6981
FREEBSD_VERSIONS = [11, 12, 13, 14, 15]
7082

7183
TARGETS = [
@@ -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

388516
main()

0 commit comments

Comments
 (0)