1pub mod manifest;
13pub mod tar;
14
15use {
16 crate::{
17 manifest::Manifest,
18 tar::{read_installs_manifest, PackageArchive},
19 },
20 anyhow::{anyhow, Context, Result},
21 fs2::FileExt,
22 log::warn,
23 once_cell::sync::Lazy,
24 pgp::{Deserializable, SignedPublicKey, StandaloneSignature},
25 sha2::Digest,
26 std::{
27 io::{Cursor, Read},
28 path::{Path, PathBuf},
29 },
30 tugger_common::http::{download_and_verify, download_to_path, get_http_client},
31};
32
33const URL_PREFIX: &str = "https://static.rust-lang.org/dist/";
34
35static GPG_SIGNING_KEY: Lazy<SignedPublicKey> = Lazy::new(|| {
36 pgp::SignedPublicKey::from_armor_single(Cursor::new(&include_bytes!("signing-key.asc")[..]))
37 .unwrap()
38 .0
39});
40
41pub fn fetch_channel_manifest(channel: &str) -> Result<Manifest> {
45 let manifest_url = format!("{}channel-rust-{}.toml", URL_PREFIX, channel);
46 let signature_url = format!("{}.asc", manifest_url);
47 let sha256_url = format!("{}.sha256", manifest_url);
48
49 let client = get_http_client()?;
50
51 warn!("fetching {}", sha256_url);
52 let mut response = client.get(&sha256_url).send()?;
53 let mut sha256_data = vec![];
54 response.read_to_end(&mut sha256_data)?;
55
56 let sha256_manifest = String::from_utf8(sha256_data)?;
57 let manifest_digest_wanted = sha256_manifest
58 .split(' ')
59 .next()
60 .ok_or_else(|| anyhow!("failed parsing SHA-256 manifest"))?
61 .to_string();
62
63 warn!("fetching {}", manifest_url);
64 let mut response = client.get(&manifest_url).send()?;
65 let mut manifest_data = vec![];
66 response.read_to_end(&mut manifest_data)?;
67
68 warn!("fetching {}", signature_url);
69 let mut response = client.get(&signature_url).send()?;
70 let mut signature_data = vec![];
71 response.read_to_end(&mut signature_data)?;
72
73 let mut hasher = sha2::Sha256::new();
74 hasher.update(&manifest_data);
75
76 let manifest_digest_got = hex::encode(hasher.finalize());
77
78 if manifest_digest_got != manifest_digest_wanted {
79 return Err(anyhow!(
80 "digest mismatch on {}; wanted {}, got {}",
81 manifest_url,
82 manifest_digest_wanted,
83 manifest_digest_got
84 ));
85 }
86
87 warn!("verified SHA-256 digest for {}", manifest_url);
88
89 let (signatures, _) = StandaloneSignature::from_armor_many(Cursor::new(&signature_data))
90 .with_context(|| format!("parsing {} armored signature data", signature_url))?;
91
92 for signature in signatures {
93 let signature = signature.context("obtaining pgp signature")?;
94
95 signature
96 .verify(&*GPG_SIGNING_KEY, &manifest_data)
97 .context("verifying pgp signature of manifest")?;
98 warn!("verified PGP signature for {}", manifest_url);
99 }
100
101 let manifest = Manifest::from_toml_bytes(&manifest_data).context("parsing manifest TOML")?;
102
103 Ok(manifest)
104}
105
106pub fn resolve_package_archive(
110 manifest: &Manifest,
111 package: &str,
112 target_triple: &str,
113 download_cache_dir: Option<&Path>,
114) -> Result<PackageArchive> {
115 let (version, target) = manifest
116 .find_package(package, target_triple)
117 .ok_or_else(|| {
118 anyhow!(
119 "package {} not available for target triple {}",
120 package,
121 target_triple
122 )
123 })?;
124
125 warn!(
126 "found Rust package {} version {} for {}",
127 package, version, target_triple
128 );
129
130 let (compression_format, remote_content) = target.download_info().ok_or_else(|| {
131 anyhow!(
132 "package {} for target {} is not available",
133 package,
134 target_triple
135 )
136 })?;
137
138 let tar_data = if let Some(download_dir) = download_cache_dir {
139 let dest_path = download_dir.join(
140 remote_content
141 .url
142 .rsplit('/')
143 .next()
144 .expect("failed to parse URL"),
145 );
146
147 download_to_path(&remote_content, &dest_path)
148 .context("downloading file to cache directory")?;
149
150 std::fs::read(&dest_path).context("reading downloaded file")?
151 } else {
152 download_and_verify(&remote_content)?
153 };
154
155 PackageArchive::new(compression_format, tar_data).context("obtaining PackageArchive")
156}
157
158#[derive(Clone, Debug)]
160pub struct InstalledToolchain {
161 pub path: PathBuf,
163
164 pub bin_path: PathBuf,
168
169 pub rustc_path: PathBuf,
171
172 pub cargo_path: PathBuf,
174}
175
176fn materialize_archive(
177 archive: &PackageArchive,
178 package: &str,
179 triple: &str,
180 install_dir: &Path,
181) -> Result<()> {
182 archive.install(install_dir).context("installing")?;
183
184 let manifest_path = install_dir.join(format!("MANIFEST.{}.{}", triple, package));
185 let mut fh = std::fs::File::create(manifest_path).context("opening manifest file")?;
186 archive
187 .write_installs_manifest(&mut fh)
188 .context("writing installs manifest")?;
189
190 Ok(())
191}
192
193fn sha256_path(path: &Path) -> Result<Vec<u8>> {
194 let mut hasher = sha2::Sha256::new();
195 let fh = std::fs::File::open(path)?;
196 let mut reader = std::io::BufReader::new(fh);
197
198 let mut buffer = [0; 32768];
199
200 loop {
201 let count = reader.read(&mut buffer)?;
202 if count == 0 {
203 break;
204 }
205 hasher.update(&buffer[..count]);
206 }
207
208 Ok(hasher.finalize().to_vec())
209}
210
211fn package_is_fresh(install_dir: &Path, package: &str, triple: &str) -> Result<bool> {
212 let manifest_path = install_dir.join(format!("MANIFEST.{}.{}", triple, package));
213
214 if !manifest_path.exists() {
215 return Ok(false);
216 }
217
218 let mut fh =
219 std::fs::File::open(&manifest_path).context("opening installs manifest for reading")?;
220 let manifest = read_installs_manifest(&mut fh)?;
221
222 for (path, wanted_digest) in manifest {
223 let install_path = install_dir.join(path);
224
225 match sha256_path(&install_path) {
226 Ok(got_digest) => {
227 if wanted_digest != hex::encode(got_digest) {
228 return Ok(false);
229 }
230 }
231 Err(_) => {
232 return Ok(false);
233 }
234 }
235 }
236
237 Ok(true)
238}
239
240pub fn install_rust_toolchain(
248 toolchain: &str,
249 host_triple: &str,
250 extra_target_triples: &[&str],
251 install_root_dir: &Path,
252 download_cache_dir: Option<&Path>,
253) -> Result<InstalledToolchain> {
254 let mut manifest = None;
255
256 let install_dir = install_root_dir.join(format!("{}-{}", toolchain, host_triple));
259
260 std::fs::create_dir_all(&install_dir)
261 .with_context(|| format!("creating directory {}", install_dir.display()))?;
262
263 let mut installs = vec![
264 (host_triple, "rustc"),
265 (host_triple, "cargo"),
266 (host_triple, "rust-std"),
267 ];
268
269 for triple in extra_target_triples {
270 if *triple != host_triple {
271 installs.push((*triple, "rust-std"));
272 }
273 }
274
275 let lock_path = install_dir.with_extension("lock");
276 let lock = std::fs::File::create(&lock_path)
277 .with_context(|| format!("creating {}", lock_path.display()))?;
278 lock.lock_exclusive().context("obtaining lock")?;
279
280 for (triple, package) in installs {
281 if package_is_fresh(&install_dir, package, triple)? {
282 warn!(
283 "{} for {} in {} is up-to-date",
284 package,
285 triple,
286 install_dir.display()
287 );
288 } else {
289 if manifest.is_none() {
290 manifest.replace(fetch_channel_manifest(toolchain).context("fetching manifest")?);
291 }
292
293 warn!(
294 "extracting {} for {} to {}",
295 package,
296 triple,
297 install_dir.display()
298 );
299 let archive = resolve_package_archive(
300 manifest.as_ref().unwrap(),
301 package,
302 triple,
303 download_cache_dir,
304 )?;
305 materialize_archive(&archive, package, triple, &install_dir)?;
306 }
307 }
308
309 lock.unlock().context("unlocking")?;
310
311 let exe_suffix = if host_triple.contains("-windows-") {
312 ".exe"
313 } else {
314 ""
315 };
316
317 Ok(InstalledToolchain {
318 path: install_dir.clone(),
319 bin_path: install_dir.join("bin"),
320 rustc_path: install_dir.join("bin").join(format!("rustc{}", exe_suffix)),
321 cargo_path: install_dir.join("bin").join(format!("cargo{}", exe_suffix)),
322 })
323}
324
325#[cfg(test)]
326mod tests {
327 use super::*;
328
329 static CACHE_DIR: Lazy<PathBuf> = Lazy::new(|| {
330 dirs::cache_dir()
331 .expect("unable to obtain cache dir")
332 .join("pyoxidizer")
333 .join("rust")
334 });
335
336 fn do_triple_test(target_triple: &str) -> Result<()> {
337 let temp_dir = tempfile::Builder::new()
338 .prefix("tugger-rust-toolchain-test")
339 .tempdir()?;
340
341 let toolchain = install_rust_toolchain(
342 "stable",
343 target_triple,
344 &[],
345 temp_dir.path(),
346 Some(&*CACHE_DIR),
347 )?;
348
349 assert_eq!(
350 toolchain.path,
351 temp_dir.path().join(format!("stable-{}", target_triple))
352 );
353
354 install_rust_toolchain(
356 "stable",
357 target_triple,
358 &[],
359 temp_dir.path(),
360 Some(&*CACHE_DIR),
361 )?;
362
363 Ok(())
364 }
365
366 #[test]
367 fn fetch_stable() -> Result<()> {
368 fetch_channel_manifest("stable")?;
369
370 Ok(())
371 }
372
373 #[test]
374 fn fetch_apple() -> Result<()> {
375 do_triple_test("x86_64-apple-darwin")
376 }
377
378 #[test]
379 fn fetch_linux() -> Result<()> {
380 do_triple_test("x86_64-unknown-linux-gnu")
381 }
382
383 #[test]
384 fn fetch_windows() -> Result<()> {
385 do_triple_test("x86_64-pc-windows-msvc")
386 }
387}