rucc_sysroot/artifact.rs
1//! What this release pins: one sysroot artifact per target, by URL and by hash.
2//!
3//! Design: `spec/cross-compile/13-distribution.md` section 13.2, which says every downloaded
4//! artifact has a hash pinned in the rucc release, checked before use, with a mismatch being a hard
5//! failure and no flag to get past it. Section 13.8 divides the work in three and the other two are
6//! written: `rucc_driver::fetch` moves the bytes with a program the machine already has, and
7//! `rucc_driver::install` decides whether what arrived is the right tree. This is the third, which
8//! is the statement of what the right one is, and it is the half that makes the other two mean
9//! anything.
10//!
11//! # Why it is here rather than with the two halves that use it
12//!
13//! Because it is read by something that cannot depend on the driver. The distribution manifest of
14//! section 13.5 is generated by a build tool, the same way `docs/TARGETS.md` and `tests/link-lines`
15//! are, and what it says about a target is what this table says plus what [`crate::Wall`] says. A
16//! build tool that pulled in the whole driver to read three strings would be a layer violation
17//! dressed up as convenience. It sits well here for a second reason as well: a pin is a fact about
18//! a sysroot, which is what this crate is for, and the fetch and the install are what a driver does
19//! with one.
20//!
21//! # Why the table is in the binary
22//!
23//! Because a hash that travels with the artifact is not a pin, and a hash in a file beside the
24//! compiler is a hash whoever replaces the artifact can replace too. The release is the authority
25//! for what an artifact of that release is, so the table is compiled into the release, which also
26//! means an upgrade can change a URL without anything on the machine having to be told.
27//!
28//! It is a table rather than a computed URL for the same reason. A name built out of a version and
29//! a tuple looks tidier and quietly says that every target's artifact is at a predictable address
30//! forever, which is a promise about somebody else's file server. A row per target costs three
31//! strings and says only what is true.
32//!
33//! # Why it is empty
34//!
35//! Because nothing has published a sysroot artifact yet. The producer is in `tamnd/rucc-cross`, per
36//! document 08.7, and the table cannot honestly name a URL and a hash before there is a file at one
37//! with the other. So [`PINNED`] has no rows today, every `--fetch` says so by name, and the test at
38//! the bottom of this file is what the first row will be held to when it is added.
39
40use std::path::{Path, PathBuf};
41
42/// One artifact: the sysroot for one target, as this release pins it.
43#[derive(Debug, Clone, Copy, PartialEq, Eq)]
44pub struct Pinned {
45 /// The target it is the sysroot for, in the spelling that names its directory under the cache.
46 pub tuple: &'static str,
47 /// Where to get it. Handed to a downloader as it stands, and nothing here builds it out of
48 /// parts.
49 pub url: &'static str,
50 /// The sha256 of the archive, lowercase hex, which is what the bytes that arrive are held to.
51 pub sha256: &'static str,
52}
53
54impl Pinned {
55 /// The name to write the archive under, which is the last component of the URL.
56 ///
57 /// The URL's own name rather than one built out of the tuple, so that the file on disk is the
58 /// file the server served and a person comparing the two is comparing names as well as bytes.
59 #[must_use]
60 pub fn file_name(&self) -> &'static str {
61 self.url.rsplit('/').next().unwrap_or(self.url)
62 }
63
64 /// Where in the cache the archive is kept.
65 ///
66 /// Under the cache rather than in a temporary directory, because a machine with no downloader is
67 /// told this exact path and a second `--fetch` carries on from the check, which is section 13.8's
68 /// answer for a host that cannot reach the network at all. It is kept after the install for the
69 /// same reason and for one more: a fetch of a target that is already installed then moves
70 /// nothing and says so.
71 #[must_use]
72 pub fn archive_in(&self, cache: &Path) -> PathBuf {
73 cache.join("downloads").join(self.file_name())
74 }
75}
76
77/// Every artifact this release pins, in tuple order.
78///
79/// Empty, for the reason the module documentation gives. A row is three strings and the test below
80/// says what they have to be.
81pub const PINNED: &[Pinned] = &[];
82
83/// The artifact this release pins for `tuple`, if it pins one.
84///
85/// The canonical spelling is what a row is named by, so the caller parses what the user wrote and
86/// asks with the tuple's own text rather than with theirs.
87#[must_use]
88pub fn pinned_for(tuple: &str) -> Option<&'static Pinned> {
89 look(PINNED, tuple)
90}
91
92/// Every target this release pins an artifact for, for a message that has to say what there is.
93#[must_use]
94pub fn pinned_targets() -> Vec<&'static str> {
95 PINNED.iter().map(|what| what.tuple).collect()
96}
97
98/// The same lookup over a table that is passed in, so the cases are testable while [`PINNED`] has no
99/// rows in it.
100fn look<'a>(table: &'a [Pinned], tuple: &str) -> Option<&'a Pinned> {
101 table.iter().find(|what| what.tuple == tuple)
102}
103
104#[cfg(test)]
105mod tests {
106 use std::path::PathBuf;
107
108 use rucc_tuple::TargetTuple;
109
110 use super::*;
111
112 /// A table with rows in it, which is what [`PINNED`] will look like.
113 const TABLE: &[Pinned] = &[
114 Pinned {
115 tuple: "aarch64-linux-musl",
116 url: "https://example.invalid/rucc-sysroot-aarch64-linux-musl.tar.gz",
117 sha256: "1111111111111111111111111111111111111111111111111111111111111111",
118 },
119 Pinned {
120 tuple: "x86_64-linux-musl",
121 url: "https://example.invalid/rucc-sysroot-x86_64-linux-musl.tar.gz",
122 sha256: "2222222222222222222222222222222222222222222222222222222222222222",
123 },
124 ];
125
126 #[test]
127 fn a_target_the_table_names_is_found_and_one_it_does_not_is_not() {
128 let found = look(TABLE, "x86_64-linux-musl").expect("the table has that one");
129 assert_eq!(found.sha256, TABLE[1].sha256);
130 assert_eq!(look(TABLE, "riscv64-linux-gnu"), None);
131 }
132
133 /// A tuple that starts with one the table has is a different target and not a match.
134 #[test]
135 fn a_longer_tuple_is_not_the_row_it_begins_with() {
136 assert_eq!(look(TABLE, "x86_64-linux-musl.1.2.5"), None);
137 assert_eq!(look(TABLE, "x86_64-linux"), None);
138 }
139
140 #[test]
141 fn the_archive_is_named_by_the_url_and_kept_under_the_cache() {
142 let what = TABLE[0];
143 assert_eq!(what.file_name(), "rucc-sysroot-aarch64-linux-musl.tar.gz");
144 assert_eq!(
145 what.archive_in(&PathBuf::from("/tmp/cache")),
146 PathBuf::from("/tmp/cache/downloads/rucc-sysroot-aarch64-linux-musl.tar.gz")
147 );
148 }
149
150 /// What every row of [`PINNED`] has to be, which passes today because there are none.
151 ///
152 /// Left as a test rather than as a comment above the table, because the day somebody adds a row
153 /// is the day the rules stop being obvious, and a pasted hash with a capital letter in it or a
154 /// tuple spelled the way the URL spells it would otherwise be found by a user.
155 #[test]
156 fn every_row_is_a_target_a_url_and_a_hash() {
157 for what in PINNED {
158 let tuple: TargetTuple =
159 what.tuple.parse().unwrap_or_else(|why| panic!("{}: {why}", what.tuple));
160 assert_eq!(
161 tuple.to_canonical_string(),
162 what.tuple,
163 "a row is named by the canonical spelling, because that is what names the \
164 directory the tree is installed at"
165 );
166 assert!(what.url.starts_with("https://"), "{}: {}", what.tuple, what.url);
167 // A query string or a fragment would make the file name something other than the last
168 // component of the URL, which is the one thing the name is read out of.
169 assert!(!what.url.contains('?') && !what.url.contains('#'), "{}", what.url);
170 assert!(!what.file_name().is_empty(), "{} ends with a separator", what.url);
171 assert_eq!(what.sha256.len(), 64, "{}: {}", what.tuple, what.sha256);
172 assert!(
173 what.sha256.bytes().all(|b| b.is_ascii_digit() || (b'a'..=b'f').contains(&b)),
174 "{}: {} is not lowercase hex, and the check compares text",
175 what.tuple,
176 what.sha256
177 );
178 }
179 let mut sorted: Vec<&str> = pinned_targets();
180 sorted.sort_unstable();
181 sorted.dedup();
182 assert_eq!(sorted, pinned_targets(), "the rows are in tuple order and each target once");
183 }
184}