rucc-sysroot 0.30.9

Sysroot layout, header search paths and link lines for rucc, computed from the target and never from the host.
Documentation
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
//! What this release pins: one sysroot artifact per target, by URL and by hash.
//!
//! Design: `spec/cross-compile/13-distribution.md` section 13.2, which says every downloaded
//! artifact has a hash pinned in the rucc release, checked before use, with a mismatch being a hard
//! failure and no flag to get past it. Section 13.8 divides the work in three and the other two are
//! written: `rucc_driver::fetch` moves the bytes with a program the machine already has, and
//! `rucc_driver::install` decides whether what arrived is the right tree. This is the third, which
//! is the statement of what the right one is, and it is the half that makes the other two mean
//! anything.
//!
//! # Why it is here rather than with the two halves that use it
//!
//! Because it is read by something that cannot depend on the driver. The distribution manifest of
//! section 13.5 is generated by a build tool, the same way `docs/TARGETS.md` and `tests/link-lines`
//! are, and what it says about a target is what this table says plus what [`crate::Wall`] says. A
//! build tool that pulled in the whole driver to read three strings would be a layer violation
//! dressed up as convenience. It sits well here for a second reason as well: a pin is a fact about
//! a sysroot, which is what this crate is for, and the fetch and the install are what a driver does
//! with one.
//!
//! # Why the table is in the binary
//!
//! Because a hash that travels with the artifact is not a pin, and a hash in a file beside the
//! compiler is a hash whoever replaces the artifact can replace too. The release is the authority
//! for what an artifact of that release is, so the table is compiled into the release, which also
//! means an upgrade can change a URL without anything on the machine having to be told.
//!
//! It is a table rather than a computed URL for the same reason. A name built out of a version and
//! a tuple looks tidier and quietly says that every target's artifact is at a predictable address
//! forever, which is a promise about somebody else's file server. A row per target costs three
//! strings and says only what is true.
//!
//! # What is in it
//!
//! Eighteen rows, which are the three windows-gnu targets, four musl ones, eight glibc ones and three WASI ones. For windows-gnu,
//! `bin/mingw-headers` in `tamnd/rucc-cross` installs mingw-w64 14.0.0's headers, `bin/mingw-runtime`
//! builds the runtime, the import libraries and a static winpthreads into the `lib` directory beside
//! them, `bin/artifact` packs the tree, and the release `sysroots-2026-09-29` is where the files are.
//! They are built for the UCRT, and the same release has a `-msvcrt` archive of each that nothing
//! here pins. The archives are 15.2 MiB for x86_64, 14.9 MiB for i686 and 12.6 MiB for aarch64,
//! installing to 141 MB, 135 MB and 119 MB. The header half is the same in all three, because
//! mingw-w64 has no per architecture split and [`crate::Sysroot::splits_by_arch`] says so, and the
//! `lib` half is what differs.
//!
//! For musl, `bin/sysroot` builds musl 1.2.6 for x86_64, aarch64, riscv64 and armv7 hard float, and
//! the release `sysroots-2026-10-14` has the four archives, each about 2 MiB and installing to between
//! 7 and 13 MB. They hold musl's headers and its static libraries and start files, and not the Linux
//! uapi headers, which are [`KERNEL_HEADERS`] and fetched once for every Linux target. Both runs that
//! produced them, on two machines, packed the same bytes.
//!
//! For glibc, `bin/glibc-sysroot` puts together the merged header tree of every pinned release from
//! 2.28 to 2.44 with glibc 2.44's start files and `libc_nonshared.a`, for x86_64, i686, aarch64,
//! armv7 hard float, riscv64, powerpc64le, s390x and loongarch64, and the release
//! `sysroots-2026-10-14b` has the eight archives, each about 600 KB. The seven whose ABI has a glibc
//! older than 2.33 also carry `libc_nonshared_stat.a`, the `stat` family those releases kept in
//! `libc_nonshared.a`, which [`crate::link::LinkLine::glibc`] adds for a pin before 2.33. There is no `libc.so` in them,
//! because the driver writes the stubs itself, and one archive serves every release of its target,
//! which is [`pinned_for_target`]. Each family's `gnu/stubs.h` has the `__stub_` names that Debian's
//! build of each release lists, guarded by release, so `__stub_lchmod` is there for x86_64 before
//! 2.34 and not after. server3 built the archives, and server1 built the same header trees and the
//! same `libc_nonshared_stat.a`. Every file but the `gnu/stubs` ones is the same as in
//! `sysroots-2026-09-24b`, which server2 and server3 built to the same bytes.
//!
//! For WASI, `bin/wasi-sysroot` takes the C part of one preview out of the sysroot of wasi-sdk 34, `wasi-sysroot-34.0.tar.gz`, which it fetches by its sha256 `9d813544`. rucc does not build wasi-libc, because the bytes clang users link against are the bytes a rucc object has to link against too. The upstream archive is 119 MB and holds libc++ for four targets and two exception models, so the C headers, the static archives and the three start files of each preview are packed on their own, and the release `sysroots-2026-10-05` has the three archives, each about 1.5 MB. Every file in them is upstream's byte for byte. server2 and server3 produced the same bytes.
//!
//! Every other target is still unpublished, which is a statement about producers rather than about
//! this table: `--fetch` of one says so by name, and the day a tree for it is published is the day a
//! row for it is added here. The rows that are here are the first thing `--fetch` has ever had
//! anything to move, so they are also what the fetch and the install are tested against.

use std::path::{Path, PathBuf};

use rucc_tuple::{Env, Os, TargetTuple};

/// One artifact: the sysroot for one target, as this release pins it.
///
/// Or the kernel header tree, which is [`KERNEL_HEADERS`] and the one artifact that is not any
/// target's.
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub struct Pinned {
    /// The target it is the sysroot for, in the spelling that names its directory under the cache,
    /// or `kernel-headers` for the tree every Linux target shares, which is that tree's directory.
    pub tuple: &'static str,
    /// Where to get it. Handed to a downloader as it stands, and nothing here builds it out of
    /// parts.
    pub url: &'static str,
    /// The sha256 of the archive, lowercase hex, which is what the bytes that arrive are held to.
    pub sha256: &'static str,
}

impl Pinned {
    /// The name to write the archive under, which is the last component of the URL.
    ///
    /// The URL's own name rather than one built out of the tuple, so that the file on disk is the
    /// file the server served and a person comparing the two is comparing names as well as bytes.
    #[must_use]
    pub fn file_name(&self) -> &'static str {
        self.url.rsplit('/').next().unwrap_or(self.url)
    }

    /// Where in the cache the archive is kept.
    ///
    /// Under the cache rather than in a temporary directory, because a machine with no downloader is
    /// told this exact path and a second `--fetch` carries on from the check, which is section 13.8's
    /// answer for a host that cannot reach the network at all. It is kept after the install for the
    /// same reason and for one more: a fetch of a target that is already installed then moves
    /// nothing and says so.
    ///
    /// The directory in front of the name is the first twelve characters of the hash, and it is
    /// there because the name alone does not say which artifact this is. Two releases of a sysroot
    /// for one target have the same file name, so a cache that kept the name alone would hold last
    /// release's archive under the name this release wants, and a fetch refuses a file that does not
    /// match rather than downloading over the top of it. That refusal is right for a file somebody
    /// placed by hand and wrong for one we put there ourselves, so the fix is to stop the collision
    /// rather than to soften the check. The hash is what has to change when the bytes change, so it
    /// is the thing that separates them.
    #[must_use]
    pub fn archive_in(&self, cache: &Path) -> PathBuf {
        cache.join("downloads").join(&self.sha256[..12]).join(self.file_name())
    }
}

/// Every artifact this release pins, in tuple order.
///
/// A row is three strings and the test below says what they have to be. The order is the tuple's
/// rather than the order they were published in, so that a row is found by reading down the column
/// and two releases of this file diff as what changed between them.
pub const PINNED: &[Pinned] = &[
    Pinned {
        tuple: "aarch64-linux-gnu",
        url: "https://github.com/tamnd/rucc-cross/releases/download/sysroots-2026-10-14b/rucc-sysroot-aarch64-linux-gnu.tar.gz",
        sha256: "a67ed17c461aa8b589a50ece0a2a39ac38f76ea8d67f7c4d049768276c91e26f",
    },
    Pinned {
        tuple: "aarch64-linux-musl",
        url: "https://github.com/tamnd/rucc-cross/releases/download/sysroots-2026-10-14/rucc-sysroot-aarch64-linux-musl.tar.gz",
        sha256: "17db6aa2eab617f2ff43577b4afdd0b23457b676329cefe3667b2cc971022e5f",
    },
    Pinned {
        tuple: "aarch64-windows-gnu",
        url: "https://github.com/tamnd/rucc-cross/releases/download/sysroots-2026-09-29/rucc-sysroot-aarch64-windows-gnu.tar.gz",
        sha256: "b304bbbe1a11eaf9b41f9be810c27a3cf4965699872e71e2b703a59941db7aaf",
    },
    Pinned {
        tuple: "armv7-linux-gnueabihf",
        url: "https://github.com/tamnd/rucc-cross/releases/download/sysroots-2026-10-14b/rucc-sysroot-armv7-linux-gnueabihf.tar.gz",
        sha256: "9d025bcc030d6cdbc1190c61db7b8b14607b4c32fc33e4e1bf34cdc0d5f75a48",
    },
    Pinned {
        tuple: "armv7-linux-musleabihf",
        url: "https://github.com/tamnd/rucc-cross/releases/download/sysroots-2026-10-14/rucc-sysroot-armv7-linux-musleabihf.tar.gz",
        sha256: "ba5bbe85a4e4609b06628b89511c020fbcfc33534d8f8a9dcfdd3c86b2cd9f96",
    },
    Pinned {
        tuple: "i686-linux-gnu",
        url: "https://github.com/tamnd/rucc-cross/releases/download/sysroots-2026-10-14b/rucc-sysroot-i686-linux-gnu.tar.gz",
        sha256: "03bb24908c6090da01685f52550158564bf9cd71945cae03b0bdd75ebf72238e",
    },
    Pinned {
        tuple: "i686-windows-gnu",
        url: "https://github.com/tamnd/rucc-cross/releases/download/sysroots-2026-09-29/rucc-sysroot-i686-windows-gnu.tar.gz",
        sha256: "cea70fbedd27aa9cdc08c335105714747919f09f72b8f33ec5d49c659b3526b1",
    },
    Pinned {
        tuple: "loongarch64-linux-gnu",
        url: "https://github.com/tamnd/rucc-cross/releases/download/sysroots-2026-10-14b/rucc-sysroot-loongarch64-linux-gnu.tar.gz",
        sha256: "f8b098fb57259980bca8706feac0397c257372e446808f445c4f4987636a14fc",
    },
    Pinned {
        tuple: "powerpc64le-linux-gnu",
        url: "https://github.com/tamnd/rucc-cross/releases/download/sysroots-2026-10-14b/rucc-sysroot-powerpc64le-linux-gnu.tar.gz",
        sha256: "33e37324413cd24e7baaf4728fd85752813e7c037ec8d07d0781e39173338a35",
    },
    Pinned {
        tuple: "riscv64-linux-gnu",
        url: "https://github.com/tamnd/rucc-cross/releases/download/sysroots-2026-10-14b/rucc-sysroot-riscv64-linux-gnu.tar.gz",
        sha256: "a7d64d533a0c3fd6ed493b8078e5c1ae7c51dbc1f06eed3bd3df542c68bdf215",
    },
    Pinned {
        tuple: "riscv64-linux-musl",
        url: "https://github.com/tamnd/rucc-cross/releases/download/sysroots-2026-10-14/rucc-sysroot-riscv64-linux-musl.tar.gz",
        sha256: "4d138cf2329c88e3e9c46b1c71078ee4f9635a41cbbf88120606a0314953743c",
    },
    Pinned {
        tuple: "s390x-linux-gnu",
        url: "https://github.com/tamnd/rucc-cross/releases/download/sysroots-2026-10-14b/rucc-sysroot-s390x-linux-gnu.tar.gz",
        sha256: "7d849a4dffe5a7af1124df940630b0c6abeb9487e6bc5881d8a7c63a4473a311",
    },
    Pinned {
        tuple: "wasm32-wasip1",
        url: "https://github.com/tamnd/rucc-cross/releases/download/sysroots-2026-10-05/rucc-sysroot-wasm32-wasip1.tar.gz",
        sha256: "4a477d560f270d965d8fa416cf199e564c16331cf768f5fefc9f45378e51ada9",
    },
    Pinned {
        tuple: "wasm32-wasip2",
        url: "https://github.com/tamnd/rucc-cross/releases/download/sysroots-2026-10-05/rucc-sysroot-wasm32-wasip2.tar.gz",
        sha256: "09d580524d79ec6ed5cc1179a5baba40fbfd724798f17e1cb3ad7a61d9f6f159",
    },
    Pinned {
        tuple: "wasm32-wasip3",
        url: "https://github.com/tamnd/rucc-cross/releases/download/sysroots-2026-10-05/rucc-sysroot-wasm32-wasip3.tar.gz",
        sha256: "48fc4a3bea00c2c7d625dfa9bc53e153db0f2183924783fa137ffc9f31383378",
    },
    Pinned {
        tuple: "x86_64-linux-gnu",
        url: "https://github.com/tamnd/rucc-cross/releases/download/sysroots-2026-10-14b/rucc-sysroot-x86_64-linux-gnu.tar.gz",
        sha256: "3a7dfa6ddac1ca1f4fcf43c5c9cc34ca5a4cdc092addf575b8036e98a1511d98",
    },
    Pinned {
        tuple: "x86_64-linux-musl",
        url: "https://github.com/tamnd/rucc-cross/releases/download/sysroots-2026-10-14/rucc-sysroot-x86_64-linux-musl.tar.gz",
        sha256: "e009c42e3bee375682c176af347f98eb42825cbef328e709e65ea9bf3ca82822",
    },
    Pinned {
        tuple: "x86_64-windows-gnu",
        url: "https://github.com/tamnd/rucc-cross/releases/download/sysroots-2026-09-29/rucc-sysroot-x86_64-windows-gnu.tar.gz",
        sha256: "9f3919c9bee5447380a9746759439e051087b2364084447d6481fb9425c93758",
    },
];

/// The Linux uapi header tree, which every Linux target reads beside its own sysroot.
///
/// One archive rather than a part of each Linux sysroot, for the reason [`crate::Kernel`] gives
/// about the directory: the tree is the same for every Linux row except a small `asm/` per
/// architecture, so a copy in every sysroot would be the same eleven megabytes once per target.
/// `bin/kernel-headers` in `tamnd/rucc-cross` produces it and `bin/artifact kernel-headers` packs it,
/// and `--fetch` of a Linux target installs it after the sysroot when it is not there already.
pub const KERNEL_HEADERS: Pinned = Pinned {
    tuple: "kernel-headers",
    url: "https://github.com/tamnd/rucc-cross/releases/download/sysroots-2026-09-23/rucc-kernel-headers.tar.gz",
    sha256: "e96934f553df19d6bdbb6804f1b52d079adc4bea10a819d51eda5bc4ec584f2d",
};

/// The artifact this release pins for `tuple`, if it pins one.
///
/// The canonical spelling is what a row is named by, so the caller parses what the user wrote and
/// asks with the tuple's own text rather than with theirs.
#[must_use]
pub fn pinned_for(tuple: &str) -> Option<&'static Pinned> {
    look(PINNED, tuple)
}

/// The artifact this release pins for a target, which for a glibc tuple that names a release is the
/// one for the same tuple without it.
///
/// One glibc sysroot serves every release, because the headers in it pick the release with
/// `__GLIBC_MINOR__` and the libraries a link reads are the stubs the driver writes for the release
/// asked for. So `x86_64-linux-gnu.2.28` and `x86_64-linux-gnu` fetch the same archive, and it is
/// installed under the pinned spelling because that is the directory the compiler reads.
#[must_use]
pub fn pinned_for_target(target: TargetTuple) -> Option<&'static Pinned> {
    pinned_for(&target.to_canonical_string())
        .or_else(|| pinned_for(&glibc_base(target)?.to_canonical_string()))
}

/// The same glibc target with its release left off, or [`None`] for a target that is not glibc or
/// names no release.
///
/// What [`pinned_for_target`] looks a pinned release up under, and what the install accepts as the
/// target of an archive fetched for one.
#[must_use]
pub fn glibc_base(target: TargetTuple) -> Option<TargetTuple> {
    if target.os() != Os::Linux || target.env() != Env::Gnu {
        return None;
    }
    target.env_version()?;
    let mut base = TargetTuple::builder(target.arch(), target.os())
        .sub_arch(target.sub_arch())
        .endian(target.endian())
        .data_model(target.data_model())
        .env(target.env())
        .abi(target.abi());
    if let Some(version) = target.os_version() {
        base = base.os_version(version);
    }
    base.build().ok()
}

/// Every target this release pins an artifact for, for a message that has to say what there is.
#[must_use]
pub fn pinned_targets() -> Vec<&'static str> {
    PINNED.iter().map(|what| what.tuple).collect()
}

/// The same lookup over a table that is passed in, so what the lookup does is tested against rows
/// that are written for it rather than against whatever [`PINNED`] happens to hold this release.
fn look<'a>(table: &'a [Pinned], tuple: &str) -> Option<&'a Pinned> {
    table.iter().find(|what| what.tuple == tuple)
}

#[cfg(test)]
mod tests {
    use std::path::PathBuf;

    use super::*;

    #[test]
    fn a_pinned_glibc_release_shares_the_artifact_of_its_tuple() {
        let tuple = |spelling: &str| spelling.parse::<TargetTuple>().expect("a tuple");
        let base = glibc_base(tuple("x86_64-linux-gnu.2.28")).expect("glibc with a release");
        assert_eq!(base.to_canonical_string(), "x86_64-linux-gnu");
        let arm = glibc_base(tuple("armv7-linux-gnu.2.31eabihf")).expect("glibc with a release");
        assert_eq!(arm.to_canonical_string(), "armv7-linux-gnueabihf");
        // No release, or not glibc, is nothing to fall back to.
        assert_eq!(glibc_base(tuple("x86_64-linux-gnu")), None);
        assert_eq!(glibc_base(tuple("x86_64-linux-musl")), None);
        // And a musl row is still found under its own name.
        assert_eq!(
            pinned_for_target(tuple("x86_64-linux-musl")).map(|what| what.tuple),
            Some("x86_64-linux-musl")
        );
    }

    /// A table with rows in it, which is what [`PINNED`] will look like.
    const TABLE: &[Pinned] = &[
        Pinned {
            tuple: "aarch64-linux-musl",
            url: "https://example.invalid/rucc-sysroot-aarch64-linux-musl.tar.gz",
            sha256: "1111111111111111111111111111111111111111111111111111111111111111",
        },
        Pinned {
            tuple: "x86_64-linux-musl",
            url: "https://example.invalid/rucc-sysroot-x86_64-linux-musl.tar.gz",
            sha256: "2222222222222222222222222222222222222222222222222222222222222222",
        },
    ];

    #[test]
    fn a_target_the_table_names_is_found_and_one_it_does_not_is_not() {
        let found = look(TABLE, "x86_64-linux-musl").expect("the table has that one");
        assert_eq!(found.sha256, TABLE[1].sha256);
        assert_eq!(look(TABLE, "riscv64-linux-gnu"), None);
    }

    /// A tuple that starts with one the table has is a different target and not a match.
    #[test]
    fn a_longer_tuple_is_not_the_row_it_begins_with() {
        assert_eq!(look(TABLE, "x86_64-linux-musl.1.2.5"), None);
        assert_eq!(look(TABLE, "x86_64-linux"), None);
    }

    #[test]
    fn the_archive_is_named_by_the_url_and_kept_under_the_cache() {
        let what = TABLE[0];
        assert_eq!(what.file_name(), "rucc-sysroot-aarch64-linux-musl.tar.gz");
        assert_eq!(
            what.archive_in(&PathBuf::from("/tmp/cache")),
            PathBuf::from(
                "/tmp/cache/downloads/111111111111/rucc-sysroot-aarch64-linux-musl.tar.gz"
            )
        );
    }

    /// Two releases of the sysroot for one target have the same file name, and the cache has to keep
    /// them apart, because a fetch refuses a file under the artifact's name that is not the artifact.
    #[test]
    fn two_releases_of_one_target_are_not_the_same_path() {
        let cache = PathBuf::from("/tmp/cache");
        let old = TABLE[0];
        let new = Pinned { sha256: TABLE[1].sha256, ..old };
        assert_eq!(old.file_name(), new.file_name());
        assert_ne!(old.archive_in(&cache), new.archive_in(&cache));
    }

    /// What every row of [`PINNED`] has to be.
    ///
    /// Left as a test rather than as a comment above the table, because the day somebody adds a row
    /// is the day the rules stop being obvious, and a pasted hash with a capital letter in it or a
    /// tuple spelled the way the URL spells it would otherwise be found by a user.
    #[test]
    fn every_row_is_a_target_a_url_and_a_hash() {
        for what in PINNED {
            let tuple: TargetTuple =
                what.tuple.parse().unwrap_or_else(|why| panic!("{}: {why}", what.tuple));
            assert_eq!(
                tuple.to_canonical_string(),
                what.tuple,
                "a row is named by the canonical spelling, because that is what names the \
                 directory the tree is installed at"
            );
            assert!(what.url.starts_with("https://"), "{}: {}", what.tuple, what.url);
            // A query string or a fragment would make the file name something other than the last
            // component of the URL, which is the one thing the name is read out of.
            assert!(!what.url.contains('?') && !what.url.contains('#'), "{}", what.url);
            assert!(!what.file_name().is_empty(), "{} ends with a separator", what.url);
            assert_eq!(what.sha256.len(), 64, "{}: {}", what.tuple, what.sha256);
            assert!(
                what.sha256.bytes().all(|b| b.is_ascii_digit() || (b'a'..=b'f').contains(&b)),
                "{}: {} is not lowercase hex, and the check compares text",
                what.tuple,
                what.sha256
            );
        }
        let mut sorted: Vec<&str> = pinned_targets();
        sorted.sort_unstable();
        sorted.dedup();
        assert_eq!(sorted, pinned_targets(), "the rows are in tuple order and each target once");
    }

    /// The kernel tree's row is held to the same rules as a target's, except that its name is the
    /// directory it is installed at rather than a tuple.
    #[test]
    fn the_kernel_tree_is_a_url_and_a_hash_and_no_target_is_called_that() {
        let what = KERNEL_HEADERS;
        assert!(what.url.starts_with("https://"), "{}", what.url);
        assert!(!what.url.contains('?') && !what.url.contains('#'), "{}", what.url);
        assert_eq!(what.file_name(), "rucc-kernel-headers.tar.gz");
        assert_eq!(what.sha256.len(), 64, "{}", what.sha256);
        assert!(
            what.sha256.bytes().all(|b| b.is_ascii_digit() || (b'a'..=b'f').contains(&b)),
            "{} is not lowercase hex",
            what.sha256
        );
        assert_eq!(pinned_for(what.tuple), None);
    }
}