Skip to content

Commit 72c8557

Browse files
committed
ci: Run semver checks even on non-host targets
1 parent 83bd32f commit 72c8557

File tree

3 files changed

+173
-35
lines changed

3 files changed

+173
-35
lines changed

.github/workflows/ci.yaml

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@ env:
1717
RUSTDOCFLAGS: -Dwarnings
1818
RUSTFLAGS: -Dwarnings
1919
RUST_BACKTRACE: full
20+
SERIES: "1.0" # 1.0 alpha series
2021

2122
defaults:
2223
run:
@@ -79,6 +80,10 @@ jobs:
7980
uses: taiki-e/install-action@cargo-semver-checks
8081
if: matrix.toolchain == 'stable'
8182

83+
- name: Retrieve semver baseline
84+
if: matrix.toolchain == 'stable'
85+
run: ./ci/semver.sh
86+
8287
# FIXME(ci): These `du` statements are temporary for debugging cache
8388
- name: Target size before restoring cache
8489
run: du -sh target | sort -k 2 || true
@@ -100,6 +105,7 @@ jobs:
100105
101106
python3 ci/verify-build.py \
102107
--toolchain "$TOOLCHAIN" \
108+
${BASELINE_CRATE_DIR:+"--baseline-crate-dir" "$BASELINE_CRATE_DIR"} \
103109
${{ matrix.only && format('--only "{0}"', matrix.only) }} \
104110
${{ matrix.half && format('--half "{0}"', matrix.half) }}
105111
- name: Target size after job completion

ci/semver.sh

Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,34 @@
1+
#!/bin/bash
2+
# Download a baseline crate to run semver checks against
3+
4+
set -euxo pipefail
5+
6+
# https://crates.io/data-access#api
7+
8+
# Need to include versions to get the default and stable versions
9+
meta=$(curl -L https://index.crates.io/li/bc/libc)
10+
11+
# Versions to check against
12+
if [ "${SERIES:-}" = "0.2" ]; then
13+
pat="^0.2"
14+
elif [ "${SERIES:-}" = "1.0" ]; then
15+
pat="^1.0"
16+
else
17+
echo "SERIES must be set to either '0.2' or '1.0'"
18+
exit 1
19+
fi
20+
21+
# Find the most recent version matching a pattern.
22+
release=$(
23+
echo "$meta" |
24+
jq -er --slurp --arg pat "$pat" 'map(select(.vers | test($pat))) | last'
25+
)
26+
version=$(echo "$release" | jq -r '.vers')
27+
28+
crate_dir="libc-$version"
29+
curl -L "https://static.crates.io/crates/libc/libc-$version.crate" | tar xzf -
30+
31+
# Need to convince Cargo it's not part of our workspace
32+
echo '[workspace]' >> "$crate_dir/Cargo.toml"
33+
34+
echo "BASELINE_CRATE_DIR=$(realpath "$crate_dir")" >> "$GITHUB_ENV"

ci/verify-build.py

Lines changed: 133 additions & 35 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,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+
6980
FREEBSD_VERSIONS = [11, 12, 13, 14, 15]
7081

7182
TARGETS = [
@@ -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"\nChecked {total} targets in {elapsed} seconds")
386484

387485

388486
main()

0 commit comments

Comments
 (0)