From 0e7cd9f35343a09ac1abf683dcd14667adedd837 Mon Sep 17 00:00:00 2001 From: Vyncint Ng <115854244+vyncint@users.noreply.github.com> Date: Thu, 10 Sep 2026 18:07:01 +0700 Subject: [PATCH] docs: make the README's links absolute, so eleven crates.io pages stop 404ing Every one of the eleven crates sets `readme = "../../README.md"`, so this file ships as each one's readme. crates.io rewrites a relative link against the crate's directory in the repository rather than the repository root, so `docs/SAFETY.md` was served as https://github.com/vyncint/launchbound/blob/HEAD/crates/launchbound-space/docs/SAFETY.md which does not exist. The same eight links were dead on all eleven published 2.2.0 pages -- checked crate by crate, not inferred from one: `docs/SAFETY.md`, `docs/ARCHITECTURE.md`, `docs/LIMITATIONS.md` (twice, once with an anchor), the Action directory, `rust-toolchain.toml` and both licences. Nothing in this repository could see it. The same file renders correctly on GitHub, where it really is at the root, so the only broken surface was the one a reader arriving from crates.io meets first -- and `docs/SAFETY.md`, the document the README calls "the product", was among the dead. Absolute links are the only shape correct from both roots. The Action's is `tree/main/action`, not `blob/`, because it is a directory: GitHub 301s the wrong one to the right one rather than failing, so it would have "worked" and still been the wrong URL to publish. `scripts/check-readme-links.sh` keeps it that way and checks three things, each exercised until it failed: a relative link is refused with the URL to use instead; an absolute target that is not in the checkout is named; and `blob` versus `tree` is held to what the path actually is, which is the one a status-code checker cannot see. Fixed for future releases only: crates.io renders each version's readme as published, so the 2.2.0 pages stay broken. This commit also carries the 2.2.1 version bump (workspace plus the eight internal pins) and the CHANGELOG entry. Signed-off-by: Vyncint Ng <115854244+vyncint@users.noreply.github.com> --- CHANGELOG.md | 28 ++++++++++++++ Cargo.lock | 22 +++++------ Cargo.toml | 18 ++++----- README.md | 20 +++++----- action/action.yml | 2 +- justfile | 10 ++++- scripts/check-readme-links.sh | 71 +++++++++++++++++++++++++++++++++++ 7 files changed, 139 insertions(+), 32 deletions(-) create mode 100755 scripts/check-readme-links.sh diff --git a/CHANGELOG.md b/CHANGELOG.md index e614aed..bc010c8 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -9,6 +9,34 @@ change measured timings are marked `bench:`. ## [Unreleased] +## [2.2.1] - 2026-09-10 + +### Fixed + +- **Every relative link in the README was a 404 on all eleven crates.io + pages.** All eleven crates set `readme = "../../README.md"`, so this file + ships as each one's readme — and crates.io rewrites a *relative* link + against the crate's directory in the repository rather than the repository + root. `docs/SAFETY.md` was served as + `…/blob/HEAD/crates/launchbound-space/docs/SAFETY.md`, which does not + exist, and the same eight links were dead on every crate: `docs/SAFETY.md`, + `docs/ARCHITECTURE.md`, `docs/LIMITATIONS.md` (twice, once with an + anchor), the Action directory, `rust-toolchain.toml` and both licences. + + Nothing here could see it. The same file renders correctly on GitHub, + where it really is at the root, so the only broken surface was the one + every reader arriving from crates.io meets first — and `docs/SAFETY.md`, + the document the README calls "the product", was among the dead. + + The links are absolute now. `scripts/check-readme-links.sh` (in `just ci`) + refuses a relative one, checks every target still exists in the checkout, + and checks `blob` versus `tree` — GitHub *redirects* a `blob` URL for a + directory rather than failing, so a checker that only reads status codes + calls it fine. + + Reaches future releases only: crates.io renders each version's readme as + published, so the 2.2.0 pages stay broken. + ## [2.2.0] - 2026-09-09 The cuda-oxide pin was 133 commits and one toolchain behind, and the watch diff --git a/Cargo.lock b/Cargo.lock index 926c944..13a0a10 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1121,7 +1121,7 @@ checksum = "bf36173d4167ed999940f804952e6b08197cae5ad5d572eb4db150ce8ad5d58f" [[package]] name = "launchbound-bench" -version = "2.2.0" +version = "2.2.1" dependencies = [ "anyhow", "launchbound-space", @@ -1135,7 +1135,7 @@ dependencies = [ [[package]] name = "launchbound-build" -version = "2.2.0" +version = "2.2.1" dependencies = [ "launchbound-space", "serde", @@ -1146,7 +1146,7 @@ dependencies = [ [[package]] name = "launchbound-cli" -version = "2.2.0" +version = "2.2.1" dependencies = [ "anyhow", "clap", @@ -1164,7 +1164,7 @@ dependencies = [ [[package]] name = "launchbound-metal" -version = "2.2.0" +version = "2.2.1" dependencies = [ "launchbound-bench", "launchbound-space", @@ -1175,7 +1175,7 @@ dependencies = [ [[package]] name = "launchbound-model" -version = "2.2.0" +version = "2.2.1" dependencies = [ "launchbound-space", "serde", @@ -1185,7 +1185,7 @@ dependencies = [ [[package]] name = "launchbound-prune" -version = "2.2.0" +version = "2.2.1" dependencies = [ "launchbound-build", "launchbound-space", @@ -1198,7 +1198,7 @@ dependencies = [ [[package]] name = "launchbound-report" -version = "2.2.0" +version = "2.2.1" dependencies = [ "insta", "jsonschema", @@ -1210,7 +1210,7 @@ dependencies = [ [[package]] name = "launchbound-runner" -version = "2.2.0" +version = "2.2.1" dependencies = [ "launchbound-bench", "launchbound-search", @@ -1218,7 +1218,7 @@ dependencies = [ [[package]] name = "launchbound-search" -version = "2.2.0" +version = "2.2.1" dependencies = [ "launchbound-bench", "proptest", @@ -1226,7 +1226,7 @@ dependencies = [ [[package]] name = "launchbound-space" -version = "2.2.0" +version = "2.2.1" dependencies = [ "proptest", "serde", @@ -1237,7 +1237,7 @@ dependencies = [ [[package]] name = "launchbound-tui" -version = "2.2.0" +version = "2.2.1" dependencies = [ "anyhow", "crossterm", diff --git a/Cargo.toml b/Cargo.toml index b42932e..ee3ffdf 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -20,7 +20,7 @@ members = [ exclude = ["corpus"] [workspace.package] -version = "2.2.0" +version = "2.2.1" edition = "2024" # MSRV for crates that do not require the pinned nightly (CONTRIBUTING.md); # set by ratatui 0.30. The analysis and compile paths require @@ -38,14 +38,14 @@ categories = ["development-tools", "development-tools::profiling"] # published crate is compatible with a release it was never built against -- # harmless for a path build, wrong as a record. They sat at 2.0.0 through the # whole 2.1.0 line; `just versions` (and a test) now refuse the drift. -launchbound-space = { path = "crates/launchbound-space", version = "2.2.0" } -launchbound-prune = { path = "crates/launchbound-prune", version = "2.2.0" } -launchbound-build = { path = "crates/launchbound-build", version = "2.2.0" } -launchbound-bench = { path = "crates/launchbound-bench", version = "2.2.0" } -launchbound-report = { path = "crates/launchbound-report", version = "2.2.0" } -launchbound-search = { path = "crates/launchbound-search", version = "2.2.0" } -launchbound-model = { path = "crates/launchbound-model", version = "2.2.0" } -launchbound-metal = { path = "crates/launchbound-metal", version = "2.2.0" } +launchbound-space = { path = "crates/launchbound-space", version = "2.2.1" } +launchbound-prune = { path = "crates/launchbound-prune", version = "2.2.1" } +launchbound-build = { path = "crates/launchbound-build", version = "2.2.1" } +launchbound-bench = { path = "crates/launchbound-bench", version = "2.2.1" } +launchbound-report = { path = "crates/launchbound-report", version = "2.2.1" } +launchbound-search = { path = "crates/launchbound-search", version = "2.2.1" } +launchbound-model = { path = "crates/launchbound-model", version = "2.2.1" } +launchbound-metal = { path = "crates/launchbound-metal", version = "2.2.1" } anyhow = "1" clap = { version = "4", features = ["derive"] } serde = { version = "1", features = ["derive"] } diff --git a/README.md b/README.md index de43080..aff0c94 100644 --- a/README.md +++ b/README.md @@ -1,9 +1,9 @@ # launchbound -[![license](https://img.shields.io/badge/license-MIT%20OR%20Apache--2.0-blue)](LICENSE-MIT) +[![license](https://img.shields.io/badge/license-MIT%20OR%20Apache--2.0-blue)](https://github.com/vyncint/launchbound/blob/main/LICENSE-MIT) [![crates.io](https://img.shields.io/crates/v/launchbound-cli?label=crates.io&color=orange)](https://crates.io/crates/launchbound-cli) -[![toolchain](https://img.shields.io/badge/toolchain-nightly--2026--08--28-orange)](rust-toolchain.toml) -[![GPU](https://img.shields.io/badge/GPU-not%20required-brightgreen)](docs/ARCHITECTURE.md) +[![toolchain](https://img.shields.io/badge/toolchain-nightly--2026--08--28-orange)](https://github.com/vyncint/launchbound/blob/main/rust-toolchain.toml) +[![GPU](https://img.shields.io/badge/GPU-not%20required-brightgreen)](https://github.com/vyncint/launchbound/blob/main/docs/ARCHITECTURE.md) [![CI](https://img.shields.io/github/actions/workflow/status/vyncint/launchbound/ci.yml?label=CI)](https://github.com/vyncint/launchbound/actions/workflows/ci.yml) A convergence-safe autotuner for Rust GPU kernels. @@ -15,7 +15,7 @@ NVIDIA it measures on real silicon. On Apple Silicon it measures on Metal. With no GPU at all it falls back to an analytical model — and says so. > Every design gate shipped with a measured result; the honest edges live -> in [docs/LIMITATIONS.md](docs/LIMITATIONS.md). Read that before trusting +> in [docs/LIMITATIONS.md](https://github.com/vyncint/launchbound/blob/main/docs/LIMITATIONS.md). Read that before trusting > a result. ## Why this tool exists @@ -80,7 +80,7 @@ either. The one `.maxntid` relationship the corpus does enforce (`block_x <= lb_max` in `stencil-1d`) holds because the kernel author wrote it as a constraint, not because launchbound knows what `.maxntid` means. A reader arriving from cuda-oxide's docs will assume otherwise; see -[docs/LIMITATIONS.md](docs/LIMITATIONS.md#launch_bounds-and-registers-are-not-validated). +[docs/LIMITATIONS.md](https://github.com/vyncint/launchbound/blob/main/docs/LIMITATIONS.md#launch_bounds-and-registers-are-not-validated). ## The pipeline @@ -113,7 +113,7 @@ For each candidate configuration at its own launch shape, `launchbound` runs A `reconverge` tool error (exit 2) is a hard stop for that candidate, never a pass by omission. The full rule, including why `--strict` is mandatory, lives -in [docs/SAFETY.md](docs/SAFETY.md) — that document is the product. A rejected +in [docs/SAFETY.md](https://github.com/vyncint/launchbound/blob/main/docs/SAFETY.md) — that document is the product. A rejected configuration is a feature, not a failure: the headline output includes every configuration that was *faster and refused*, with the rule ID and source span. @@ -166,7 +166,7 @@ shape make a barrier or a collective non-convergent, and does the static shared memory fit — and its verdict line says so. It has **no view of instruction availability**: whether the device code can be lowered for that part at all is `needs_cc` in `kernel.toml`, the author's claim, taken on -trust ([docs/LIMITATIONS.md](docs/LIMITATIONS.md)). +trust ([docs/LIMITATIONS.md](https://github.com/vyncint/launchbound/blob/main/docs/LIMITATIONS.md)). `apply` re-verifies what it emits through the gate before printing anything. `--no-verify` emits without it — for a machine that has the run directory @@ -189,7 +189,7 @@ missing reason is a usage error, not a warning. It is never the default, and `prune` needing no GPU is the reason it exists as its own verb: it is the only part of the pipeline a developer on a laptop can run, and it is the part that -finds the bugs. It also ships as a [GitHub Action](action/) — +finds the bugs. It also ships as a [GitHub Action](https://github.com/vyncint/launchbound/tree/main/action) — `uses: vyncint/launchbound/action@v2` puts the gate in your CI. ## Compared to the neighbours @@ -214,7 +214,7 @@ That report is re-runnable: `launchbound report --rejected`. ## Limitations -The honest list lives in [docs/LIMITATIONS.md](docs/LIMITATIONS.md) — read +The honest list lives in [docs/LIMITATIONS.md](https://github.com/vyncint/launchbound/blob/main/docs/LIMITATIONS.md) — read it before trusting a result. Highlights: a clean gate is **not a proof of correctness** (reconverge's documented limits are inherited wholesale, and the launch-shape classifier recognizes the measured `warp_id()` family); @@ -228,7 +228,7 @@ or not at all. ## License -Dual-licensed under [MIT](LICENSE-MIT) or [Apache-2.0](LICENSE-APACHE), at +Dual-licensed under [MIT](https://github.com/vyncint/launchbound/blob/main/LICENSE-MIT) or [Apache-2.0](https://github.com/vyncint/launchbound/blob/main/LICENSE-APACHE), at your option. Unless you explicitly state otherwise, any contribution intentionally submitted for inclusion in the work by you, as defined in the Apache-2.0 license, shall be dual licensed as above, without any additional diff --git a/action/action.yml b/action/action.yml index e829e9f..9893291 100644 --- a/action/action.yml +++ b/action/action.yml @@ -40,7 +40,7 @@ inputs: default: tool-error version: description: >- - Which launchbound release runs the gate, e.g. "2.2.0". The default, + Which launchbound release runs the gate, e.g. "2.2.1". The default, `latest`, installs the newest release on crates.io each run. Pin a number if you want the gate to change only when you say so. required: false diff --git a/justfile b/justfile index 6230c61..f14ddbd 100644 --- a/justfile +++ b/justfile @@ -6,7 +6,7 @@ default: ci # "all required jobs green" aggregator job in ci.yml — this recipe is the # aggregator, and the `ci` job runs it verbatim on both OSes — so a new gate # becomes required by being listed here. -ci: fmt-check clippy test docs deny schemas pins versions skill +ci: fmt-check clippy test docs deny schemas pins versions links skill # Cargo errors on a memberless virtual workspace, so the cargo recipes no-op # until the first crate lands in S1. `grep -c` prints 1 when packages is empty. @@ -63,6 +63,14 @@ pins: versions: ./scripts/check-versions.sh +# Every link in the README is absolute, names the right GitHub path kind, +# and points at something that exists. All eleven crates ship this file as +# their readme, and crates.io rewrites a relative link against the crate's +# directory -- so all eight were 404 on all eleven published 2.2.0 pages, +# and nothing here could see it. +links: + ./scripts/check-readme-links.sh + # Golden + JSON Schema validation of report documents (S4). schemas: cargo test -p launchbound-report --test schema_and_golden diff --git a/scripts/check-readme-links.sh b/scripts/check-readme-links.sh new file mode 100755 index 0000000..829c4ce --- /dev/null +++ b/scripts/check-readme-links.sh @@ -0,0 +1,71 @@ +#!/usr/bin/env bash +# Every link in the packaged README must be absolute, must name the right +# GitHub path kind, and must point at something that exists here. +# +# All eleven crates set `readme = "../../README.md"`, so this file is shipped +# as each one's readme and crates.io renders it there. crates.io rewrites a +# *relative* link against the crate's directory in the repository, not the +# repository root -- so `docs/SAFETY.md` became +# `…/blob/HEAD/crates/launchbound-space/docs/SAFETY.md`, and all eight +# relative links were 404 on all eleven published 2.2.0 pages. Nothing here +# could see it: the same file renders correctly on GitHub, where it *is* at +# the root. +# +# So: absolute links only, and the path each names must exist. The +# existence check is offline and deterministic, which is the point -- it +# catches a renamed or deleted target, and a link checker that only asks +# GitHub would not, because GitHub answers 200 for a redirect. +# +# `blob` vs `tree` is checked for the same reason: `blob/main/action/` is a +# directory and GitHub 301s it to `tree/main/action`. It resolves, so a +# status-code checker calls it fine; it is still the wrong URL to publish. +# +# Portability: macOS ships bash 3.2 and BSD grep/sed. +set -euo pipefail + +readme="${1:-README.md}" +root=$(cd "$(dirname "$0")/.." && pwd) +repo="https://github.com/vyncint/launchbound" +status=0 + +# `[text](target)` whose target is neither absolute nor a bare fragment. +relative=$(grep -oE '\]\([^)]+\)' "$readme" \ + | sed -E 's/^\]\(//; s/\)$//' \ + | grep -vE '^(https?:|#|mailto:)' || true) +if [ -n "$relative" ]; then + echo "$relative" | while IFS= read -r link; do + echo "README LINK: relative target \"$link\" — crates.io rewrites it against" >&2 + echo " crates//, where it does not exist. Use $repo/blob/main/$link" >&2 + done + status=1 +fi + +checked=0 +bad=0 +for url in $(grep -oE "$repo/(blob|tree)/main/[^)]+" "$readme" | sort -u); do + kind=$(printf '%s' "$url" | sed -E "s|^.*/(blob\|tree)/main/.*$|\1|") + path=$(printf '%s' "$url" | sed -E "s|^.*/(blob\|tree)/main/||; s|#.*$||; s|/$||") + checked=$((checked + 1)) + if [ ! -e "$root/$path" ]; then + echo "README LINK: \"$path\" is linked but not in the repository" >&2 + bad=$((bad + 1)) + continue + fi + # A directory is `tree`, a file is `blob`. GitHub redirects the wrong one + # rather than failing, so only this says so. + if [ -d "$root/$path" ] && [ "$kind" != tree ]; then + echo "README LINK: \"$path\" is a directory; use /tree/main/ not /blob/main/" >&2 + bad=$((bad + 1)) + elif [ -f "$root/$path" ] && [ "$kind" != blob ]; then + echo "README LINK: \"$path\" is a file; use /blob/main/ not /tree/main/" >&2 + bad=$((bad + 1)) + fi +done +if [ "$bad" -gt 0 ]; then + status=1 +fi + +if [ "$status" -eq 0 ]; then + echo "readme links: $checked absolute link(s), every target present and the right kind" +fi +exit "$status"