rucc-sysroot 0.10.59

Sysroot layout, header search paths and link lines for rucc, computed from the target and never from the host.
Documentation
//! 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
//!
//! Three rows, which are the three windows-gnu targets. `bin/mingw-headers` in `tamnd/rucc-cross`
//! installs mingw-w64 14.0.0's headers, `bin/artifact` packs the tree, and the release
//! `sysroots-2026-09-18` is where the files are. Each archive is 8.4 MiB of the same 1702 headers,
//! 84 MiB installed, and the three differ only in the `target` line of the manifest inside them,
//! because mingw-w64 has no per architecture split and [`crate::Sysroot::splits_by_arch`] says so.
//!
//! 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};

/// One artifact: the sysroot for one target, as this release pins it.
#[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.
    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.
    #[must_use]
    pub fn archive_in(&self, cache: &Path) -> PathBuf {
        cache.join("downloads").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-windows-gnu",
        url: "https://github.com/tamnd/rucc-cross/releases/download/sysroots-2026-09-18/rucc-sysroot-aarch64-windows-gnu.tar.gz",
        sha256: "037b839abe565493360d320d833b5bfa048884eed445b51d41aafb59acfbac84",
    },
    Pinned {
        tuple: "i686-windows-gnu",
        url: "https://github.com/tamnd/rucc-cross/releases/download/sysroots-2026-09-18/rucc-sysroot-i686-windows-gnu.tar.gz",
        sha256: "e66deb5e67e35e12d2299d136a0d150168568e2b8aa7aab0c2b6cc8254b0cb53",
    },
    Pinned {
        tuple: "x86_64-windows-gnu",
        url: "https://github.com/tamnd/rucc-cross/releases/download/sysroots-2026-09-18/rucc-sysroot-x86_64-windows-gnu.tar.gz",
        sha256: "8774d78560c196f182b8cf865e9fe57e2937779fdcf14b7f7b3f321de2cf4488",
    },
];

/// 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)
}

/// 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 rucc_tuple::TargetTuple;

    use super::*;

    /// 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/rucc-sysroot-aarch64-linux-musl.tar.gz")
        );
    }

    /// 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");
    }
}