tugger_rust_toolchain/
lib.rs

1// This Source Code Form is subject to the terms of the Mozilla Public
2// License, v. 2.0. If a copy of the MPL was not distributed with this
3// file, You can obtain one at https://mozilla.org/MPL/2.0/.
4
5//! Obtain and interact with Rust toolchains.
6//!
7//! This module effectively reimplements the Rust toolchain discovery
8//! and download features of `rustup` to facilitate automatic Rust toolchain
9//! install. This enables people without Rust on their machines to easily
10//! use PyOxidizer.
11
12pub 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
41/// Fetch, verify, and parse a Rust toolchain manifest for a named channel.
42///
43/// Returns the verified and parsed manifest.
44pub 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
106/// Resolve a [PackageArchive] for a requested Rust toolchain package.
107///
108/// This is safe to call concurrently from different threads or processes.
109pub 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/// Represents an installed toolchain on the filesystem.
159#[derive(Clone, Debug)]
160pub struct InstalledToolchain {
161    /// Root directory of this toolchain.
162    pub path: PathBuf,
163
164    /// Path to executable binaries in this toolchain.
165    ///
166    /// Suitable for inclusion on `PATH`.
167    pub bin_path: PathBuf,
168
169    /// Path to `rustc` executable.
170    pub rustc_path: PathBuf,
171
172    /// Path to `cargo` executable.
173    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
240/// Install a functional Rust toolchain capable of running on and building for a target triple.
241///
242/// This is a convenience method for fetching the packages that compose a minimal
243/// Rust installation capable of compiling.
244///
245/// `host_triple` denotes the host triple of the toolchain to fetch.
246/// `extra_target_triples` denotes extra triples for targets we are building for.
247pub 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    // The actual install directory is composed of the toolchain name and the
257    // host triple.
258    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        // Doing it again should no-op.
355        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}