rzup/
lib.rs

1// Copyright 2025 RISC Zero, Inc.
2//
3// Licensed under the Apache License, Version 2.0 (the "License");
4// you may not use this file except in compliance with the License.
5// You may obtain a copy of the License at
6//
7//     http://www.apache.org/licenses/LICENSE-2.0
8//
9// Unless required by applicable law or agreed to in writing, software
10// distributed under the License is distributed on an "AS IS" BASIS,
11// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12// See the License for the specific language governing permissions and
13// limitations under the License.
14
15mod build;
16#[cfg(feature = "cli")]
17pub mod cli;
18mod components;
19mod distribution;
20mod env;
21pub mod error;
22mod events;
23mod paths;
24mod registry;
25mod settings;
26
27use std::path::{Path, PathBuf};
28
29use self::distribution::{
30    signature::{PrivateKey, PublicKey},
31    Platform,
32};
33use self::env::Environment;
34use self::events::{RzupEvent, TransferKind};
35use self::paths::Paths;
36use self::registry::Registry;
37use self::settings::Settings;
38
39#[cfg(feature = "publish")]
40use aws_credential_types::Credentials as AwsCredentials;
41
42#[cfg(not(feature = "publish"))]
43pub struct AwsCredentials;
44
45pub use self::components::Component;
46pub use self::error::{Result, RzupError};
47pub use semver::Version;
48
49#[derive(Clone, Debug)]
50pub struct BaseUrls {
51    pub risc0_github_base_url: String,
52    pub github_api_base_url: String,
53    pub risc0_base_url: String,
54    pub s3_base_url: String,
55}
56
57impl Default for BaseUrls {
58    fn default() -> Self {
59        Self {
60            risc0_github_base_url: "https://github.com/risc0".into(),
61            github_api_base_url: "https://api.github.com".into(),
62            risc0_base_url: "https://risczero.com".into(),
63            s3_base_url: "https://risc0-artifacts.s3.us-west-2.amazonaws.com".into(),
64        }
65    }
66}
67
68/// Rzup manages the RISC Zero toolchain components by handling installation, uninstallation,
69/// and version management of various tools like the Rust toolchain and cargo extensions.
70pub struct Rzup {
71    environment: Environment,
72    registry: Registry,
73}
74
75impl Rzup {
76    /// Creates a new Rzup instance using default environment paths.
77    pub fn new() -> Result<Self> {
78        let environment = Environment::new(|s| std::env::var(s), |_| {})?;
79        let registry = Registry::new(&environment, Default::default())?;
80
81        Ok(Self {
82            environment,
83            registry,
84        })
85    }
86
87    /// Creates a new Rzup instance using default environment paths, and the given event handler.
88    pub fn new_with_event_handler(
89        event_handler: impl Fn(RzupEvent) + Send + Sync + 'static,
90    ) -> Result<Self> {
91        let environment = Environment::new(|s| std::env::var(s), event_handler)?;
92        let registry = Registry::new(&environment, Default::default())?;
93
94        Ok(Self {
95            environment,
96            registry,
97        })
98    }
99
100    /// Creates a new Rzup instance with a custom root directory, base URLs, and GitHub token.
101    ///
102    /// # Arguments
103    /// * `risc0_dir` - The root directory path for storing components and settings
104    /// * `rustup_dir` - The path to rustup's home directory (usually ~/.rustup)
105    /// * `cargo_dir` - The path to cargo's home directory (usually ~/.cargo)
106    /// * `base_urls` - The base URLs used to communicate with GitHub
107    /// * `github_token` - The token to use when communicating with GitHub
108    /// * `aws_creds_factory` - Function which gets credentials for communicating with S3
109    /// * `private_key_getter` - Function which gets the private key for signing uploads
110    /// * `public_key` - The public key used to verify S3 components
111    /// * `platform` - The platform of the system which we are installing artifacts for
112    /// * `event_handler` - Callback for events that provide progress during rzup operations.
113    #[allow(clippy::too_many_arguments)]
114    pub fn with_paths_urls_creds_platform_and_event_handler(
115        risc0_dir: impl Into<PathBuf>,
116        rustup_dir: impl AsRef<Path>,
117        cargo_dir: impl AsRef<Path>,
118        base_urls: BaseUrls,
119        github_token: Option<String>,
120        aws_creds_factory: impl Fn() -> Option<AwsCredentials> + Send + Sync + 'static,
121        private_key_getter: impl Fn() -> Result<PrivateKey> + Send + Sync + 'static,
122        public_key: PublicKey,
123        platform: Platform,
124        event_handler: impl Fn(RzupEvent) + Send + Sync + 'static,
125    ) -> Result<Self> {
126        let environment = Environment::with_paths_creds_platform_and_event_handler(
127            risc0_dir,
128            rustup_dir,
129            cargo_dir,
130            github_token,
131            aws_creds_factory,
132            private_key_getter,
133            public_key,
134            platform,
135            event_handler,
136        )?;
137        let registry = Registry::new(&environment, base_urls)?;
138
139        Ok(Self {
140            environment,
141            registry,
142        })
143    }
144
145    /// Sets an event handler for receiving notifications about operations.
146    ///
147    /// # Arguments
148    /// * `handler` - Function that will be called for each event
149    #[cfg(test)]
150    pub(crate) fn set_event_handler(
151        &mut self,
152        event_handler: impl Fn(RzupEvent) + Send + Sync + 'static,
153    ) {
154        self.environment.set_event_handler(event_handler);
155    }
156
157    /// Installs all default components.
158    ///
159    /// # Arguments
160    /// * `force` - If true, reinstalls even if already installed
161    #[cfg_attr(not(feature = "cli"), expect(dead_code))]
162    pub(crate) fn install_all(&mut self, force: bool) -> Result<()> {
163        self.registry
164            .install_all_components(&self.environment, force)?;
165        Ok(())
166    }
167
168    /// Installs a specific component version.
169    ///
170    /// # Arguments
171    /// * `component` - Component
172    /// * `version` - Specific version to install, or None for latest
173    /// * `force` - If true, reinstalls even if already installed
174    #[cfg_attr(not(feature = "cli"), expect(dead_code))]
175    pub(crate) fn install_component(
176        &mut self,
177        component: &Component,
178        version: Option<Version>,
179        force: bool,
180    ) -> Result<()> {
181        self.registry
182            .install_component(&self.environment, component, version, force)?;
183        Ok(())
184    }
185
186    /// Uninstalls a specific component version.
187    ///
188    /// # Arguments
189    /// * `component` - Component
190    /// * `version` - Version to uninstall
191    #[cfg_attr(not(feature = "cli"), expect(dead_code))]
192    pub(crate) fn uninstall_component(
193        &mut self,
194        component: &Component,
195        version: Version,
196    ) -> Result<()> {
197        self.registry
198            .uninstall_component(&self.environment, component, version)?;
199        Ok(())
200    }
201
202    /// Lists all installed versions of a component.
203    ///
204    /// # Arguments
205    /// * `component` - Component
206    ///
207    /// # Returns
208    /// A newest-to-oldest list of installed component versions
209    #[cfg_attr(not(feature = "cli"), expect(dead_code))]
210    pub(crate) fn list_versions(&self, component: &Component) -> Result<Vec<Version>> {
211        Ok(
212            Registry::list_component_versions(&self.environment, component)?
213                .into_iter()
214                .map(|(v, _)| v)
215                .collect(),
216        )
217    }
218
219    /// Gets the currently default version of a component and its path.
220    ///
221    /// # Arguments
222    /// * `component` - Component
223    pub fn get_default_version(
224        &self,
225        component: &Component,
226    ) -> Result<Option<(Version, std::path::PathBuf)>> {
227        self.registry
228            .get_default_component_version(&self.environment, component)
229    }
230
231    fn emit(&self, event: RzupEvent) {
232        self.environment.emit(event)
233    }
234
235    #[cfg_attr(not(feature = "cli"), expect(dead_code))]
236    pub(crate) fn print(&self, message: String) {
237        self.emit(RzupEvent::Print { message });
238    }
239
240    /// Fetches the latest available version of a component.
241    ///
242    /// # Arguments
243    /// * `component` - Component
244    #[cfg_attr(not(feature = "cli"), expect(dead_code))]
245    pub(crate) fn get_latest_version(&self, component: &Component) -> Result<Version> {
246        components::get_latest_version(component, &self.environment, self.registry.base_urls())
247    }
248
249    /// Sets the default version for a component.
250    ///
251    /// # Arguments
252    /// * `component` - Component
253    /// * `version` - Version to set as default
254    pub(crate) fn set_default_version(
255        &mut self,
256        component: &Component,
257        version: Version,
258    ) -> Result<()> {
259        self.registry
260            .set_default_component_version(&self.environment, component, version)
261    }
262
263    /// Checks if a specific version of a component exists.
264    ///
265    /// # Arguments
266    /// * `component` - Component
267    /// * `version` - Version to check
268    #[cfg_attr(not(feature = "cli"), expect(dead_code))]
269    pub(crate) fn version_exists(&self, component: &Component, version: &Version) -> Result<bool> {
270        let component_installed = component.parent_component().unwrap_or(*component);
271        Ok(Paths::find_version_dir(&self.environment, &component_installed, version)?.is_some())
272    }
273
274    /// Gets the settings manager.
275    #[cfg_attr(not(feature = "cli"), expect(dead_code))]
276    pub(crate) fn settings(&self) -> &Settings {
277        self.registry.settings()
278    }
279
280    /// Gets the version-specific directory path for a component.
281    ///
282    /// Errors if the version isn't installed.
283    ///
284    /// # Arguments
285    /// * `component` - Component
286    /// * `version` - Version to get directory for
287    pub fn get_version_dir(&self, component: &Component, version: &Version) -> Result<PathBuf> {
288        let component = component.parent_component().unwrap_or(*component);
289        Paths::find_version_dir(&self.environment, &component, version)?
290            .ok_or_else(|| RzupError::VersionNotFound(version.clone()))
291    }
292
293    /// Update rzup by downloading and re-running the installation script.
294    #[cfg_attr(not(feature = "cli"), expect(dead_code))]
295    pub(crate) fn self_update(&self) -> Result<()> {
296        self.emit(RzupEvent::InstallationStarted {
297            id: "rzup".to_string(),
298            version: "latest".to_string(),
299        });
300
301        let script_contents = distribution::download_text(
302            format!(
303                "{base_url}/install",
304                base_url = self.registry.base_urls().risc0_base_url
305            ),
306            &None,
307        )?;
308
309        let output = std::process::Command::new("/usr/bin/env")
310            .args(["bash", "-c", &script_contents])
311            .output()
312            .map_err(|e| RzupError::Other(format!("Failed to execute update script: {e}")))?;
313
314        if !output.status.success() {
315            self.emit(RzupEvent::InstallationFailed {
316                id: "rzup".to_string(),
317                version: "latest".to_string(),
318            });
319            return Err(RzupError::Other(format!(
320                "Self-update failed: {}",
321                String::from_utf8_lossy(&output.stderr)
322            )));
323        }
324
325        self.emit(RzupEvent::InstallationCompleted {
326            id: "rzup".to_string(),
327            version: "latest".to_string(),
328        });
329
330        Ok(())
331    }
332
333    #[cfg_attr(not(feature = "cli"), expect(dead_code))]
334    pub(crate) fn build_rust_toolchain(
335        &mut self,
336        repo_url: &str,
337        tag_or_commit: &Option<String>,
338        path: &Option<String>,
339    ) -> Result<()> {
340        let version =
341            build::build_rust_toolchain(&self.environment, repo_url, tag_or_commit, path)?;
342        self.set_default_version(&Component::RustToolchain, version)?;
343        Ok(())
344    }
345
346    /// Upload a new version of a component to S3.
347    ///
348    /// If a component already exists for the given component + platform + version, an error is
349    /// returned, unless `force` is true.
350    #[cfg(feature = "publish")]
351    #[cfg_attr(not(feature = "cli"), expect(dead_code))]
352    pub(crate) fn publish_upload(
353        &mut self,
354        component: &Component,
355        version: &Version,
356        platform: Option<Platform>,
357        payload: &Path,
358        force: bool,
359    ) -> Result<()> {
360        let s3 = distribution::s3::S3Bucket::new(self.registry.base_urls());
361        s3.publish_upload(
362            &self.environment,
363            component,
364            version,
365            platform,
366            payload,
367            force,
368        )
369    }
370
371    /// Set the given version as the latest version for a given component on S3.
372    ///
373    /// If the given release doesn't exist, an error is returned.
374    #[cfg(feature = "publish")]
375    #[cfg_attr(not(feature = "cli"), expect(dead_code))]
376    pub(crate) fn publish_set_latest(
377        &mut self,
378        component: &Component,
379        version: &Version,
380    ) -> Result<()> {
381        let s3 = distribution::s3::S3Bucket::new(self.registry.base_urls());
382        s3.publish_set_latest(&self.environment, component, version)
383    }
384
385    /// Creates a tar.xz file.
386    #[cfg(feature = "publish")]
387    #[cfg_attr(not(feature = "cli"), expect(dead_code))]
388    pub(crate) fn publish_create_artifact(
389        &mut self,
390        input: &Path,
391        output: &Path,
392        compression_level: u32,
393    ) -> Result<()> {
394        use liblzma::write::XzEncoder;
395
396        let (estimated_tar_size, paths) = self.recursively_find_paths(input)?;
397
398        let id = format!("artifact {}", output.display());
399        self.environment.emit(RzupEvent::TransferStarted {
400            kind: TransferKind::Compress,
401            id: id.clone(),
402            version: None,
403            url: None,
404            len: Some(estimated_tar_size),
405        });
406
407        let file = std::fs::File::create(output).map_err(|e| {
408            RzupError::Other(format!(
409                "error creating output path {}: {e}",
410                output.display()
411            ))
412        })?;
413        let compressor = XzEncoder::new_parallel(file, compression_level);
414        let writer =
415            crate::distribution::ProgressWriter::new(id.clone(), &self.environment, compressor);
416        let mut builder = tar::Builder::new(writer);
417
418        for (full_path, archive_path) in paths {
419            builder.append_path_with_name(full_path, archive_path)?;
420        }
421
422        builder.finish()?;
423        drop(builder);
424
425        self.environment.emit(RzupEvent::TransferCompleted {
426            kind: TransferKind::Compress,
427            id,
428            version: None,
429        });
430
431        Ok(())
432    }
433
434    /// Walk a given path and find all files for the purpose of creating a tar. It returns a sum of
435    /// the files sizes (with 512 per file for a tar-header) and a listing of (full-path,
436    /// relative-path).
437    ///
438    /// Relative paths are relative to the given input path, or when the input path is to a file,
439    /// relative to the file's parent directory.
440    #[cfg(feature = "publish")]
441    fn recursively_find_paths(&mut self, input: &Path) -> Result<(u64, Vec<(PathBuf, PathBuf)>)> {
442        let mut estimated_tar_size = 0;
443        let mut paths = vec![];
444        if input.is_dir() {
445            self.environment.emit(RzupEvent::Print {
446                message: "Walking input path".into(),
447            });
448            for entry in walkdir::WalkDir::new(input) {
449                let entry = entry.map_err(|e| {
450                    RzupError::Other(format!(
451                        "error reading entry from input path {}: {e}",
452                        input.display()
453                    ))
454                })?;
455                let full_path = entry.path();
456                let entry_metadata = entry.metadata().map_err(|e| {
457                    RzupError::Other(format!("error reading entry {}: {e}", full_path.display()))
458                })?;
459                if entry_metadata.is_dir() {
460                    continue;
461                }
462                let archive_path = full_path
463                    .strip_prefix(input)
464                    .expect("all walked paths are under the input path");
465
466                estimated_tar_size += entry_metadata.len();
467                // each tar header is usually 512 bytes.
468                estimated_tar_size += 512;
469
470                paths.push((full_path.to_owned(), archive_path.to_owned()));
471            }
472        } else {
473            estimated_tar_size += input
474                .metadata()
475                .map_err(|e| {
476                    RzupError::Other(format!("error reading input file: {} {e}", input.display()))
477                })?
478                .len();
479            // each tar header is usually 512 bytes.
480            estimated_tar_size += 512;
481            paths.push((
482                input.to_owned(),
483                PathBuf::from(input.file_name().expect("non-directory has a name")),
484            ));
485        }
486        Ok((estimated_tar_size, paths))
487    }
488}
489
490#[cfg(test)]
491mod tests {
492    use super::*;
493    use crate::distribution::Os;
494    use serde_json::json;
495    use sha2::Digest as _;
496    use std::collections::HashMap;
497    use std::convert::Infallible;
498    use std::io::Write as _;
499    use std::net::SocketAddr;
500    use std::path::Path;
501    use std::sync::{Arc, Mutex};
502    use tempfile::TempDir;
503
504    pub struct MockDistributionServer {
505        pub base_urls: BaseUrls,
506        pub private_key: PrivateKey,
507    }
508
509    type HyperResponse = hyper::Response<http_body_util::Full<hyper::body::Bytes>>;
510
511    /// pre-calculated SHA256 sum of tar.xz with `hello-world/tar_contents.bin`
512    const HELLO_WORLD_DUMMY_TAR_XZ_SHA256: &str =
513        "fb05f4f1c334bd3d32ccb043626aad6a849467f9e7674856a1f81906d145f5ac";
514
515    /// pre-calculated SHA256 sum of tar.xz with `hello-world2/tar_contents.bin`
516    const HELLO_WORLD2_DUMMY_TAR_XZ_SHA256: &str =
517        "68ec421acb728e69d0b4497fe6a622f7347b116649039b48fd00c5a062ceddb4";
518
519    /// pre-calculated SHA256 sum of tar.xz with `hello-world2/tar_contents.bin`
520    const HELLO_WORLD3_DUMMY_TAR_XZ_SHA256: &str =
521        "ed9efde9a314a9063a9b91d21e9eb1508defce0817ee6a81142d8bf6fb1f045e";
522
523    fn dummy_tar_gz_response() -> HyperResponse {
524        let mut tar_bytes = vec![];
525        let mut tar_builder = tar::Builder::new(&mut tar_bytes);
526        let mut header = tar::Header::new_gnu();
527        header.set_size(4);
528        tar_builder
529            .append_data(&mut header, "tar_contents.bin", &[1, 2, 3, 4][..])
530            .unwrap();
531        tar_builder.finish().unwrap();
532        drop(tar_builder);
533
534        let mut tar_gz_bytes = vec![];
535        let mut encoder =
536            flate2::write::GzEncoder::new(&mut tar_gz_bytes, flate2::Compression::default());
537        encoder.write_all(&tar_bytes).unwrap();
538        drop(encoder);
539
540        hyper::Response::builder()
541            .status(200)
542            .header("content-type", "application/octet-stream")
543            .body(http_body_util::Full::new(hyper::body::Bytes::from(
544                tar_gz_bytes,
545            )))
546            .unwrap()
547    }
548
549    fn dummy_tar_xz_response(sub_dir: &str) -> HyperResponse {
550        let mut tar_bytes = vec![];
551        let mut tar_builder = tar::Builder::new(&mut tar_bytes);
552        let mut header = tar::Header::new_gnu();
553        header.set_size(4);
554        tar_builder
555            .append_data(
556                &mut header,
557                format!("{sub_dir}/tar_contents.bin"),
558                &[1, 2, 3, 4][..],
559            )
560            .unwrap();
561        tar_builder.finish().unwrap();
562        drop(tar_builder);
563
564        let mut tar_xz_bytes = vec![];
565        let mut encoder = liblzma::write::XzEncoder::new(&mut tar_xz_bytes, 1);
566        encoder.write_all(&tar_bytes).unwrap();
567        drop(encoder);
568
569        hyper::Response::builder()
570            .status(200)
571            .header("content-type", "application/octet-stream")
572            .body(http_body_util::Full::new(hyper::body::Bytes::from(
573                tar_xz_bytes,
574            )))
575            .unwrap()
576    }
577
578    fn build_mock_server_data(
579        install_script: String,
580        private_key: &PrivateKey,
581    ) -> HashMap<String, HyperResponse> {
582        fn json_response(json: impl Into<String>) -> HyperResponse {
583            hyper::Response::builder()
584                .status(200)
585                .header("content-type", "application/json")
586                .body(http_body_util::Full::new(hyper::body::Bytes::from(
587                    json.into(),
588                )))
589                .unwrap()
590        }
591
592        fn not_found() -> HyperResponse {
593            hyper::Response::builder()
594                .status(404)
595                .body(http_body_util::Full::new(hyper::body::Bytes::from("")))
596                .unwrap()
597        }
598
599        fn text_response(text: String) -> HyperResponse {
600            hyper::Response::builder()
601                .status(200)
602                .header("content-type", "text/plain")
603                .body(http_body_util::Full::new(hyper::body::Bytes::from(text)))
604                .unwrap()
605        }
606
607        let mut risc0_groth16_manifest_json = json!({
608            "releases": {
609                "1.0.0": {
610                    "target_agnostic": {
611                        "artifact": {
612                            "sha256_blobs": [
613                                HELLO_WORLD_DUMMY_TAR_XZ_SHA256
614                            ]
615                        },
616                    }
617                },
618                "2.0.0": {
619                    "target_agnostic": {
620                        "artifact": {
621                            "sha256_blobs": [
622                                HELLO_WORLD2_DUMMY_TAR_XZ_SHA256
623                            ]
624                        },
625                    }
626                },
627                "3.0.0-badsha": {
628                    "target_agnostic": {
629                        "artifact": {
630                            "sha256_blobs": [
631                                HELLO_WORLD3_DUMMY_TAR_XZ_SHA256
632                            ]
633                        },
634                    }
635                },
636            },
637            "latest_version": "2.0.0"
638        });
639
640        let signature =
641            private_key.sign(&serde_json::to_vec(&risc0_groth16_manifest_json).unwrap()[..]);
642        risc0_groth16_manifest_json
643            .as_object_mut()
644            .unwrap()
645            .insert("signature".into(), signature.to_string().into());
646
647        maplit::hashmap! {
648            "/github_api/repos/risc0/risc0/releases/latest".into() => {
649                json_response("{\"tag_name\":\"v1.1.0\"}")
650            },
651            "/github_api/repos/risc0/risc0/releases/tags/v1.0.0".into() => json_response("{}"),
652            "/github_api/repos/risc0/risc0/releases/tags/v1.0.0-rc.1".into() => json_response("{}"),
653            "/github_api/repos/risc0/risc0/releases/tags/v1.0.0-rc.2".into() => json_response("{}"),
654            "/github_api/repos/risc0/rust/releases/tags/r0.1.79.0".into() => json_response("{}"),
655            "/risc0_github/risc0/releases/download/v1.0.0/\
656                cargo-risczero-x86_64-unknown-linux-gnu.tgz".into() => dummy_tar_gz_response(),
657            "/risc0_github/risc0/releases/download/v1.0.0-rc.1/\
658                cargo-risczero-x86_64-unknown-linux-gnu.tgz".into() => dummy_tar_gz_response(),
659            "/risc0_github/risc0/releases/download/v1.0.0-rc.2/\
660                cargo-risczero-x86_64-unknown-linux-gnu.tgz".into() => dummy_tar_gz_response(),
661            "/risc0_github/risc0/releases/download/v1.0.0/\
662                cargo-risczero-aarch64-apple-darwin.tgz".into() => dummy_tar_gz_response(),
663            "/risc0_github/rust/releases/download/r0.1.79.0/\
664                rust-toolchain-x86_64-unknown-linux-gnu.tar.gz".into() => dummy_tar_gz_response(),
665            "/risc0_github/rust/releases/download/r0.1.79.0/\
666                rust-toolchain-aarch64-apple-darwin.tar.gz".into() => dummy_tar_gz_response(),
667            "/github_api/repos/risc0/toolchain/releases/tags/2024.01.05".into() =>
668                json_response("{}"),
669            "/risc0_github/toolchain/releases/download/2024.01.05/\
670                riscv32im-linux-x86_64.tar.xz".into() =>
671                dummy_tar_xz_response("riscv32im-linux-x86_64"),
672            "/risc0_github/toolchain/releases/download/2024.01.05/\
673                riscv32im-gdb-linux-x86_64.tar.xz".into() => dummy_tar_xz_response("."),
674            "/risc0_github/toolchain/releases/download/2024.01.05/\
675                riscv32im-osx-arm64.tar.xz".into() => dummy_tar_xz_response("riscv32im-osx-arm64"),
676            "/risc0_github/toolchain/releases/download/2024.01.05/\
677                riscv32im-gdb-osx-arm64.tar.xz".into() => dummy_tar_xz_response("."),
678            "/github_api/repos/risc0/toolchain/releases/tags/2024.01.06".into() =>
679                json_response("{}"),
680            "/risc0_github/toolchain/releases/download/2024.01.06/\
681                riscv32im-linux-x86_64.tar.xz".into() =>
682                dummy_tar_xz_response("riscv32im-linux-x86_64"),
683            "/risc0_github/toolchain/releases/download/2024.01.06/\
684                riscv32im-gdb-linux-x86_64.tar.xz".into() => dummy_tar_xz_response("."),
685            "/github_api/repos/risc0/rust/releases/tags/r0.1.81.0".into() => json_response("{}"),
686            "/risc0_github/rust/releases/download/r0.1.81.0/\
687                rust-toolchain-x86_64-unknown-linux-gnu.tar.gz".into() => dummy_tar_gz_response(),
688            "/risc0_github/rust/releases/download/r0.1.81.0/\
689                rust-toolchain-aarch64-apple-darwin.tar.gz".into() => dummy_tar_gz_response(),
690            "/github_api/repos/risc0/risc0/releases/tags/v5.0.0".into() => not_found(),
691            "/github_api/repos/risc0/risc0/releases/tags/v1.1.0".into() => json_response("{}"),
692            "/risc0_github/risc0/releases/download/v1.1.0/\
693                cargo-risczero-x86_64-unknown-linux-gnu.tgz".into() => dummy_tar_gz_response(),
694            "/risc0/install".into() => text_response(install_script.clone()),
695            "/s3/rzup/components/risc0-groth16/distribution_manifest.json".into() =>
696                json_response(serde_json::to_string(&risc0_groth16_manifest_json).unwrap()),
697            format!("/s3/rzup/components/risc0-groth16/sha256/{HELLO_WORLD_DUMMY_TAR_XZ_SHA256}") => dummy_tar_xz_response("hello-world"),
698            format!("/s3/rzup/components/risc0-groth16/sha256/{HELLO_WORLD2_DUMMY_TAR_XZ_SHA256}") => dummy_tar_xz_response("hello-world2"),
699            format!("/s3/rzup/components/risc0-groth16/sha256/{HELLO_WORLD3_DUMMY_TAR_XZ_SHA256}") => dummy_tar_xz_response("hello-world2"),
700        }
701    }
702
703    fn hyper_len(resp: HyperResponse) -> u64 {
704        use hyper::body::Body as _;
705
706        resp.body().size_hint().exact().unwrap()
707    }
708
709    async fn request_handler(
710        require_bearer_token: bool,
711        server_data: Arc<Mutex<HashMap<String, HyperResponse>>>,
712        req: hyper::Request<hyper::body::Incoming>,
713    ) -> std::result::Result<HyperResponse, Infallible> {
714        if require_bearer_token {
715            let value = req
716                .headers()
717                .get("Authorization")
718                .expect("Authorization provided");
719            assert_eq!(value, "Bearer suchsecrettesttoken");
720        }
721
722        let req_uri = req.uri().to_string();
723        if req.method() == http::Method::GET {
724            if let Some(response) = server_data.lock().unwrap().get(&req_uri) {
725                Ok(response.clone())
726            } else {
727                panic!("unexpected URI: {req_uri}");
728            }
729        } else if req.method() == http::Method::PUT {
730            use http_body_util::BodyExt as _;
731
732            // required S3 upload headers
733            for h in ["x-amz-date", "authorization", "x-amz-content-sha256"] {
734                req.headers()
735                    .get(h)
736                    .unwrap_or_else(|| panic!("expected header: {h}"));
737            }
738
739            let value = req
740                .headers()
741                .get("content-type")
742                .expect("expected header: content-type");
743            assert_eq!(value, "application/octet-stream");
744
745            let resp = hyper::Response::builder()
746                .status(200)
747                .header("content-type", "application/octet-stream")
748                .body(http_body_util::Full::new(
749                    req.into_body().collect().await.unwrap().to_bytes(),
750                ))
751                .unwrap();
752            server_data.lock().unwrap().insert(req_uri, resp);
753
754            Ok(hyper::Response::builder()
755                .status(200)
756                .header("content-type", "application/xml")
757                .body(http_body_util::Full::new(hyper::body::Bytes::from(
758                    "<xml>ok</xml>",
759                )))
760                .unwrap())
761        } else {
762            panic!("unexpected HTTP method {}", req.method());
763        }
764    }
765
766    #[tokio::main]
767    async fn server_main(
768        install_script: String,
769        require_bearer_token: bool,
770        private_key: PrivateKey,
771        sender: tokio::sync::oneshot::Sender<SocketAddr>,
772    ) {
773        let server_data = Arc::new(Mutex::new(build_mock_server_data(
774            install_script,
775            &private_key,
776        )));
777
778        let listener = tokio::net::TcpListener::bind("localhost:0").await.unwrap();
779        sender.send(listener.local_addr().unwrap()).unwrap();
780
781        while let Ok((stream, _)) = listener.accept().await {
782            let server_data = server_data.clone();
783            hyper::server::conn::http1::Builder::new()
784                .serve_connection(
785                    hyper_util::rt::TokioIo::new(stream),
786                    hyper::service::service_fn(move |req| {
787                        request_handler(require_bearer_token, server_data.clone(), req)
788                    }),
789                )
790                .await
791                .unwrap()
792        }
793    }
794
795    fn test_private_key() -> PrivateKey {
796        let mut rng = rand::thread_rng();
797        rsa::RsaPrivateKey::new(&mut rng, 2048).unwrap().into()
798    }
799
800    impl MockDistributionServer {
801        pub fn new_with_options(
802            install_script: String,
803            require_bearer_token: bool,
804            private_key: PrivateKey,
805        ) -> Self {
806            let (send, recv) = tokio::sync::oneshot::channel();
807            let private_key_other = private_key.clone();
808            std::thread::spawn(move || {
809                server_main(
810                    install_script,
811                    require_bearer_token,
812                    private_key_other,
813                    send,
814                )
815            });
816            let address = recv.blocking_recv().unwrap();
817            Self {
818                base_urls: BaseUrls {
819                    risc0_github_base_url: format!("http://{address}/risc0_github"),
820                    github_api_base_url: format!("http://{address}/github_api"),
821                    risc0_base_url: format!("http://{address}/risc0"),
822                    s3_base_url: format!("http://{address}/s3"),
823                },
824                private_key,
825            }
826        }
827
828        pub fn new() -> Self {
829            Self::new_with_options("".into(), false, test_private_key())
830        }
831
832        pub fn new_with_install_script(install_script: String) -> Self {
833            Self::new_with_options(install_script, false, test_private_key())
834        }
835
836        pub fn new_with_required_bearer_token() -> Self {
837            Self::new_with_options("".into(), true, test_private_key())
838        }
839    }
840
841    #[macro_export]
842    macro_rules! http_test_harness {
843        ($test_name:ident) => {
844            paste::paste! {
845                #[test]
846                #[ignore = "requires GitHub API access"]
847                fn [<$test_name _against_real_github>]() {
848                    $test_name(Default::default(), PublicKey::official())
849                }
850
851                #[test]
852                fn [<$test_name _against_mock_server>]() {
853                    let server = $crate::tests::MockDistributionServer::new();
854                    $test_name(server.base_urls.clone(), server.private_key.public_key())
855                }
856            }
857        };
858    }
859
860    pub fn invalid_base_urls() -> BaseUrls {
861        BaseUrls {
862            risc0_github_base_url: "".into(),
863            github_api_base_url: "".into(),
864            risc0_base_url: "".into(),
865            s3_base_url: "".into(),
866        }
867    }
868
869    fn setup_test_env(
870        base_urls: BaseUrls,
871        github_token: Option<String>,
872        aws_creds: Option<AwsCredentials>,
873        private_key: PrivateKey,
874        platform: Platform,
875    ) -> (TempDir, Rzup) {
876        let tmp_dir = TempDir::new().unwrap();
877        let public_key = private_key.public_key();
878        let rzup = Rzup::with_paths_urls_creds_platform_and_event_handler(
879            tmp_dir.path().join(".risc0"),
880            tmp_dir.path().join(".rustup"),
881            tmp_dir.path().join(".cargo"),
882            base_urls,
883            github_token,
884            move || aws_creds.clone(),
885            move || Ok(private_key.clone()),
886            public_key,
887            platform,
888            |_| {},
889        )
890        .unwrap();
891        (tmp_dir, rzup)
892    }
893
894    #[test]
895    fn test_rzup_initialization() {
896        let (_tmp_dir, rzup) = setup_test_env(
897            invalid_base_urls(),
898            None,
899            None,
900            test_private_key(),
901            Platform::new("x86_64", Os::Linux),
902        );
903        assert!(rzup
904            .settings()
905            .get_default_version(&Component::RustToolchain)
906            .is_none());
907        assert!(rzup
908            .settings()
909            .get_default_version(&Component::CargoRiscZero)
910            .is_none());
911    }
912
913    fn test_install_and_uninstall_end_to_end(base_urls: BaseUrls, public_key: PublicKey) {
914        let tmp_dir = TempDir::new().unwrap();
915        let mut rzup = Rzup::with_paths_urls_creds_platform_and_event_handler(
916            tmp_dir.path().join(".risc0"),
917            tmp_dir.path().join(".rustup"),
918            tmp_dir.path().join(".cargo"),
919            base_urls,
920            None, /* github_token */
921            || None,
922            || Err(RzupError::Other("no private key".into())),
923            public_key,
924            Platform::detect().unwrap(),
925            |_| {},
926        )
927        .unwrap();
928
929        let cargo_risczero_version = Version::new(1, 0, 0);
930
931        assert_eq!(
932            rzup.get_version_dir(&Component::CargoRiscZero, &cargo_risczero_version),
933            Err(RzupError::VersionNotFound(cargo_risczero_version.clone()))
934        );
935        assert_eq!(
936            rzup.get_version_dir(&Component::R0Vm, &cargo_risczero_version),
937            Err(RzupError::VersionNotFound(cargo_risczero_version.clone()))
938        );
939
940        // Test installation
941        rzup.install_component(
942            &Component::CargoRiscZero,
943            Some(cargo_risczero_version.clone()),
944            false,
945        )
946        .unwrap();
947        assert!(rzup
948            .version_exists(&Component::CargoRiscZero, &cargo_risczero_version)
949            .unwrap());
950        assert_eq!(
951            rzup.settings()
952                .get_default_version(&Component::CargoRiscZero)
953                .unwrap(),
954            cargo_risczero_version
955        );
956        assert_eq!(
957            rzup.list_versions(&Component::CargoRiscZero).unwrap(),
958            vec![Version::new(1, 0, 0)]
959        );
960        assert!(rzup
961            .get_version_dir(&Component::CargoRiscZero, &cargo_risczero_version)
962            .is_ok());
963
964        // Test uninstallation
965        rzup.uninstall_component(&Component::CargoRiscZero, cargo_risczero_version.clone())
966            .unwrap();
967        assert!(!rzup
968            .version_exists(&Component::CargoRiscZero, &cargo_risczero_version)
969            .unwrap());
970        assert_eq!(
971            rzup.list_versions(&Component::CargoRiscZero).unwrap(),
972            vec![]
973        );
974        assert_eq!(
975            rzup.get_version_dir(&Component::CargoRiscZero, &cargo_risczero_version),
976            Err(RzupError::VersionNotFound(cargo_risczero_version.clone()))
977        );
978
979        // Test virtual installation
980        rzup.install_component(
981            &Component::R0Vm,
982            Some(cargo_risczero_version.clone()),
983            false,
984        )
985        .unwrap();
986        assert!(rzup
987            .version_exists(&Component::R0Vm, &cargo_risczero_version)
988            .unwrap());
989        assert_eq!(
990            rzup.settings()
991                .get_default_version(&Component::R0Vm)
992                .unwrap(),
993            cargo_risczero_version
994        );
995        assert_eq!(
996            rzup.list_versions(&Component::R0Vm).unwrap(),
997            vec![Version::new(1, 0, 0)]
998        );
999        assert!(rzup
1000            .get_version_dir(&Component::R0Vm, &cargo_risczero_version)
1001            .is_ok());
1002
1003        // Test uninstallation
1004        rzup.uninstall_component(&Component::R0Vm, cargo_risczero_version.clone())
1005            .unwrap();
1006        assert!(!rzup
1007            .version_exists(&Component::R0Vm, &cargo_risczero_version)
1008            .unwrap());
1009        assert_eq!(rzup.list_versions(&Component::R0Vm).unwrap(), vec![]);
1010        assert_eq!(
1011            rzup.get_version_dir(&Component::R0Vm, &cargo_risczero_version),
1012            Err(RzupError::VersionNotFound(cargo_risczero_version.clone()))
1013        );
1014
1015        // Rust
1016        let rust_version = Version::new(1, 79, 0);
1017        assert_eq!(
1018            rzup.get_version_dir(&Component::RustToolchain, &rust_version),
1019            Err(RzupError::VersionNotFound(rust_version.clone()))
1020        );
1021        rzup.install_component(&Component::RustToolchain, Some(rust_version.clone()), false)
1022            .unwrap();
1023        assert!(rzup
1024            .version_exists(&Component::RustToolchain, &rust_version)
1025            .unwrap());
1026        assert_eq!(
1027            rzup.list_versions(&Component::RustToolchain).unwrap(),
1028            vec![Version::new(1, 79, 0)]
1029        );
1030        assert!(rzup
1031            .get_version_dir(&Component::RustToolchain, &rust_version)
1032            .is_ok());
1033
1034        // Test uninstallation
1035        rzup.uninstall_component(&Component::RustToolchain, rust_version.clone())
1036            .unwrap();
1037        assert!(!rzup
1038            .version_exists(&Component::RustToolchain, &rust_version)
1039            .unwrap());
1040        assert_eq!(
1041            rzup.list_versions(&Component::RustToolchain).unwrap(),
1042            vec![]
1043        );
1044        assert_eq!(
1045            rzup.get_version_dir(&Component::RustToolchain, &rust_version),
1046            Err(RzupError::VersionNotFound(rust_version.clone()))
1047        );
1048
1049        // C++
1050        let cpp_version = Version::new(2024, 1, 5);
1051        assert_eq!(
1052            rzup.get_version_dir(&Component::CppToolchain, &cpp_version),
1053            Err(RzupError::VersionNotFound(cpp_version.clone()))
1054        );
1055        rzup.install_component(&Component::CppToolchain, Some(cpp_version.clone()), false)
1056            .unwrap();
1057        assert!(rzup
1058            .version_exists(&Component::CppToolchain, &cpp_version)
1059            .unwrap());
1060        assert_eq!(
1061            rzup.list_versions(&Component::CppToolchain).unwrap(),
1062            vec![Version::new(2024, 1, 5)]
1063        );
1064        assert!(rzup
1065            .get_version_dir(&Component::CppToolchain, &cpp_version)
1066            .is_ok());
1067
1068        // Test uninstallation
1069        rzup.uninstall_component(&Component::CppToolchain, cpp_version.clone())
1070            .unwrap();
1071        assert!(!rzup
1072            .version_exists(&Component::CppToolchain, &cpp_version)
1073            .unwrap());
1074        assert_eq!(
1075            rzup.list_versions(&Component::CppToolchain).unwrap(),
1076            vec![]
1077        );
1078        assert_eq!(
1079            rzup.get_version_dir(&Component::CppToolchain, &cpp_version),
1080            Err(RzupError::VersionNotFound(cpp_version.clone()))
1081        );
1082
1083        // groth16
1084        let groth16_version = Version::new(1, 0, 0);
1085        assert_eq!(
1086            rzup.get_version_dir(&Component::Risc0Groth16, &groth16_version),
1087            Err(RzupError::VersionNotFound(groth16_version.clone()))
1088        );
1089        rzup.install_component(
1090            &Component::Risc0Groth16,
1091            Some(groth16_version.clone()),
1092            false,
1093        )
1094        .unwrap();
1095        assert!(rzup
1096            .version_exists(&Component::Risc0Groth16, &groth16_version)
1097            .unwrap());
1098        assert_eq!(
1099            rzup.list_versions(&Component::Risc0Groth16).unwrap(),
1100            vec![Version::new(1, 0, 0)]
1101        );
1102        assert!(rzup
1103            .get_version_dir(&Component::Risc0Groth16, &groth16_version)
1104            .is_ok());
1105
1106        // Test uninstallation
1107        rzup.uninstall_component(&Component::Risc0Groth16, groth16_version.clone())
1108            .unwrap();
1109        assert!(!rzup
1110            .version_exists(&Component::Risc0Groth16, &groth16_version)
1111            .unwrap());
1112        assert_eq!(
1113            rzup.list_versions(&Component::Risc0Groth16).unwrap(),
1114            vec![]
1115        );
1116        assert_eq!(
1117            rzup.get_version_dir(&Component::Risc0Groth16, &groth16_version),
1118            Err(RzupError::VersionNotFound(groth16_version.clone()))
1119        );
1120    }
1121
1122    http_test_harness!(test_install_and_uninstall_end_to_end);
1123
1124    fn run_and_assert_events(
1125        rzup: &mut Rzup,
1126        body: impl FnOnce(&mut Rzup),
1127        expected_events: Vec<RzupEvent>,
1128    ) {
1129        let (event_send, event_recv) = std::sync::mpsc::channel();
1130        rzup.set_event_handler(move |event| {
1131            event_send.send(event).unwrap();
1132        });
1133
1134        body(rzup);
1135        rzup.set_event_handler(|_| {});
1136
1137        let mut events = vec![];
1138        while let Ok(event) = event_recv.recv() {
1139            if !matches!(event, RzupEvent::Debug { .. }) {
1140                events.push(event);
1141            }
1142        }
1143        assert_eq!(events, expected_events);
1144    }
1145
1146    fn assert_symlinks(path: &Path, mut expected_symlinks: Vec<(String, String)>) {
1147        let mut found_symlinks = vec![];
1148        for entry in walkdir::WalkDir::new(path) {
1149            let entry = entry.unwrap();
1150            if entry.path_is_symlink() {
1151                let entry_path = entry.path();
1152                let entry_relative_path = entry_path.strip_prefix(path).unwrap();
1153                let target_path = std::fs::read_link(entry_path).unwrap();
1154                let target_relative_path = target_path.strip_prefix(path).unwrap();
1155                found_symlinks.push((
1156                    entry_relative_path.to_str().unwrap().to_owned(),
1157                    target_relative_path.to_str().unwrap().to_owned(),
1158                ));
1159            }
1160        }
1161
1162        found_symlinks.sort();
1163        expected_symlinks.sort();
1164        assert_eq!(found_symlinks, expected_symlinks);
1165    }
1166
1167    fn assert_files(path: &Path, mut expected_files: Vec<String>) {
1168        // These files are expected to be here always more or less
1169        expected_files.push(".risc0/settings.toml".into());
1170        expected_files.push(".risc0/.rzup".into());
1171
1172        let mut found_files = vec![];
1173        for entry in walkdir::WalkDir::new(path.join(".risc0")) {
1174            let entry = entry.unwrap();
1175            if entry.file_type().is_file() {
1176                let entry_path = entry.path();
1177                if entry_path.extension().is_none_or(|ext| ext != "lock") {
1178                    let entry_relative_path = entry_path.strip_prefix(path).unwrap();
1179                    found_files.push(entry_relative_path.to_str().unwrap().to_owned());
1180                }
1181            }
1182        }
1183
1184        found_files.sort();
1185        expected_files.sort();
1186        assert_eq!(found_files, expected_files);
1187    }
1188
1189    #[allow(clippy::too_many_arguments)]
1190    fn fresh_install_test(
1191        base_urls: BaseUrls,
1192        private_key: PrivateKey,
1193        component: Component,
1194        component_to_install: Component,
1195        version: Version,
1196        expected_url: String,
1197        download_length: u64,
1198        expected_files: Vec<String>,
1199        expected_symlinks: Vec<(String, String)>,
1200        expected_version_dir: &str,
1201        use_github_token: bool,
1202        platform: Platform,
1203    ) {
1204        let github_token = use_github_token.then_some("suchsecrettesttoken".into());
1205        let (tmp_dir, mut rzup) =
1206            setup_test_env(base_urls.clone(), github_token, None, private_key, platform);
1207
1208        run_and_assert_events(
1209            &mut rzup,
1210            |rzup| {
1211                rzup.install_component(&component, Some(version.clone()), false)
1212                    .unwrap();
1213                assert!(rzup.version_exists(&component, &version).unwrap());
1214            },
1215            vec![
1216                RzupEvent::InstallationStarted {
1217                    id: component.to_string(),
1218                    version: version.to_string(),
1219                },
1220                RzupEvent::TransferStarted {
1221                    kind: TransferKind::Download,
1222                    id: component_to_install.to_string(),
1223                    version: Some(version.to_string()),
1224                    url: Some(expected_url),
1225                    len: Some(download_length),
1226                },
1227                RzupEvent::TransferProgress {
1228                    id: component_to_install.to_string(),
1229                    incr: download_length,
1230                },
1231                RzupEvent::TransferCompleted {
1232                    kind: TransferKind::Download,
1233                    id: component_to_install.to_string(),
1234                    version: Some(version.to_string()),
1235                },
1236                RzupEvent::InstallationCompleted {
1237                    id: component.to_string(),
1238                    version: version.to_string(),
1239                },
1240            ],
1241        );
1242
1243        let actual_version_dir = rzup.get_version_dir(&component, &version).unwrap();
1244        assert_eq!(
1245            actual_version_dir
1246                .strip_prefix(tmp_dir.path())
1247                .unwrap()
1248                .to_str()
1249                .unwrap(),
1250            expected_version_dir
1251        );
1252
1253        assert_symlinks(tmp_dir.path(), expected_symlinks);
1254        assert_files(tmp_dir.path(), expected_files);
1255    }
1256
1257    #[allow(clippy::too_many_arguments)]
1258    fn already_installed_test(
1259        base_urls: BaseUrls,
1260        private_key: PrivateKey,
1261        component: Component,
1262        version: Version,
1263        expected_files: Vec<String>,
1264        expected_symlinks: Vec<(String, String)>,
1265        expected_version_dir: &str,
1266        use_github_token: bool,
1267        platform: Platform,
1268    ) {
1269        let github_token = use_github_token.then_some("suchsecrettesttoken".into());
1270        let (tmp_dir, mut rzup) =
1271            setup_test_env(base_urls.clone(), github_token, None, private_key, platform);
1272
1273        rzup.install_component(&component, Some(version.clone()), false)
1274            .unwrap();
1275
1276        run_and_assert_events(
1277            &mut rzup,
1278            |rzup| {
1279                rzup.install_component(&component, Some(version.clone()), false)
1280                    .unwrap();
1281            },
1282            vec![RzupEvent::ComponentAlreadyInstalled {
1283                id: component.to_string(),
1284                version: version.to_string(),
1285            }],
1286        );
1287
1288        let actual_version_dir = rzup.get_version_dir(&component, &version).unwrap();
1289        assert_eq!(
1290            actual_version_dir
1291                .strip_prefix(tmp_dir.path())
1292                .unwrap()
1293                .to_str()
1294                .unwrap(),
1295            expected_version_dir
1296        );
1297
1298        assert_symlinks(tmp_dir.path(), expected_symlinks);
1299        assert_files(tmp_dir.path(), expected_files);
1300    }
1301
1302    #[allow(clippy::too_many_arguments)]
1303    fn already_installed_force_test(
1304        base_urls: BaseUrls,
1305        private_key: PrivateKey,
1306        component: Component,
1307        component_to_install: Component,
1308        version: Version,
1309        expected_url: String,
1310        download_length: u64,
1311        expected_files: Vec<String>,
1312        expected_symlinks: Vec<(String, String)>,
1313        expected_version_dir: &str,
1314        use_github_token: bool,
1315        platform: Platform,
1316    ) {
1317        let github_token = use_github_token.then_some("suchsecrettesttoken".into());
1318        let (tmp_dir, mut rzup) =
1319            setup_test_env(base_urls.clone(), github_token, None, private_key, platform);
1320
1321        rzup.install_component(&component, Some(version.clone()), false)
1322            .unwrap();
1323
1324        run_and_assert_events(
1325            &mut rzup,
1326            |rzup| {
1327                rzup.install_component(&component, Some(version.clone()), true)
1328                    .unwrap();
1329            },
1330            vec![
1331                RzupEvent::InstallationStarted {
1332                    id: component.to_string(),
1333                    version: version.to_string(),
1334                },
1335                RzupEvent::TransferStarted {
1336                    kind: TransferKind::Download,
1337                    id: component_to_install.to_string(),
1338                    version: Some(version.to_string()),
1339                    url: Some(expected_url),
1340                    len: Some(download_length),
1341                },
1342                RzupEvent::TransferProgress {
1343                    id: component_to_install.to_string(),
1344                    incr: download_length,
1345                },
1346                RzupEvent::TransferCompleted {
1347                    kind: TransferKind::Download,
1348                    id: component_to_install.to_string(),
1349                    version: Some(version.to_string()),
1350                },
1351                RzupEvent::InstallationCompleted {
1352                    id: component.to_string(),
1353                    version: version.to_string(),
1354                },
1355            ],
1356        );
1357
1358        let actual_version_dir = rzup.get_version_dir(&component, &version).unwrap();
1359        assert_eq!(
1360            actual_version_dir
1361                .strip_prefix(tmp_dir.path())
1362                .unwrap()
1363                .to_str()
1364                .unwrap(),
1365            expected_version_dir
1366        );
1367
1368        assert_symlinks(tmp_dir.path(), expected_symlinks);
1369        assert_files(tmp_dir.path(), expected_files);
1370    }
1371
1372    #[allow(clippy::too_many_arguments)]
1373    fn install_test(
1374        base_urls: BaseUrls,
1375        private_key: PrivateKey,
1376        component: Component,
1377        component_to_install: Component,
1378        version: Version,
1379        expected_url: String,
1380        download_length: u64,
1381        expected_files: Vec<String>,
1382        expected_symlinks: Vec<(String, String)>,
1383        expected_version_dir: &str,
1384        use_github_token: bool,
1385        platform: Platform,
1386    ) {
1387        fresh_install_test(
1388            base_urls.clone(),
1389            private_key.clone(),
1390            component,
1391            component_to_install,
1392            version.clone(),
1393            expected_url.clone(),
1394            download_length,
1395            expected_files.clone(),
1396            expected_symlinks.clone(),
1397            expected_version_dir,
1398            use_github_token,
1399            platform,
1400        );
1401
1402        already_installed_test(
1403            base_urls.clone(),
1404            private_key.clone(),
1405            component,
1406            version.clone(),
1407            expected_files.clone(),
1408            expected_symlinks.clone(),
1409            expected_version_dir,
1410            use_github_token,
1411            platform,
1412        );
1413
1414        already_installed_force_test(
1415            base_urls.clone(),
1416            private_key,
1417            component,
1418            component_to_install,
1419            version.clone(),
1420            expected_url.clone(),
1421            download_length,
1422            expected_files.clone(),
1423            expected_symlinks.clone(),
1424            expected_version_dir,
1425            use_github_token,
1426            platform,
1427        );
1428    }
1429
1430    fn test_install_cargo_risczero(platform: Platform, target_triple: &str) {
1431        let server = MockDistributionServer::new();
1432        install_test(
1433            server.base_urls.clone(),
1434            server.private_key.clone(),
1435            Component::CargoRiscZero,
1436            Component::CargoRiscZero,
1437            Version::new(1, 0, 0),
1438            format!(
1439                "{base_url}/risc0/releases/download/v1.0.0/\
1440                cargo-risczero-{target_triple}.tgz",
1441                base_url = server.base_urls.risc0_github_base_url
1442            ),
1443            hyper_len(dummy_tar_gz_response()),
1444            vec![format!(
1445                ".risc0/extensions/v1.0.0-cargo-risczero-{target_triple}/tar_contents.bin"
1446            )],
1447            vec![(
1448                ".cargo/bin/cargo-risczero".into(),
1449                format!(".risc0/extensions/v1.0.0-cargo-risczero-{target_triple}/cargo-risczero"),
1450            )],
1451            &format!(".risc0/extensions/v1.0.0-cargo-risczero-{target_triple}"),
1452            false, /* use_github_token */
1453            platform,
1454        )
1455    }
1456
1457    #[test]
1458    fn install_cargo_risczero_x86_64_linux() {
1459        test_install_cargo_risczero(
1460            Platform::new("x86_64", Os::Linux),
1461            "x86_64-unknown-linux-gnu",
1462        );
1463    }
1464
1465    #[test]
1466    fn install_cargo_risczero_aarch64_mac() {
1467        test_install_cargo_risczero(Platform::new("aarch64", Os::MacOs), "aarch64-apple-darwin");
1468    }
1469
1470    fn test_install_r0vm(platform: Platform, target_triple: &str) {
1471        let server = MockDistributionServer::new();
1472        install_test(
1473            server.base_urls.clone(),
1474            server.private_key.clone(),
1475            Component::R0Vm,
1476            Component::CargoRiscZero,
1477            Version::new(1, 0, 0),
1478            format!(
1479                "{base_url}/risc0/releases/download/v1.0.0/\
1480                cargo-risczero-{target_triple}.tgz",
1481                base_url = server.base_urls.risc0_github_base_url
1482            ),
1483            hyper_len(dummy_tar_gz_response()),
1484            vec![format!(
1485                ".risc0/extensions/v1.0.0-cargo-risczero-{target_triple}/tar_contents.bin"
1486            )],
1487            vec![
1488                (
1489                    ".cargo/bin/cargo-risczero".into(),
1490                    format!(
1491                        ".risc0/extensions/v1.0.0-cargo-risczero-{target_triple}/cargo-risczero"
1492                    ),
1493                ),
1494                (
1495                    ".cargo/bin/r0vm".into(),
1496                    format!(".risc0/extensions/v1.0.0-cargo-risczero-{target_triple}/r0vm"),
1497                ),
1498            ],
1499            &format!(".risc0/extensions/v1.0.0-cargo-risczero-{target_triple}"),
1500            false, /* use_github_token */
1501            platform,
1502        )
1503    }
1504
1505    #[test]
1506    fn install_r0vm_x86_64_linux() {
1507        test_install_r0vm(
1508            Platform::new("x86_64", Os::Linux),
1509            "x86_64-unknown-linux-gnu",
1510        );
1511    }
1512
1513    #[test]
1514    fn install_r0vm_aarch64_mac() {
1515        test_install_r0vm(Platform::new("aarch64", Os::MacOs), "aarch64-apple-darwin");
1516    }
1517
1518    fn test_install_rust(platform: Platform, target_triple: &str) {
1519        let server = MockDistributionServer::new();
1520        install_test(
1521            server.base_urls.clone(),
1522            server.private_key.clone(),
1523            Component::RustToolchain,
1524            Component::RustToolchain,
1525            Version::new(1, 81, 0),
1526            format!(
1527                "{base_url}/rust/releases/download/r0.1.81.0/\
1528                rust-toolchain-{target_triple}.tar.gz",
1529                base_url = server.base_urls.risc0_github_base_url
1530            ),
1531            hyper_len(dummy_tar_gz_response()),
1532            vec![format!(
1533                ".risc0/toolchains/v1.81.0-rust-{target_triple}/tar_contents.bin"
1534            )],
1535            vec![(
1536                ".rustup/toolchains/risc0".into(),
1537                format!(".risc0/toolchains/v1.81.0-rust-{target_triple}"),
1538            )],
1539            &format!(".risc0/toolchains/v1.81.0-rust-{target_triple}"),
1540            false, /* use_github_token */
1541            platform,
1542        )
1543    }
1544
1545    #[test]
1546    fn install_rust_x86_64_linux() {
1547        test_install_rust(
1548            Platform::new("x86_64", Os::Linux),
1549            "x86_64-unknown-linux-gnu",
1550        );
1551    }
1552
1553    #[test]
1554    fn install_rust_aarch64_mac() {
1555        test_install_rust(Platform::new("aarch64", Os::MacOs), "aarch64-apple-darwin");
1556    }
1557
1558    fn test_install_cpp(platform: Platform, target_double: &str, target_triple: &str) {
1559        let server = MockDistributionServer::new();
1560
1561        // This is just the size of the archive we end up creating.
1562        let download_size = hyper_len(dummy_tar_xz_response(&format!("riscv32im-{target_double}")));
1563
1564        install_test(
1565            server.base_urls.clone(),
1566            server.private_key.clone(),
1567            Component::CppToolchain,
1568            Component::CppToolchain,
1569            Version::new(2024, 1, 5),
1570            format!(
1571                "{base_url}/toolchain/releases/download/2024.01.05/\
1572                riscv32im-{target_double}.tar.xz",
1573                base_url = server.base_urls.risc0_github_base_url
1574            ),
1575            download_size,
1576            vec![format!(
1577                ".risc0/toolchains/v2024.1.5-cpp-{target_triple}/\
1578                riscv32im-{target_double}/tar_contents.bin"
1579            )],
1580            vec![(
1581                ".risc0/cpp".into(),
1582                format!(
1583                    ".risc0/toolchains/v2024.1.5-cpp-{target_triple}/riscv32im-{target_double}"
1584                ),
1585            )],
1586            &format!(".risc0/toolchains/v2024.1.5-cpp-{target_triple}/riscv32im-{target_double}"),
1587            false, /* use_github_token */
1588            platform,
1589        )
1590    }
1591
1592    #[test]
1593    fn install_cpp_x86_64_linux() {
1594        test_install_cpp(
1595            Platform::new("x86_64", Os::Linux),
1596            "linux-x86_64",
1597            "x86_64-unknown-linux-gnu",
1598        );
1599    }
1600
1601    #[test]
1602    fn install_cpp_aarch64_mac() {
1603        test_install_cpp(
1604            Platform::new("aarch64", Os::MacOs),
1605            "osx-arm64",
1606            "aarch64-apple-darwin",
1607        );
1608    }
1609
1610    fn test_install_gdb(platform: Platform, target_double: &str, target_triple: &str) {
1611        let server = MockDistributionServer::new();
1612
1613        install_test(
1614            server.base_urls.clone(),
1615            server.private_key.clone(),
1616            Component::Gdb,
1617            Component::Gdb,
1618            Version::new(2024, 1, 5),
1619            format!(
1620                "{base_url}/toolchain/releases/download/2024.01.05/\
1621                riscv32im-gdb-{target_double}.tar.xz",
1622                base_url = server.base_urls.risc0_github_base_url
1623            ),
1624            hyper_len(dummy_tar_xz_response(".")), /* download_size */
1625            vec![format!(
1626                ".risc0/extensions/v2024.1.5-gdb-{target_triple}/tar_contents.bin"
1627            )],
1628            vec![(
1629                ".risc0/bin/riscv32im-gdb".into(),
1630                format!(".risc0/extensions/v2024.1.5-gdb-{target_triple}/riscv32im-gdb"),
1631            )],
1632            &format!(".risc0/extensions/v2024.1.5-gdb-{target_triple}"),
1633            false, /* use_github_token */
1634            platform,
1635        )
1636    }
1637
1638    #[test]
1639    fn install_gdb_x86_64_linux() {
1640        test_install_gdb(
1641            Platform::new("x86_64", Os::Linux),
1642            "linux-x86_64",
1643            "x86_64-unknown-linux-gnu",
1644        );
1645    }
1646
1647    #[test]
1648    fn install_gdb_aarch64_mac() {
1649        test_install_gdb(
1650            Platform::new("aarch64", Os::MacOs),
1651            "osx-arm64",
1652            "aarch64-apple-darwin",
1653        );
1654    }
1655
1656    fn test_install_risc0_groth16(platform: Platform) {
1657        let server = MockDistributionServer::new();
1658
1659        install_test(
1660            server.base_urls.clone(),
1661            server.private_key.clone(),
1662            Component::Risc0Groth16,
1663            Component::Risc0Groth16,
1664            Version::new(1, 0, 0),
1665            format!(
1666                "{base_url}/rzup/components/risc0-groth16/sha256/{HELLO_WORLD_DUMMY_TAR_XZ_SHA256}",
1667                base_url = server.base_urls.s3_base_url
1668            ),
1669            140, /* download_size */
1670            vec![format!(
1671                ".risc0/extensions/v1.0.0-risc0-groth16/hello-world/tar_contents.bin"
1672            )],
1673            vec![],
1674            ".risc0/extensions/v1.0.0-risc0-groth16",
1675            false, /* use_github_token */
1676            platform,
1677        )
1678    }
1679
1680    #[test]
1681    fn install_risc0_groth16_x86_64_linux() {
1682        test_install_risc0_groth16(Platform::new("x86_64", Os::Linux));
1683    }
1684
1685    #[test]
1686    fn install_risc0_groth16_aarch64_mac() {
1687        test_install_risc0_groth16(Platform::new("aarch64", Os::MacOs));
1688    }
1689
1690    #[test]
1691    fn install_with_github_token() {
1692        let server = MockDistributionServer::new_with_required_bearer_token();
1693        install_test(
1694            server.base_urls.clone(),
1695            server.private_key.clone(),
1696            Component::CargoRiscZero,
1697            Component::CargoRiscZero,
1698            Version::new(1, 0, 0),
1699            format!(
1700                "{base_url}/risc0/releases/download/v1.0.0/\
1701                cargo-risczero-x86_64-unknown-linux-gnu.tgz",
1702                base_url = server.base_urls.risc0_github_base_url
1703            ),
1704            hyper_len(dummy_tar_gz_response()),
1705            vec![
1706                ".risc0/extensions/v1.0.0-cargo-risczero-x86_64-unknown-linux-gnu/tar_contents.bin"
1707                    .into(),
1708            ],
1709            vec![(
1710                ".cargo/bin/cargo-risczero".into(),
1711                ".risc0/extensions/v1.0.0-cargo-risczero-x86_64-unknown-linux-gnu/cargo-risczero"
1712                    .into(),
1713            )],
1714            ".risc0/extensions/v1.0.0-cargo-risczero-x86_64-unknown-linux-gnu",
1715            true, /* use_github_token */
1716            Platform::new("x86_64", Os::Linux),
1717        )
1718    }
1719
1720    fn test_list_multiple_versions(component: Component, version1: Version, version2: Version) {
1721        let server = MockDistributionServer::new();
1722        let (_tmp_dir, mut rzup) = setup_test_env(
1723            server.base_urls.clone(),
1724            None,
1725            None,
1726            server.private_key.clone(),
1727            Platform::new("x86_64", Os::Linux),
1728        );
1729
1730        rzup.install_component(&component, Some(version1.clone()), false)
1731            .unwrap();
1732
1733        rzup.install_component(&component, Some(version2.clone()), false)
1734            .unwrap();
1735
1736        assert_eq!(
1737            rzup.list_versions(&component).unwrap(),
1738            vec![version2, version1]
1739        );
1740    }
1741
1742    #[test]
1743    fn list_multiple_versions_cargo_risczero() {
1744        test_list_multiple_versions(
1745            Component::CargoRiscZero,
1746            Version::new(1, 0, 0),
1747            Version::new(1, 1, 0),
1748        );
1749    }
1750
1751    #[test]
1752    fn list_multiple_versions_r0vm() {
1753        test_list_multiple_versions(
1754            Component::R0Vm,
1755            Version::new(1, 0, 0),
1756            Version::new(1, 1, 0),
1757        );
1758    }
1759
1760    #[test]
1761    fn list_multiple_versions_rust() {
1762        test_list_multiple_versions(
1763            Component::RustToolchain,
1764            Version::new(1, 79, 0),
1765            Version::new(1, 81, 0),
1766        );
1767    }
1768
1769    #[test]
1770    fn list_multiple_versions_cpp() {
1771        test_list_multiple_versions(
1772            Component::CppToolchain,
1773            Version::new(2024, 1, 5),
1774            Version::new(2024, 1, 6),
1775        );
1776    }
1777
1778    #[test]
1779    fn list_multiple_versions_gdb() {
1780        test_list_multiple_versions(
1781            Component::Gdb,
1782            Version::new(2024, 1, 5),
1783            Version::new(2024, 1, 6),
1784        );
1785    }
1786
1787    #[test]
1788    fn list_multiple_versions_risc0_groth16() {
1789        test_list_multiple_versions(
1790            Component::Risc0Groth16,
1791            Version::new(1, 0, 0),
1792            Version::new(2, 0, 0),
1793        );
1794    }
1795
1796    fn set_default_version_test(
1797        rzup: &mut Rzup,
1798        tmp_dir: &TempDir,
1799        component: Component,
1800        version1: Version,
1801        version2: Version,
1802        expected_symlinks1: Vec<(String, String)>,
1803        expected_symlinks2: Vec<(String, String)>,
1804    ) {
1805        rzup.install_component(&component, Some(version1.clone()), false)
1806            .unwrap();
1807
1808        assert_eq!(
1809            rzup.get_default_version(&component).unwrap().unwrap().0,
1810            version1
1811        );
1812
1813        rzup.install_component(&component, Some(version2.clone()), false)
1814            .unwrap();
1815        assert_symlinks(tmp_dir.path(), expected_symlinks2.clone());
1816
1817        assert_eq!(
1818            rzup.get_default_version(&component).unwrap().unwrap().0,
1819            version2
1820        );
1821
1822        rzup.set_default_version(&component, version1.clone())
1823            .unwrap();
1824        assert_symlinks(tmp_dir.path(), expected_symlinks1.clone());
1825
1826        assert_eq!(
1827            rzup.get_default_version(&component).unwrap().unwrap().0,
1828            version1
1829        );
1830    }
1831
1832    #[test]
1833    fn set_default_version_cargo_risczero() {
1834        let server = MockDistributionServer::new();
1835        let (tmp_dir, mut rzup) = setup_test_env(
1836            server.base_urls.clone(),
1837            None,
1838            None,
1839            server.private_key.clone(),
1840            Platform::new("x86_64", Os::Linux),
1841        );
1842
1843        set_default_version_test(
1844            &mut rzup,
1845            &tmp_dir,
1846            Component::CargoRiscZero,
1847            Version::new(1, 0, 0),
1848            Version::new(1, 1, 0),
1849            vec![(
1850                ".cargo/bin/cargo-risczero".into(),
1851                ".risc0/extensions/v1.0.0-cargo-risczero-x86_64-unknown-linux-gnu/cargo-risczero"
1852                    .into(),
1853            )],
1854            vec![(
1855                ".cargo/bin/cargo-risczero".into(),
1856                ".risc0/extensions/v1.1.0-cargo-risczero-x86_64-unknown-linux-gnu/cargo-risczero"
1857                    .into(),
1858            )],
1859        );
1860    }
1861
1862    #[test]
1863    fn set_default_version_r0vm() {
1864        let server = MockDistributionServer::new();
1865        let (tmp_dir, mut rzup) = setup_test_env(
1866            server.base_urls.clone(),
1867            None,
1868            None,
1869            server.private_key.clone(),
1870            Platform::new("x86_64", Os::Linux),
1871        );
1872
1873        set_default_version_test(
1874            &mut rzup,
1875            &tmp_dir,
1876            Component::R0Vm,
1877            Version::new(1, 0, 0),
1878            Version::new(1, 1, 0),
1879            vec![
1880                (
1881                    ".cargo/bin/cargo-risczero".into(),
1882                    ".risc0/extensions/v1.0.0-cargo-risczero-x86_64-unknown-linux-gnu/cargo-risczero"
1883                        .into(),
1884                ),
1885                (
1886                    ".cargo/bin/r0vm".into(),
1887                    ".risc0/extensions/v1.0.0-cargo-risczero-x86_64-unknown-linux-gnu/r0vm".into(),
1888                )
1889            ],
1890            vec![
1891                (
1892                    ".cargo/bin/cargo-risczero".into(),
1893                    ".risc0/extensions/v1.0.0-cargo-risczero-x86_64-unknown-linux-gnu/cargo-risczero"
1894                        .into(),
1895                ),
1896                (
1897                    ".cargo/bin/r0vm".into(),
1898                    ".risc0/extensions/v1.1.0-cargo-risczero-x86_64-unknown-linux-gnu/r0vm".into(),
1899                )
1900            ],
1901        );
1902    }
1903
1904    #[test]
1905    fn set_default_version_r0vm_rc_version() {
1906        let server = MockDistributionServer::new();
1907        let (tmp_dir, mut rzup) = setup_test_env(
1908            server.base_urls.clone(),
1909            None,
1910            None,
1911            server.private_key.clone(),
1912            Platform::new("x86_64", Os::Linux),
1913        );
1914
1915        set_default_version_test(
1916            &mut rzup,
1917            &tmp_dir,
1918            Component::R0Vm,
1919            Version::parse("1.0.0-rc.1").unwrap(),
1920            Version::parse("1.0.0-rc.2").unwrap(),
1921            vec![
1922                (
1923                    ".cargo/bin/cargo-risczero".into(),
1924                    ".risc0/extensions/v1.0.0-rc.1-cargo-risczero-x86_64-unknown-linux-gnu/cargo-risczero"
1925                        .into(),
1926                ),
1927                (
1928                    ".cargo/bin/r0vm".into(),
1929                    ".risc0/extensions/v1.0.0-rc.1-cargo-risczero-x86_64-unknown-linux-gnu/r0vm"
1930                        .into(),
1931                )
1932            ],
1933            vec![
1934                (
1935                    ".cargo/bin/cargo-risczero".into(),
1936                    ".risc0/extensions/v1.0.0-rc.1-cargo-risczero-x86_64-unknown-linux-gnu/cargo-risczero"
1937                        .into(),
1938                ),
1939                (
1940                    ".cargo/bin/r0vm".into(),
1941                    ".risc0/extensions/v1.0.0-rc.2-cargo-risczero-x86_64-unknown-linux-gnu/r0vm"
1942                        .into(),
1943                )
1944            ],
1945        );
1946    }
1947
1948    #[test]
1949    fn set_default_version_r0vm_after_cargo_risczero_installed() {
1950        let server = MockDistributionServer::new();
1951        let (tmp_dir, mut rzup) = setup_test_env(
1952            server.base_urls.clone(),
1953            None,
1954            None,
1955            server.private_key.clone(),
1956            Platform::new("x86_64", Os::Linux),
1957        );
1958
1959        rzup.install_component(
1960            &Component::CargoRiscZero,
1961            Some(Version::new(1, 0, 0)),
1962            false, /* force */
1963        )
1964        .unwrap();
1965        rzup.install_component(
1966            &Component::CargoRiscZero,
1967            Some(Version::new(1, 1, 0)),
1968            false, /* force */
1969        )
1970        .unwrap();
1971
1972        set_default_version_test(
1973            &mut rzup,
1974            &tmp_dir,
1975            Component::R0Vm,
1976            Version::new(1, 0, 0),
1977            Version::new(1, 1, 0),
1978            vec![
1979                (
1980                    ".cargo/bin/cargo-risczero".into(),
1981                    ".risc0/extensions/v1.1.0-cargo-risczero-x86_64-unknown-linux-gnu/cargo-risczero"
1982                        .into(),
1983                ),
1984                (
1985                    ".cargo/bin/r0vm".into(),
1986                    ".risc0/extensions/v1.0.0-cargo-risczero-x86_64-unknown-linux-gnu/r0vm".into(),
1987                )
1988            ],
1989            vec![
1990                (
1991                    ".cargo/bin/cargo-risczero".into(),
1992                    ".risc0/extensions/v1.1.0-cargo-risczero-x86_64-unknown-linux-gnu/cargo-risczero"
1993                        .into(),
1994                ),
1995                (
1996                    ".cargo/bin/r0vm".into(),
1997                    ".risc0/extensions/v1.1.0-cargo-risczero-x86_64-unknown-linux-gnu/r0vm".into(),
1998                )
1999            ],
2000        );
2001    }
2002
2003    #[test]
2004    fn set_default_version_rust() {
2005        let server = MockDistributionServer::new();
2006        let (tmp_dir, mut rzup) = setup_test_env(
2007            server.base_urls.clone(),
2008            None,
2009            None,
2010            server.private_key.clone(),
2011            Platform::new("x86_64", Os::Linux),
2012        );
2013
2014        set_default_version_test(
2015            &mut rzup,
2016            &tmp_dir,
2017            Component::RustToolchain,
2018            Version::new(1, 79, 0),
2019            Version::new(1, 81, 0),
2020            vec![(
2021                ".rustup/toolchains/risc0".into(),
2022                ".risc0/toolchains/v1.79.0-rust-x86_64-unknown-linux-gnu".into(),
2023            )],
2024            vec![(
2025                ".rustup/toolchains/risc0".into(),
2026                ".risc0/toolchains/v1.81.0-rust-x86_64-unknown-linux-gnu".into(),
2027            )],
2028        );
2029    }
2030
2031    #[test]
2032    fn set_default_version_cpp() {
2033        let server = MockDistributionServer::new();
2034        let (tmp_dir, mut rzup) = setup_test_env(
2035            server.base_urls.clone(),
2036            None,
2037            None,
2038            server.private_key.clone(),
2039            Platform::new("x86_64", Os::Linux),
2040        );
2041
2042        set_default_version_test(
2043            &mut rzup,
2044            &tmp_dir,
2045            Component::CppToolchain,
2046            Version::new(2024, 1, 5),
2047            Version::new(2024, 1, 6),
2048            vec![(
2049                ".risc0/cpp".into(),
2050                ".risc0/toolchains/v2024.1.5-cpp-x86_64-unknown-linux-gnu/riscv32im-linux-x86_64"
2051                    .into(),
2052            )],
2053            vec![(
2054                ".risc0/cpp".into(),
2055                ".risc0/toolchains/v2024.1.6-cpp-x86_64-unknown-linux-gnu/riscv32im-linux-x86_64"
2056                    .into(),
2057            )],
2058        );
2059    }
2060
2061    #[test]
2062    fn set_default_version_gdb() {
2063        let server = MockDistributionServer::new();
2064        let (tmp_dir, mut rzup) = setup_test_env(
2065            server.base_urls.clone(),
2066            None,
2067            None,
2068            server.private_key.clone(),
2069            Platform::new("x86_64", Os::Linux),
2070        );
2071
2072        set_default_version_test(
2073            &mut rzup,
2074            &tmp_dir,
2075            Component::Gdb,
2076            Version::new(2024, 1, 5),
2077            Version::new(2024, 1, 6),
2078            vec![(
2079                ".risc0/bin/riscv32im-gdb".into(),
2080                ".risc0/extensions/v2024.1.5-gdb-x86_64-unknown-linux-gnu/riscv32im-gdb".into(),
2081            )],
2082            vec![(
2083                ".risc0/bin/riscv32im-gdb".into(),
2084                ".risc0/extensions/v2024.1.6-gdb-x86_64-unknown-linux-gnu/riscv32im-gdb".into(),
2085            )],
2086        );
2087    }
2088
2089    #[test]
2090    fn set_default_version_risc0_groth16() {
2091        let server = MockDistributionServer::new();
2092        let (tmp_dir, mut rzup) = setup_test_env(
2093            server.base_urls.clone(),
2094            None,
2095            None,
2096            server.private_key.clone(),
2097            Platform::new("x86_64", Os::Linux),
2098        );
2099
2100        set_default_version_test(
2101            &mut rzup,
2102            &tmp_dir,
2103            Component::Risc0Groth16,
2104            Version::new(1, 0, 0),
2105            Version::new(2, 0, 0),
2106            vec![],
2107            vec![],
2108        );
2109    }
2110
2111    fn default_version_after_uninstall(
2112        tmp_dir: &TempDir,
2113        rzup: &mut Rzup,
2114        component: Component,
2115        version1: Version,
2116        version2: Version,
2117        uninstall_with_rm: bool,
2118        expected_path: &Path,
2119    ) {
2120        rzup.install_component(&component, Some(version2.clone()), false)
2121            .unwrap();
2122
2123        rzup.install_component(&component, Some(version1.clone()), false)
2124            .unwrap();
2125
2126        if uninstall_with_rm {
2127            let mut version_dir = rzup.get_version_dir(&component, &version1).unwrap();
2128            // Remove C++ sub-dir component
2129            if component == Component::CppToolchain {
2130                version_dir.pop();
2131            }
2132            std::fs::remove_dir_all(version_dir).unwrap()
2133        } else {
2134            rzup.uninstall_component(&component, version1.clone())
2135                .unwrap();
2136
2137            // ensure we updated the settings.toml
2138            let settings: settings::Settings = toml::from_str(
2139                &std::fs::read_to_string(tmp_dir.path().join(".risc0/settings.toml")).unwrap(),
2140            )
2141            .unwrap();
2142            let mut expected = settings::Settings::default();
2143            expected.set_default_version(&component, &version2);
2144            if let Some(parent) = component.parent_component() {
2145                expected.set_default_version(&parent, &version2);
2146            }
2147            assert_eq!(settings, expected);
2148        }
2149
2150        let (default_version, path) = rzup.get_default_version(&component).unwrap().unwrap();
2151        assert_eq!(default_version, version2);
2152        assert_eq!(path, expected_path);
2153    }
2154
2155    #[test]
2156    fn default_version_after_uninstall_cargo_risczero() {
2157        let server = MockDistributionServer::new();
2158        let (tmp_dir, mut rzup) = setup_test_env(
2159            server.base_urls.clone(),
2160            None,
2161            None,
2162            server.private_key.clone(),
2163            Platform::new("x86_64", Os::Linux),
2164        );
2165
2166        for uninstall_with_rm in [true, false] {
2167            default_version_after_uninstall(
2168                &tmp_dir,
2169                &mut rzup,
2170                Component::CargoRiscZero,
2171                Version::new(1, 0, 0),
2172                Version::new(1, 1, 0),
2173                uninstall_with_rm,
2174                &tmp_dir
2175                    .path()
2176                    .join(".risc0/extensions/v1.1.0-cargo-risczero-x86_64-unknown-linux-gnu"),
2177            );
2178        }
2179    }
2180
2181    #[test]
2182    fn default_version_after_uninstall_r0vm() {
2183        let server = MockDistributionServer::new();
2184        let (tmp_dir, mut rzup) = setup_test_env(
2185            server.base_urls.clone(),
2186            None,
2187            None,
2188            server.private_key.clone(),
2189            Platform::new("x86_64", Os::Linux),
2190        );
2191
2192        for uninstall_with_rm in [true, false] {
2193            default_version_after_uninstall(
2194                &tmp_dir,
2195                &mut rzup,
2196                Component::R0Vm,
2197                Version::new(1, 0, 0),
2198                Version::new(1, 1, 0),
2199                uninstall_with_rm,
2200                &tmp_dir
2201                    .path()
2202                    .join(".risc0/extensions/v1.1.0-cargo-risczero-x86_64-unknown-linux-gnu"),
2203            );
2204        }
2205    }
2206
2207    #[test]
2208    fn default_version_after_uninstall_rust() {
2209        let server = MockDistributionServer::new();
2210        let (tmp_dir, mut rzup) = setup_test_env(
2211            server.base_urls.clone(),
2212            None,
2213            None,
2214            server.private_key.clone(),
2215            Platform::new("x86_64", Os::Linux),
2216        );
2217
2218        for uninstall_with_rm in [true, false] {
2219            default_version_after_uninstall(
2220                &tmp_dir,
2221                &mut rzup,
2222                Component::RustToolchain,
2223                Version::new(1, 79, 0),
2224                Version::new(1, 81, 0),
2225                uninstall_with_rm,
2226                &tmp_dir
2227                    .path()
2228                    .join(".risc0/toolchains/v1.81.0-rust-x86_64-unknown-linux-gnu"),
2229            );
2230        }
2231    }
2232
2233    #[test]
2234    fn default_version_after_uninstall_cpp() {
2235        let server = MockDistributionServer::new();
2236        let (tmp_dir, mut rzup) = setup_test_env(
2237            server.base_urls.clone(),
2238            None,
2239            None,
2240            server.private_key.clone(),
2241            Platform::new("x86_64", Os::Linux),
2242        );
2243
2244        for uninstall_with_rm in [true, false] {
2245            default_version_after_uninstall(
2246                &tmp_dir,
2247                &mut rzup,
2248                Component::CppToolchain,
2249                Version::new(2024, 1, 5),
2250                Version::new(2024, 1, 6),
2251                uninstall_with_rm,
2252                &tmp_dir.path().join(
2253                    ".risc0/toolchains/v2024.1.6-cpp-x86_64-unknown-linux-gnu/riscv32im-linux-x86_64",
2254                ),
2255            );
2256        }
2257    }
2258
2259    #[test]
2260    fn default_version_after_uninstall_gdb() {
2261        let server = MockDistributionServer::new();
2262        let (tmp_dir, mut rzup) = setup_test_env(
2263            server.base_urls.clone(),
2264            None,
2265            None,
2266            server.private_key.clone(),
2267            Platform::new("x86_64", Os::Linux),
2268        );
2269
2270        for uninstall_with_rm in [true, false] {
2271            default_version_after_uninstall(
2272                &tmp_dir,
2273                &mut rzup,
2274                Component::Gdb,
2275                Version::new(2024, 1, 5),
2276                Version::new(2024, 1, 6),
2277                uninstall_with_rm,
2278                &tmp_dir
2279                    .path()
2280                    .join(".risc0/extensions/v2024.1.6-gdb-x86_64-unknown-linux-gnu"),
2281            );
2282        }
2283    }
2284
2285    #[test]
2286    fn default_version_after_uninstall_risc0_groth16() {
2287        let server = MockDistributionServer::new();
2288        let (tmp_dir, mut rzup) = setup_test_env(
2289            server.base_urls.clone(),
2290            None,
2291            None,
2292            server.private_key.clone(),
2293            Platform::new("x86_64", Os::Linux),
2294        );
2295
2296        for uninstall_with_rm in [true, false] {
2297            default_version_after_uninstall(
2298                &tmp_dir,
2299                &mut rzup,
2300                Component::Risc0Groth16,
2301                Version::new(1, 0, 0),
2302                Version::new(2, 0, 0),
2303                uninstall_with_rm,
2304                &tmp_dir
2305                    .path()
2306                    .join(".risc0/extensions/v2.0.0-risc0-groth16"),
2307            );
2308        }
2309    }
2310
2311    #[test]
2312    fn install_non_existent() {
2313        let server = MockDistributionServer::new();
2314        let (_tmp_dir, mut rzup) = setup_test_env(
2315            server.base_urls.clone(),
2316            None,
2317            None,
2318            server.private_key.clone(),
2319            Platform::new("x86_64", Os::Linux),
2320        );
2321        let cargo_risczero_version = Version::new(5, 0, 0);
2322
2323        run_and_assert_events(
2324            &mut rzup,
2325            |rzup| {
2326                let error = rzup
2327                    .install_component(
2328                        &Component::CargoRiscZero,
2329                        Some(cargo_risczero_version.clone()),
2330                        false,
2331                    )
2332                    .unwrap_err();
2333                assert_eq!(
2334                    error,
2335                    RzupError::InvalidVersion("5.0.0 is not available for cargo-risczero".into())
2336                );
2337            },
2338            vec![
2339                RzupEvent::InstallationStarted {
2340                    id: "cargo-risczero".into(),
2341                    version: "5.0.0".into(),
2342                },
2343                RzupEvent::InstallationFailed {
2344                    id: "cargo-risczero".into(),
2345                    version: "5.0.0".into(),
2346                },
2347            ],
2348        );
2349
2350        assert_eq!(
2351            rzup.list_versions(&Component::CargoRiscZero).unwrap(),
2352            vec![]
2353        );
2354    }
2355
2356    #[test]
2357    fn install_bad_shasum() {
2358        let server = MockDistributionServer::new();
2359        let (_tmp_dir, mut rzup) = setup_test_env(
2360            server.base_urls.clone(),
2361            None,
2362            None,
2363            server.private_key.clone(),
2364            Platform::new("x86_64", Os::Linux),
2365        );
2366
2367        let base_url = &server.base_urls.s3_base_url;
2368        run_and_assert_events(
2369            &mut rzup,
2370            |rzup| {
2371                let error = rzup
2372                    .install_component(
2373                        &Component::Risc0Groth16,
2374                        Some("3.0.0-badsha".parse().unwrap()),
2375                        false,
2376                    )
2377                    .unwrap_err();
2378                assert_eq!(
2379                    error,
2380                    RzupError::Sha256Mismatch {
2381                        expected: HELLO_WORLD3_DUMMY_TAR_XZ_SHA256.into(),
2382                        actual: HELLO_WORLD2_DUMMY_TAR_XZ_SHA256.into()
2383                    }
2384                );
2385            },
2386            vec![
2387                RzupEvent::InstallationStarted {
2388                    id: "risc0-groth16".into(),
2389                    version: "3.0.0-badsha".into(),
2390                },
2391                RzupEvent::TransferStarted {
2392                    kind: TransferKind::Download,
2393                    id: "risc0-groth16".into(),
2394                    version: Some("3.0.0-badsha".into()),
2395                    url: Some(format!(
2396                        "{base_url}/rzup/components/risc0-groth16/sha256/{HELLO_WORLD3_DUMMY_TAR_XZ_SHA256}"
2397                    )),
2398                    len: Some(hyper_len(dummy_tar_xz_response("hello-world"))),
2399                },
2400                RzupEvent::TransferProgress {
2401                    id: "risc0-groth16".into(),
2402                    incr: hyper_len(dummy_tar_xz_response("hello-world")),
2403                },
2404                RzupEvent::InstallationFailed {
2405                    id: "risc0-groth16".into(),
2406                    version: "3.0.0-badsha".into(),
2407                },
2408            ],
2409        );
2410
2411        assert_eq!(
2412            rzup.list_versions(&Component::Risc0Groth16).unwrap(),
2413            vec![]
2414        );
2415    }
2416
2417    #[test]
2418    fn install_bad_signature() {
2419        let server = MockDistributionServer::new();
2420        let (_tmp_dir, mut rzup) = setup_test_env(
2421            server.base_urls.clone(),
2422            None,
2423            None,
2424            test_private_key(), // fresh private key
2425            Platform::new("x86_64", Os::Linux),
2426        );
2427
2428        run_and_assert_events(
2429            &mut rzup,
2430            |rzup| {
2431                let error = rzup
2432                    .install_component(
2433                        &Component::Risc0Groth16,
2434                        Some("2.0.0".parse().unwrap()),
2435                        false,
2436                    )
2437                    .unwrap_err();
2438                assert_eq!(
2439                    error,
2440                    RzupError::InvalidSignature("signature error: verification error".into())
2441                );
2442            },
2443            vec![RzupEvent::InstallationStarted {
2444                id: "risc0-groth16".into(),
2445                version: "2.0.0".into(),
2446            }],
2447        );
2448
2449        assert_eq!(
2450            rzup.list_versions(&Component::Risc0Groth16).unwrap(),
2451            vec![]
2452        );
2453    }
2454
2455    fn modify_published_distribution_manifest_json(
2456        s3_base_url: &str,
2457        modify: impl FnOnce(&mut serde_json::Value),
2458    ) {
2459        let manifest_url =
2460            format!("{s3_base_url}/rzup/components/risc0-groth16/distribution_manifest.json",);
2461        let client = reqwest::blocking::Client::new();
2462        let mut manifest: serde_json::Value =
2463            client.get(&manifest_url).send().unwrap().json().unwrap();
2464        modify(&mut manifest);
2465        client
2466            .put(manifest_url)
2467            .header("x-amz-date", "foo")
2468            .header("authorization", "foo")
2469            .header("x-amz-content-sha256", "foo")
2470            .header("content-type", "application/octet-stream")
2471            .body(serde_json::to_vec(&manifest).unwrap())
2472            .send()
2473            .unwrap();
2474    }
2475
2476    #[test]
2477    fn install_missing_signature() {
2478        let server = MockDistributionServer::new();
2479        let (_tmp_dir, mut rzup) = setup_test_env(
2480            server.base_urls.clone(),
2481            None,
2482            None,
2483            server.private_key.clone(),
2484            Platform::new("x86_64", Os::Linux),
2485        );
2486
2487        // Remove the signature from the manifest.
2488        modify_published_distribution_manifest_json(&server.base_urls.s3_base_url, |manifest| {
2489            manifest.as_object_mut().unwrap().remove("signature");
2490        });
2491
2492        run_and_assert_events(
2493            &mut rzup,
2494            |rzup| {
2495                let error = rzup
2496                    .install_component(
2497                        &Component::Risc0Groth16,
2498                        Some("2.0.0".parse().unwrap()),
2499                        false,
2500                    )
2501                    .unwrap_err();
2502                assert_eq!(
2503                    error,
2504                    RzupError::Other("distribution_manifest.json missing signature".into())
2505                );
2506            },
2507            vec![RzupEvent::InstallationStarted {
2508                id: "risc0-groth16".into(),
2509                version: "2.0.0".into(),
2510            }],
2511        );
2512
2513        assert_eq!(
2514            rzup.list_versions(&Component::Risc0Groth16).unwrap(),
2515            vec![]
2516        );
2517    }
2518
2519    fn uninstall_test(component: Component, version: Version) {
2520        let server = MockDistributionServer::new();
2521        let (tmp_dir, mut rzup) = setup_test_env(
2522            server.base_urls.clone(),
2523            None,
2524            None,
2525            server.private_key.clone(),
2526            Platform::new("x86_64", Os::Linux),
2527        );
2528
2529        rzup.install_component(&component, Some(version.clone()), false)
2530            .unwrap();
2531
2532        run_and_assert_events(
2533            &mut rzup,
2534            |rzup| {
2535                rzup.uninstall_component(&component, version.clone())
2536                    .unwrap();
2537            },
2538            vec![RzupEvent::Uninstalled {
2539                id: component.to_string(),
2540                version: version.to_string(),
2541            }],
2542        );
2543
2544        assert_files(tmp_dir.path(), vec![]);
2545    }
2546
2547    #[test]
2548    fn uninstall_cargo_risczero() {
2549        uninstall_test(Component::CargoRiscZero, Version::new(1, 0, 0));
2550    }
2551
2552    #[test]
2553    fn uninstall_r0vm() {
2554        uninstall_test(Component::R0Vm, Version::new(1, 0, 0));
2555    }
2556
2557    #[test]
2558    fn uninstall_rust() {
2559        uninstall_test(Component::RustToolchain, Version::new(1, 81, 0));
2560    }
2561
2562    #[test]
2563    fn uninstall_cpp() {
2564        uninstall_test(Component::CppToolchain, Version::new(2024, 1, 5));
2565    }
2566
2567    #[test]
2568    fn uninstall_gdb() {
2569        uninstall_test(Component::Gdb, Version::new(2024, 1, 5));
2570    }
2571
2572    #[test]
2573    fn uninstall_risc0_groth16() {
2574        uninstall_test(Component::Risc0Groth16, Version::new(1, 0, 0));
2575    }
2576
2577    #[test]
2578    fn get_latest_version_cargo_risczero() {
2579        let server = MockDistributionServer::new();
2580        let (_tmp_dir, rzup) = setup_test_env(
2581            server.base_urls.clone(),
2582            None,
2583            None,
2584            server.private_key.clone(),
2585            Platform::new("x86_64", Os::Linux),
2586        );
2587
2588        assert_eq!(
2589            rzup.get_latest_version(&Component::CargoRiscZero).unwrap(),
2590            Version::new(1, 1, 0)
2591        );
2592    }
2593
2594    #[test]
2595    fn get_latest_version_risc0_groth16() {
2596        let server = MockDistributionServer::new();
2597        let (_tmp_dir, rzup) = setup_test_env(
2598            server.base_urls.clone(),
2599            None,
2600            None,
2601            server.private_key.clone(),
2602            Platform::new("x86_64", Os::Linux),
2603        );
2604
2605        assert_eq!(
2606            rzup.get_latest_version(&Component::Risc0Groth16).unwrap(),
2607            Version::new(2, 0, 0)
2608        );
2609    }
2610
2611    struct LegacyVersionsFixture {
2612        rzup: Rzup,
2613        tmp_dir: TempDir,
2614        legacy_rust_dir: PathBuf,
2615        legacy_cargo_risczero_dir: PathBuf,
2616        legacy_cpp_dir: PathBuf,
2617    }
2618
2619    impl LegacyVersionsFixture {
2620        fn new(rust_dir_name: &str, cargo_risczero_dir_name: &str, cpp_dir_name: &str) -> Self {
2621            let (tmp_dir, rzup) = setup_test_env(
2622                invalid_base_urls(),
2623                None,
2624                None,
2625                test_private_key(),
2626                Platform::new("x86_64", Os::Linux),
2627            );
2628
2629            let legacy_rust_dir = tmp_dir.path().join(".risc0/toolchains").join(rust_dir_name);
2630            std::fs::create_dir_all(&legacy_rust_dir).unwrap();
2631
2632            let legacy_cargo_risczero_dir = tmp_dir
2633                .path()
2634                .join(".risc0/extensions")
2635                .join(cargo_risczero_dir_name);
2636            std::fs::create_dir_all(&legacy_cargo_risczero_dir).unwrap();
2637
2638            let legacy_cpp_dir = tmp_dir.path().join(".risc0/toolchains").join(cpp_dir_name);
2639            std::fs::create_dir_all(&legacy_cpp_dir).unwrap();
2640
2641            Self {
2642                rzup,
2643                tmp_dir,
2644                legacy_rust_dir,
2645                legacy_cargo_risczero_dir,
2646                legacy_cpp_dir,
2647            }
2648        }
2649    }
2650
2651    fn get_legacy_versions(rust_dir_name: &str, cargo_risczero_dir_name: &str, cpp_dir_name: &str) {
2652        let fix = LegacyVersionsFixture::new(rust_dir_name, cargo_risczero_dir_name, cpp_dir_name);
2653
2654        assert_eq!(
2655            fix.rzup
2656                .get_version_dir(&Component::RustToolchain, &Version::new(1, 81, 0))
2657                .unwrap(),
2658            fix.legacy_rust_dir
2659        );
2660        assert_eq!(
2661            fix.rzup
2662                .get_version_dir(
2663                    &Component::CargoRiscZero,
2664                    &Version::parse("1.2.1-rc.0").unwrap()
2665                )
2666                .unwrap(),
2667            fix.legacy_cargo_risczero_dir
2668        );
2669        assert_eq!(
2670            fix.rzup
2671                .get_version_dir(&Component::CppToolchain, &Version::new(2024, 1, 5))
2672                .unwrap(),
2673            fix.legacy_cpp_dir
2674        );
2675    }
2676
2677    #[test]
2678    fn get_legacy_versions_old_rzup_apple_aarch64() {
2679        get_legacy_versions(
2680            "r0.1.81.0-risc0-rust-aarch64-apple-darwin",
2681            "v1.2.1-rc.0-cargo-risczero",
2682            "2024.01.05-risc0-cpp-aarch64-apple-darwin",
2683        );
2684    }
2685
2686    #[test]
2687    fn get_legacy_versions_old_rzup_linux_x86() {
2688        get_legacy_versions(
2689            "r0.1.81.0-risc0-rust-x86_64-unknown-linux-gnu",
2690            "v1.2.1-rc.0-cargo-risczero",
2691            "2024.01.05-risc0-cpp-x86_64-unknown-linux-gnu",
2692        );
2693    }
2694
2695    #[test]
2696    fn get_legacy_versions_cargo_risczero_install_apple_aarch64() {
2697        get_legacy_versions(
2698            "rust_aarch64-apple-darwin_r0.1.81.0",
2699            "v1.2.1-rc.0-cargo-risczero",
2700            "c_aarch64-apple-darwin_2024.01.05",
2701        );
2702    }
2703
2704    #[test]
2705    fn get_legacy_versions_cargo_risczero_install_linux_x86() {
2706        get_legacy_versions(
2707            "rust_x86_64-unknown-linux-gnu_r0.1.81.0",
2708            "v1.2.1-rc.0-cargo-risczero",
2709            "c_x86_64-unknown-linux-gnu_2024.01.05",
2710        );
2711    }
2712
2713    fn get_default_legacy_versions(
2714        rust_dir_name: &str,
2715        cargo_risczero_dir_name: &str,
2716        cpp_dir_name: &str,
2717    ) {
2718        let fix = LegacyVersionsFixture::new(rust_dir_name, cargo_risczero_dir_name, cpp_dir_name);
2719
2720        assert_eq!(
2721            fix.rzup
2722                .get_default_version(&Component::RustToolchain)
2723                .unwrap()
2724                .unwrap()
2725                .0,
2726            Version::new(1, 81, 0)
2727        );
2728
2729        assert_eq!(
2730            fix.rzup
2731                .get_default_version(&Component::CargoRiscZero)
2732                .unwrap()
2733                .unwrap()
2734                .0,
2735            Version::parse("1.2.1-rc.0").unwrap()
2736        );
2737
2738        assert_eq!(
2739            fix.rzup
2740                .get_default_version(&Component::CppToolchain)
2741                .unwrap()
2742                .unwrap()
2743                .0,
2744            Version::new(2024, 1, 5)
2745        );
2746    }
2747
2748    #[test]
2749    fn get_default_legacy_versions_old_rzup_apple_aarch64() {
2750        get_default_legacy_versions(
2751            "r0.1.81.0-risc0-rust-aarch64-apple-darwin",
2752            "v1.2.1-rc.0-cargo-risczero",
2753            "2024.01.05-risc0-cpp-aarch64-apple-darwin",
2754        );
2755    }
2756
2757    #[test]
2758    fn get_default_legacy_versions_old_rzup_linux_x86() {
2759        get_default_legacy_versions(
2760            "r0.1.81.0-risc0-rust-x86_64-unknown-linux-gnu",
2761            "v1.2.1-rc.0-cargo-risczero",
2762            "2024.01.05-risc0-cpp-x86_64-unknown-linux-gnu",
2763        );
2764    }
2765
2766    #[test]
2767    fn get_default_legacy_versions_cargo_risczero_install_apple_aarch64() {
2768        get_default_legacy_versions(
2769            "rust_aarch64-apple-darwin_r0.1.81.0",
2770            "v1.2.1-rc.0-cargo-risczero",
2771            "c_aarch64-apple-darwin_2024.01.05",
2772        );
2773    }
2774
2775    #[test]
2776    fn get_default_legacy_versions_cargo_risczero_install_linux_x86() {
2777        get_default_legacy_versions(
2778            "rust_x86_64-unknown-linux-gnu_r0.1.81.0",
2779            "v1.2.1-rc.0-cargo-risczero",
2780            "c_x86_64-unknown-linux-gnu_2024.01.05",
2781        );
2782    }
2783
2784    fn list_legacy_versions(dir1: &str, dir2: &str, expected_versions: Vec<Version>) {
2785        let (tmp_dir, rzup) = setup_test_env(
2786            invalid_base_urls(),
2787            None,
2788            None,
2789            test_private_key(),
2790            Platform::new("x86_64", Os::Linux),
2791        );
2792
2793        let legacy_rust_dir1 = tmp_dir.path().join(".risc0/toolchains").join(dir1);
2794        std::fs::create_dir_all(&legacy_rust_dir1).unwrap();
2795
2796        let legacy_rust_dir2 = tmp_dir.path().join(".risc0/toolchains").join(dir2);
2797        std::fs::create_dir_all(&legacy_rust_dir2).unwrap();
2798
2799        assert_eq!(
2800            rzup.list_versions(&Component::RustToolchain).unwrap(),
2801            expected_versions
2802        );
2803    }
2804
2805    #[test]
2806    fn list_legacy_versions_old_rzup_apple_aarch64() {
2807        list_legacy_versions(
2808            "r0.1.79.0-risc0-rust-aarch64-apple-darwin",
2809            "r0.1.81.0-risc0-rust-aarch64-apple-darwin",
2810            vec![Version::new(1, 81, 0), Version::new(1, 79, 0)],
2811        );
2812    }
2813
2814    #[test]
2815    fn list_legacy_versions_old_rzup_linux_x86() {
2816        list_legacy_versions(
2817            "r0.1.79.0-risc0-rust-x86_64-unknown-linux-gnu",
2818            "r0.1.81.0-risc0-rust-x86_64-unknown-linux-gnu",
2819            vec![Version::new(1, 81, 0), Version::new(1, 79, 0)],
2820        );
2821    }
2822
2823    #[test]
2824    fn list_legacy_versions_cargo_risczero_install_aaple_aarch64() {
2825        list_legacy_versions(
2826            "rust_aarch64-apple-darwin_r0.1.79.0",
2827            "rust_aarch64-apple-darwin_r0.1.81.0",
2828            vec![Version::new(1, 81, 0), Version::new(1, 79, 0)],
2829        );
2830    }
2831
2832    #[test]
2833    fn list_legacy_versions_cargo_risczero_install_linux_x86() {
2834        list_legacy_versions(
2835            "rust_x86_64-unknown-linux-gnu_r0.1.79.0",
2836            "rust_x86_64-unknown-linux-gnu_r0.1.81.0",
2837            vec![Version::new(1, 81, 0), Version::new(1, 79, 0)],
2838        );
2839    }
2840
2841    fn set_default_version_legacy_versions(
2842        rust_dir_name: &str,
2843        cargo_risczero_dir_name: &str,
2844        cpp_dir_name: &str,
2845    ) {
2846        let mut fix =
2847            LegacyVersionsFixture::new(rust_dir_name, cargo_risczero_dir_name, cpp_dir_name);
2848        fix.rzup
2849            .set_default_version(&Component::RustToolchain, Version::new(1, 81, 0))
2850            .unwrap();
2851
2852        fix.rzup
2853            .set_default_version(
2854                &Component::CargoRiscZero,
2855                Version::parse("1.2.1-rc.0").unwrap(),
2856            )
2857            .unwrap();
2858
2859        fix.rzup
2860            .set_default_version(&Component::CppToolchain, Version::new(2024, 1, 5))
2861            .unwrap();
2862
2863        assert_symlinks(
2864            fix.tmp_dir.path(),
2865            vec![
2866                (
2867                    ".cargo/bin/cargo-risczero".into(),
2868                    fix.legacy_cargo_risczero_dir
2869                        .strip_prefix(fix.tmp_dir.path())
2870                        .unwrap()
2871                        .join("cargo-risczero")
2872                        .to_str()
2873                        .unwrap()
2874                        .into(),
2875                ),
2876                (
2877                    ".rustup/toolchains/risc0".into(),
2878                    fix.legacy_rust_dir
2879                        .strip_prefix(fix.tmp_dir.path())
2880                        .unwrap()
2881                        .to_str()
2882                        .unwrap()
2883                        .into(),
2884                ),
2885                (
2886                    ".risc0/cpp".into(),
2887                    fix.legacy_cpp_dir
2888                        .strip_prefix(fix.tmp_dir.path())
2889                        .unwrap()
2890                        .to_str()
2891                        .unwrap()
2892                        .into(),
2893                ),
2894            ],
2895        );
2896    }
2897
2898    #[test]
2899    fn set_default_version_legacy_versions_old_rzup_apple_aarch64() {
2900        set_default_version_legacy_versions(
2901            "r0.1.81.0-risc0-rust-aarch64-apple-darwin",
2902            "v1.2.1-rc.0-cargo-risczero",
2903            "2024.01.05-risc0-cpp-aarch64-apple-darwin",
2904        );
2905    }
2906
2907    #[test]
2908    fn set_default_version_legacy_versions_old_rzup_linux_x86() {
2909        set_default_version_legacy_versions(
2910            "r0.1.81.0-risc0-rust-x86_64-unknown-linux-gnu",
2911            "v1.2.1-rc.0-cargo-risczero",
2912            "2024.01.05-risc0-cpp-x86_64-unknown-linux-gnu",
2913        );
2914    }
2915
2916    #[test]
2917    fn set_default_version_legacy_versions_cargo_risczero_install_apple_aarch64() {
2918        set_default_version_legacy_versions(
2919            "rust_aarch64-apple-darwin_r0.1.81.0",
2920            "v1.2.1-rc.0-cargo-risczero",
2921            "c_aarch64-apple-darwin_2024.01.05",
2922        );
2923    }
2924
2925    #[test]
2926    fn set_default_version_legacy_versions_cargo_risczero_install_linux_x86() {
2927        set_default_version_legacy_versions(
2928            "rust_x86_64-unknown-linux-gnu_r0.1.81.0",
2929            "v1.2.1-rc.0-cargo-risczero",
2930            "c_x86_64-unknown-linux-gnu_2024.01.05",
2931        );
2932    }
2933
2934    #[test]
2935    fn set_default_version_legacy_version_has_dir_instead_of_symlink() {
2936        let (tmp_dir, mut rzup) = setup_test_env(
2937            invalid_base_urls(),
2938            None,
2939            None,
2940            test_private_key(),
2941            Platform::new("x86_64", Os::Linux),
2942        );
2943
2944        let legacy_cpp_dir =
2945            PathBuf::from(".risc0/toolchains/2024.01.05-risc0-cpp-x86_64-unknown-linux-gnu");
2946        std::fs::create_dir_all(tmp_dir.path().join(&legacy_cpp_dir)).unwrap();
2947
2948        // I'm not sure why some machine would have a directory here instead of a symlink, but
2949        // there was an automation machine in this state.
2950        std::fs::create_dir(tmp_dir.path().join(".risc0/cpp")).unwrap();
2951
2952        rzup.set_default_version(&Component::CppToolchain, Version::new(2024, 1, 5))
2953            .unwrap();
2954
2955        assert_symlinks(
2956            tmp_dir.path(),
2957            vec![(".risc0/cpp".into(), legacy_cpp_dir.to_str().unwrap().into())],
2958        );
2959    }
2960
2961    fn uninstall_legacy_versions(
2962        rust_dir_name: &str,
2963        cargo_risczero_dir_name: &str,
2964        cpp_dir_name: &str,
2965    ) {
2966        let mut fix =
2967            LegacyVersionsFixture::new(rust_dir_name, cargo_risczero_dir_name, cpp_dir_name);
2968
2969        fix.rzup
2970            .set_default_version(&Component::RustToolchain, Version::new(1, 81, 0))
2971            .unwrap();
2972
2973        fix.rzup
2974            .uninstall_component(&Component::RustToolchain, Version::new(1, 81, 0))
2975            .unwrap();
2976
2977        fix.rzup
2978            .set_default_version(
2979                &Component::CargoRiscZero,
2980                Version::parse("1.2.1-rc.0").unwrap(),
2981            )
2982            .unwrap();
2983
2984        fix.rzup
2985            .uninstall_component(
2986                &Component::CargoRiscZero,
2987                Version::parse("1.2.1-rc.0").unwrap(),
2988            )
2989            .unwrap();
2990
2991        fix.rzup
2992            .set_default_version(&Component::CppToolchain, Version::new(2024, 1, 5))
2993            .unwrap();
2994
2995        fix.rzup
2996            .uninstall_component(&Component::CppToolchain, Version::new(2024, 1, 5))
2997            .unwrap();
2998
2999        assert_files(fix.tmp_dir.path(), vec![]);
3000    }
3001
3002    #[test]
3003    fn uninstall_legacy_versions_old_rzup_apple_aarch64() {
3004        uninstall_legacy_versions(
3005            "r0.1.81.0-risc0-rust-aarch64-apple-darwin",
3006            "v1.2.1-rc.0-cargo-risczero",
3007            "2024.01.05-risc0-cpp-aarch64-apple-darwin",
3008        )
3009    }
3010
3011    #[test]
3012    fn uninstall_legacy_versions_old_rzup_linux_x86() {
3013        uninstall_legacy_versions(
3014            "r0.1.81.0-risc0-rust-x86_64-unknown-linux-gnu",
3015            "v1.2.1-rc.0-cargo-risczero",
3016            "2024.01.05-risc0-cpp-x86_64-unknown-linux-gnu",
3017        )
3018    }
3019
3020    #[test]
3021    fn uninstall_legacy_versions_cargo_risczero_install_apple_aarch64() {
3022        uninstall_legacy_versions(
3023            "rust_aarch64-apple-darwin_r0.1.81.0",
3024            "v1.2.1-rc.0-cargo-risczero",
3025            "c_aarch64-apple-darwin_2024.01.05",
3026        )
3027    }
3028
3029    #[test]
3030    fn uninstall_legacy_versions_cargo_risczero_install_linux_x86() {
3031        uninstall_legacy_versions(
3032            "rust_x86_64-unknown-linux-gnu_r0.1.81.0",
3033            "v1.2.1-rc.0-cargo-risczero",
3034            "c_x86_64-unknown-linux-gnu_2024.01.05",
3035        )
3036    }
3037
3038    #[test]
3039    fn self_update() {
3040        let temp_dir = TempDir::new().unwrap();
3041        let server = MockDistributionServer::new_with_install_script(format!(
3042            "#!/bin/bash
3043            set -eo pipefail
3044            touch {}/self_update_ran
3045            ",
3046            temp_dir.path().display()
3047        ));
3048        let (_, mut rzup) = setup_test_env(
3049            server.base_urls.clone(),
3050            None,
3051            None,
3052            server.private_key.clone(),
3053            Platform::new("x86_64", Os::Linux),
3054        );
3055
3056        run_and_assert_events(
3057            &mut rzup,
3058            |rzup| {
3059                rzup.self_update().unwrap();
3060            },
3061            vec![
3062                RzupEvent::InstallationStarted {
3063                    id: "rzup".into(),
3064                    version: "latest".into(),
3065                },
3066                RzupEvent::InstallationCompleted {
3067                    id: "rzup".into(),
3068                    version: "latest".into(),
3069                },
3070            ],
3071        );
3072        assert!(temp_dir.path().join("self_update_ran").exists());
3073    }
3074
3075    #[test]
3076    fn self_update_failure() {
3077        let temp_dir = TempDir::new().unwrap();
3078        let server = MockDistributionServer::new_with_install_script(
3079            "#!/bin/bash
3080            set -eo pipefail
3081            echo test_failure 1>&2
3082            exit 1
3083            "
3084            .into(),
3085        );
3086        let (_, mut rzup) = setup_test_env(
3087            server.base_urls.clone(),
3088            None,
3089            None,
3090            server.private_key.clone(),
3091            Platform::new("x86_64", Os::Linux),
3092        );
3093
3094        run_and_assert_events(
3095            &mut rzup,
3096            |rzup| {
3097                let error = rzup.self_update().unwrap_err();
3098                assert_eq!(
3099                    error,
3100                    RzupError::Other("Self-update failed: test_failure\n".into())
3101                );
3102            },
3103            vec![
3104                RzupEvent::InstallationStarted {
3105                    id: "rzup".into(),
3106                    version: "latest".into(),
3107                },
3108                RzupEvent::InstallationFailed {
3109                    id: "rzup".into(),
3110                    version: "latest".into(),
3111                },
3112            ],
3113        );
3114        assert!(!temp_dir.path().join("self_update_ran").exists());
3115    }
3116
3117    fn write_script(path: &Path, contents: &str) {
3118        use std::os::unix::fs::PermissionsExt as _;
3119
3120        std::fs::write(path, contents).unwrap();
3121        let mut perms = std::fs::metadata(path).unwrap().permissions();
3122        perms.set_mode(0o775);
3123        std::fs::set_permissions(path, perms).unwrap();
3124    }
3125
3126    fn create_fake_rust_repo(tmp_dir: &TempDir, rust_version: Version) -> (PathBuf, Version) {
3127        let test_repo = tmp_dir.path().join("test-repo");
3128        std::fs::create_dir(&test_repo).unwrap();
3129        build::run_command(
3130            "git",
3131            &["-c", "init.defaultBranch=master", "init"],
3132            Some(&test_repo),
3133            &[
3134                ("GIT_CONFIG_SYSTEM", "/dev/null"),
3135                ("GIT_CONFIG_GLOBAL", "/dev/null"),
3136            ],
3137        )
3138        .unwrap();
3139
3140        std::fs::create_dir(test_repo.join("src")).unwrap();
3141        std::fs::write(test_repo.join("src/version"), rust_version.to_string()).unwrap();
3142        write_script(
3143            &test_repo.join("x"),
3144            "\
3145            #!/bin/bash
3146            mkdir -p build/foo/stage2/bin
3147            mkdir -p build/foo/stage2-tools-bin
3148            mkdir -p build/foo/stage3/lib/rustlib/riscv32im-risc0-zkvm-elf
3149            touch build/foo/stage2/bin/rustc
3150            touch build/foo/stage2-tools-bin/cargo-fmt
3151            echo 'build output line 1'
3152            echo 'build output line 2'
3153            ",
3154        );
3155
3156        build::run_command("git", &["add", "."], Some(&test_repo), &[]).unwrap();
3157        build::run_command(
3158            "git",
3159            &[
3160                "-c",
3161                "user.name=Testy",
3162                "-c",
3163                "user.email=testy@example.com",
3164                "commit",
3165                "--message",
3166                "initial commit",
3167            ],
3168            Some(&test_repo),
3169            &[
3170                ("GIT_CONFIG_SYSTEM", "/dev/null"),
3171                ("GIT_CONFIG_GLOBAL", "/dev/null"),
3172            ],
3173        )
3174        .unwrap();
3175        build::run_command("git", &["tag", "foo"], Some(&test_repo), &[]).unwrap();
3176
3177        write_script(
3178            &test_repo.join("x"),
3179            "\
3180            #!/bin/bash
3181            mkdir -p build/foo/stage2/bin
3182            mkdir -p build/foo/stage2-tools-bin
3183            mkdir -p build/foo/stage3/lib/rustlib/riscv32im-risc0-zkvm-elf
3184            touch build/foo/stage2/bin/rustc
3185            touch build/foo/stage2-tools-bin/cargo-fmt
3186            touch build/foo/stage2-tools-bin/bar-fmt
3187            echo 'build output line 1'
3188            echo 'build output line 2'
3189            env >> x_env
3190            echo '=====' >> x_env
3191            ",
3192        );
3193
3194        build::run_command("git", &["add", "."], Some(&test_repo), &[]).unwrap();
3195        build::run_command(
3196            "git",
3197            &[
3198                "-c",
3199                "user.name=Testy",
3200                "-c",
3201                "user.email=testy@example.com",
3202                "commit",
3203                "--message",
3204                "bar",
3205            ],
3206            Some(&test_repo),
3207            &[
3208                ("GIT_CONFIG_SYSTEM", "/dev/null"),
3209                ("GIT_CONFIG_GLOBAL", "/dev/null"),
3210            ],
3211        )
3212        .unwrap();
3213
3214        let commit = build::git_short_rev_parse(&test_repo, "HEAD").unwrap();
3215        let mut version = Version::new(1, 34, 0);
3216        version.build = semver::BuildMetadata::new(&commit).unwrap();
3217
3218        (test_repo, version)
3219    }
3220
3221    #[test]
3222    fn build_rust_toolchain() {
3223        let server = MockDistributionServer::new();
3224        let (tmp_dir, mut rzup) = setup_test_env(
3225            server.base_urls.clone(),
3226            None,
3227            None,
3228            server.private_key.clone(),
3229            Platform::new("x86_64", Os::Linux),
3230        );
3231
3232        let (test_repo, version) = create_fake_rust_repo(&tmp_dir, Version::new(1, 34, 0));
3233
3234        let repo_url = format!("file://{}", test_repo.display());
3235
3236        run_and_assert_events(
3237            &mut rzup,
3238            |rzup| {
3239                rzup.build_rust_toolchain(&repo_url, &Some("master".to_string()), &None)
3240                    .unwrap();
3241            },
3242            vec![
3243                RzupEvent::BuildingRustToolchain,
3244                RzupEvent::BuildingRustToolchainUpdate {
3245                    message: "cloning git repository".into(),
3246                },
3247                RzupEvent::BuildingRustToolchainUpdate {
3248                    message: "./x build".into(),
3249                },
3250                RzupEvent::BuildingRustToolchainUpdate {
3251                    message: "build output line 1".into(),
3252                },
3253                RzupEvent::BuildingRustToolchainUpdate {
3254                    message: "build output line 2".into(),
3255                },
3256                RzupEvent::BuildingRustToolchainUpdate {
3257                    message: "./x build --stage 2".into(),
3258                },
3259                RzupEvent::BuildingRustToolchainUpdate {
3260                    message: "build output line 1".into(),
3261                },
3262                RzupEvent::BuildingRustToolchainUpdate {
3263                    message: "build output line 2".into(),
3264                },
3265                RzupEvent::BuildingRustToolchainUpdate {
3266                    message: "./x build --stage 3".into(),
3267                },
3268                RzupEvent::BuildingRustToolchainUpdate {
3269                    message: "build output line 1".into(),
3270                },
3271                RzupEvent::BuildingRustToolchainUpdate {
3272                    message: "build output line 2".into(),
3273                },
3274                RzupEvent::BuildingRustToolchainUpdate {
3275                    message: "installing".into(),
3276                },
3277                RzupEvent::DoneBuildingRustToolchain {
3278                    version: version.to_string(),
3279                },
3280            ],
3281        );
3282
3283        assert_eq!(
3284            rzup.get_default_version(&Component::RustToolchain)
3285                .unwrap()
3286                .unwrap()
3287                .0,
3288            version
3289        );
3290
3291        std::fs::remove_dir_all(tmp_dir.path().join(".risc0/tmp")).unwrap();
3292        assert_files(
3293            tmp_dir.path(),
3294            vec![
3295                format!(".risc0/toolchains/v{version}-rust-x86_64-unknown-linux-gnu/bin/bar-fmt"),
3296                format!(".risc0/toolchains/v{version}-rust-x86_64-unknown-linux-gnu/bin/cargo-fmt"),
3297                format!(".risc0/toolchains/v{version}-rust-x86_64-unknown-linux-gnu/bin/rustc"),
3298            ],
3299        );
3300    }
3301
3302    #[test]
3303    fn build_rust_toolchain_twice_different_versions() {
3304        let server = MockDistributionServer::new();
3305        let (tmp_dir, mut rzup) = setup_test_env(
3306            server.base_urls.clone(),
3307            None,
3308            None,
3309            server.private_key.clone(),
3310            Platform::new("x86_64", Os::Linux),
3311        );
3312
3313        let (test_repo, master) = create_fake_rust_repo(&tmp_dir, Version::new(1, 34, 0));
3314
3315        let repo_url = format!("file://{}", test_repo.display());
3316        rzup.build_rust_toolchain(&repo_url, &Some("foo".to_string()), &None)
3317            .unwrap();
3318
3319        let foo_commit = build::git_short_rev_parse(&test_repo, "foo").unwrap();
3320        let mut foo = master.clone();
3321        foo.build = semver::BuildMetadata::new(&foo_commit).unwrap();
3322
3323        rzup.build_rust_toolchain(&repo_url, &Some("master".to_string()), &None)
3324            .unwrap();
3325
3326        std::fs::remove_dir_all(tmp_dir.path().join(".risc0/tmp")).unwrap();
3327        assert_files(
3328            tmp_dir.path(),
3329            vec![
3330                format!(".risc0/toolchains/v{master}-rust-x86_64-unknown-linux-gnu/bin/bar-fmt"),
3331                format!(".risc0/toolchains/v{master}-rust-x86_64-unknown-linux-gnu/bin/cargo-fmt"),
3332                format!(".risc0/toolchains/v{master}-rust-x86_64-unknown-linux-gnu/bin/rustc"),
3333                format!(".risc0/toolchains/v{foo}-rust-x86_64-unknown-linux-gnu/bin/cargo-fmt"),
3334                format!(".risc0/toolchains/v{foo}-rust-x86_64-unknown-linux-gnu/bin/rustc"),
3335            ],
3336        );
3337    }
3338
3339    fn parse_env_output(input: &str) -> Vec<HashMap<&str, &str>> {
3340        let Some(last_divider) = input.rfind("====") else {
3341            return vec![];
3342        };
3343
3344        input[..last_divider]
3345            .split("====")
3346            .map(|invocation| {
3347                invocation
3348                    .split("\n")
3349                    .filter_map(|line| line.split_once("="))
3350                    .collect()
3351            })
3352            .collect()
3353    }
3354
3355    #[test]
3356    fn build_rust_toolchain_loweratomic_flag() {
3357        let expectations = [
3358            (Version::new(1, 80, 0), "-Cpasses=loweratomic"),
3359            (Version::new(1, 81, 0), "-Cpasses=loweratomic"),
3360            (Version::new(1, 81, 1), "-Cpasses=loweratomic"),
3361            (Version::new(1, 82, 0), "-Cpasses=lower-atomic"),
3362            (Version::new(1, 82, 1), "-Cpasses=lower-atomic"),
3363            (Version::new(1, 83, 0), "-Cpasses=lower-atomic"),
3364        ];
3365
3366        for (version, expected_lower_atomic) in expectations {
3367            let server = MockDistributionServer::new();
3368            let (tmp_dir, mut rzup) = setup_test_env(
3369                server.base_urls.clone(),
3370                None,
3371                None,
3372                server.private_key.clone(),
3373                Platform::new("x86_64", Os::Linux),
3374            );
3375
3376            let (test_repo, _) = create_fake_rust_repo(&tmp_dir, version.clone());
3377            let repo_url = format!("file://{}", test_repo.display());
3378
3379            rzup.build_rust_toolchain(&repo_url, &Some("master".to_string()), &None)
3380                .unwrap();
3381
3382            // Our fake x script creates this file
3383            let env_raw = std::fs::read_to_string(
3384                tmp_dir.path().join(".risc0/tmp/build-rust-toolchain/x_env"),
3385            )
3386            .unwrap();
3387            let env = parse_env_output(&env_raw);
3388
3389            // three ./x invocations
3390            assert_eq!(env.len(), 3);
3391
3392            for e in env {
3393                assert_eq!(
3394                    e["CARGO_TARGET_RISCV32IM_RISC0_ZKVM_ELF_RUSTFLAGS"], expected_lower_atomic,
3395                    "lower atomic unexpected for {version}"
3396                );
3397            }
3398        }
3399    }
3400
3401    fn create_test_tar_for_upload(tmp_dir: &TempDir, id: u64) -> (u64, PathBuf, String) {
3402        let mut tar_bytes = vec![];
3403        let mut tar_builder = tar::Builder::new(&mut tar_bytes);
3404        let mut header = tar::Header::new_gnu();
3405        header.set_size(4);
3406        tar_builder
3407            .append_data(
3408                &mut header,
3409                format!("tar_contents{id}.bin"),
3410                &[1, 2, 3, 4][..],
3411            )
3412            .unwrap();
3413        tar_builder.finish().unwrap();
3414        drop(tar_builder);
3415
3416        let mut tar_xz_bytes = vec![];
3417        let mut encoder = liblzma::write::XzEncoder::new(&mut tar_xz_bytes, 1);
3418        encoder.write_all(&tar_bytes).unwrap();
3419        drop(encoder);
3420
3421        let mut hasher = sha2::Sha256::new();
3422        hasher.update(&tar_xz_bytes);
3423        let sha256 = format!("{:x}", hasher.finalize());
3424
3425        let download_size = tar_xz_bytes.len() as u64;
3426        let payload = tmp_dir.path().join("upload.tar.xz");
3427        std::fs::write(&payload, tar_xz_bytes).unwrap();
3428
3429        (download_size, payload, sha256)
3430    }
3431
3432    fn upload_test(
3433        base_url: &str,
3434        download_size: u64,
3435        platform: Option<Platform>,
3436        payload: &Path,
3437        sha256: &str,
3438        force: bool,
3439        rzup: &mut Rzup,
3440    ) {
3441        run_and_assert_events(
3442            rzup,
3443            |rzup| {
3444                rzup.publish_upload(
3445                    &Component::Risc0Groth16,
3446                    &Version::new(4, 0, 0),
3447                    platform,
3448                    payload,
3449                    force,
3450                )
3451                .unwrap();
3452            },
3453            vec![
3454                RzupEvent::Print {
3455                    message: "Getting private key from AWS".into(),
3456                },
3457                RzupEvent::Print {
3458                    message: "Reading distribution_manifest.json".into(),
3459                },
3460                RzupEvent::Print {
3461                    message: "Validating artifact".into(),
3462                },
3463                RzupEvent::Print {
3464                    message: "Calculating sha256 for risc0-groth16 4.0.0".into(),
3465                },
3466                RzupEvent::TransferStarted {
3467                    kind: TransferKind::Upload,
3468                    id: format!("risc0-groth16/{sha256}"),
3469                    version: Some("4.0.0".into()),
3470                    url: Some(format!(
3471                        "{base_url}/rzup/components/risc0-groth16/sha256/{sha256}"
3472                    )),
3473                    len: Some(download_size),
3474                },
3475                RzupEvent::TransferProgress {
3476                    id: format!("risc0-groth16/{sha256}"),
3477                    incr: download_size,
3478                },
3479                RzupEvent::TransferCompleted {
3480                    kind: TransferKind::Upload,
3481                    id: format!("risc0-groth16/{sha256}"),
3482                    version: Some("4.0.0".into()),
3483                },
3484                RzupEvent::Print {
3485                    message: "Updating distribution_manifest.json for risc0-groth16 4.0.0".into(),
3486                },
3487            ],
3488        );
3489    }
3490
3491    fn publish_fixture() -> (MockDistributionServer, Platform, TempDir, Rzup) {
3492        let server = MockDistributionServer::new();
3493        let aws_creds = AwsCredentials::new(
3494            "AKIDEXAMPLE",
3495            "wJalrXUtnFEMI/K7MDENG+bPxRfiCYEXAMPLEKEY",
3496            None,
3497            None,
3498            "hardcoded-credentials",
3499        );
3500        let platform = Platform::new("x86_64", Os::Linux);
3501        let (tmp_dir, rzup) = setup_test_env(
3502            server.base_urls.clone(),
3503            None,
3504            Some(aws_creds),
3505            server.private_key.clone(),
3506            platform,
3507        );
3508        (server, platform, tmp_dir, rzup)
3509    }
3510
3511    fn publish_upload_test(target_specific: bool) {
3512        let (server, platform, tmp_dir, mut rzup) = publish_fixture();
3513
3514        let base_url = &server.base_urls.s3_base_url;
3515        let (download_size, payload, sha256) = create_test_tar_for_upload(&tmp_dir, 1);
3516        upload_test(
3517            base_url,
3518            download_size,
3519            target_specific.then_some(platform),
3520            &payload,
3521            &sha256,
3522            false, /* force */
3523            &mut rzup,
3524        );
3525
3526        install_test(
3527            server.base_urls.clone(),
3528            server.private_key.clone(),
3529            Component::Risc0Groth16,
3530            Component::Risc0Groth16,
3531            Version::new(4, 0, 0),
3532            format!(
3533                "{base_url}/rzup/components/risc0-groth16/sha256/{sha256}",
3534                base_url = server.base_urls.s3_base_url
3535            ),
3536            download_size,
3537            vec![format!(
3538                ".risc0/extensions/v4.0.0-risc0-groth16/tar_contents1.bin"
3539            )],
3540            vec![],
3541            ".risc0/extensions/v4.0.0-risc0-groth16",
3542            false, /* use_github_token */
3543            platform,
3544        );
3545
3546        assert_eq!(
3547            rzup.get_latest_version(&Component::Risc0Groth16).unwrap(),
3548            Version::new(2, 0, 0)
3549        );
3550    }
3551
3552    #[test]
3553    fn publish_upload_target_agnostic() {
3554        publish_upload_test(true /* target_specific */)
3555    }
3556
3557    #[test]
3558    fn publish_upload_target_specific() {
3559        publish_upload_test(true /* target_specific */)
3560    }
3561
3562    #[test]
3563    fn publish_upload_invalid_tar_xz() {
3564        let (_server, _platform, tmp_dir, mut rzup) = publish_fixture();
3565
3566        let data = b"abcdef";
3567        let payload = tmp_dir.path().join("upload_bin");
3568        std::fs::write(&payload, data).unwrap();
3569
3570        let err = rzup
3571            .publish_upload(
3572                &Component::Risc0Groth16,
3573                &Version::new(4, 0, 0),
3574                None, /* platform */
3575                &payload,
3576                false, /* force */
3577            )
3578            .unwrap_err();
3579
3580        assert_eq!(
3581            err,
3582            RzupError::Other("invalid tar.xz file: premature eof".into())
3583        );
3584    }
3585
3586    #[test]
3587    fn publish_upload_empty_tar_xz() {
3588        let (_server, _platform, tmp_dir, mut rzup) = publish_fixture();
3589
3590        let mut tar_bytes = vec![];
3591        let mut tar_builder = tar::Builder::new(&mut tar_bytes);
3592        tar_builder.finish().unwrap();
3593        drop(tar_builder);
3594
3595        let mut tar_xz_bytes = vec![];
3596        let mut encoder = liblzma::write::XzEncoder::new(&mut tar_xz_bytes, 1);
3597        encoder.write_all(&tar_bytes).unwrap();
3598        drop(encoder);
3599
3600        let payload = tmp_dir.path().join("upload.tar.xz");
3601        std::fs::write(&payload, tar_xz_bytes).unwrap();
3602
3603        let err = rzup
3604            .publish_upload(
3605                &Component::Risc0Groth16,
3606                &Version::new(4, 0, 0),
3607                None, /* platform */
3608                &payload,
3609                false, /* force */
3610            )
3611            .unwrap_err();
3612
3613        assert_eq!(err, RzupError::Other("invalid tar.xz file: empty".into()));
3614    }
3615
3616    fn publish_upload_duplicate_force_false_test(
3617        a_platform: Option<Platform>,
3618        b_platform: Option<Platform>,
3619        expected_msg: &str,
3620    ) {
3621        let (server, _platform, tmp_dir, mut rzup) = publish_fixture();
3622
3623        let base_url = &server.base_urls.s3_base_url;
3624        let (download_size, payload, sha256) = create_test_tar_for_upload(&tmp_dir, 1);
3625        upload_test(
3626            base_url,
3627            download_size,
3628            a_platform,
3629            &payload,
3630            &sha256,
3631            false, /* force */
3632            &mut rzup,
3633        );
3634
3635        let err = rzup
3636            .publish_upload(
3637                &Component::Risc0Groth16,
3638                &Version::new(4, 0, 0),
3639                b_platform,
3640                &payload,
3641                false, /* force */
3642            )
3643            .unwrap_err();
3644        assert_eq!(err, RzupError::Other(expected_msg.into()));
3645    }
3646
3647    #[test]
3648    fn publish_upload_duplicate_force_false() {
3649        publish_upload_duplicate_force_false_test(
3650            None,
3651            None,
3652            "artifact already exists for this release, add --force flag to overwrite",
3653        );
3654        publish_upload_duplicate_force_false_test(
3655            Some(Platform::new("x86_64", Os::Linux)),
3656            None,
3657            "target-specific artifact already exists for this release version, \
3658            add --force flag to overwrite",
3659        );
3660        publish_upload_duplicate_force_false_test(
3661            None,
3662            Some(Platform::new("x86_64", Os::Linux)),
3663            "target-agnostic artifact already exists for this release version, \
3664            add --force flag to overwrite",
3665        );
3666        publish_upload_duplicate_force_false_test(
3667            Some(Platform::new("x86_64", Os::Linux)),
3668            Some(Platform::new("x86_64", Os::Linux)),
3669            "artifact already exists for this release and target, add --force flag to overwrite",
3670        );
3671    }
3672
3673    fn publish_upload_duplicate_force_true_test(
3674        a_platform: Option<Platform>,
3675        b_platform: Option<Platform>,
3676    ) {
3677        let (server, platform, tmp_dir, mut rzup) = publish_fixture();
3678
3679        let base_url = &server.base_urls.s3_base_url;
3680        let (download_size, payload, sha256) = create_test_tar_for_upload(&tmp_dir, 1);
3681        upload_test(
3682            base_url,
3683            download_size,
3684            a_platform,
3685            &payload,
3686            &sha256,
3687            false, /* force */
3688            &mut rzup,
3689        );
3690
3691        let (download_size, payload, sha256) = create_test_tar_for_upload(&tmp_dir, 2);
3692        upload_test(
3693            base_url,
3694            download_size,
3695            b_platform,
3696            &payload,
3697            &sha256,
3698            true, /* force */
3699            &mut rzup,
3700        );
3701
3702        install_test(
3703            server.base_urls.clone(),
3704            server.private_key.clone(),
3705            Component::Risc0Groth16,
3706            Component::Risc0Groth16,
3707            Version::new(4, 0, 0),
3708            format!(
3709                "{base_url}/rzup/components/risc0-groth16/sha256/{sha256}",
3710                base_url = server.base_urls.s3_base_url
3711            ),
3712            download_size,
3713            vec![format!(
3714                ".risc0/extensions/v4.0.0-risc0-groth16/tar_contents2.bin"
3715            )],
3716            vec![],
3717            ".risc0/extensions/v4.0.0-risc0-groth16",
3718            false, /* use_github_token */
3719            platform,
3720        );
3721    }
3722
3723    #[test]
3724    fn publish_upload_duplicate_force_true() {
3725        publish_upload_duplicate_force_true_test(None, None);
3726        publish_upload_duplicate_force_true_test(Some(Platform::new("x86_64", Os::Linux)), None);
3727        publish_upload_duplicate_force_true_test(None, Some(Platform::new("x86_64", Os::Linux)));
3728        publish_upload_duplicate_force_true_test(
3729            Some(Platform::new("x86_64", Os::Linux)),
3730            Some(Platform::new("x86_64", Os::Linux)),
3731        );
3732    }
3733
3734    #[test]
3735    fn publish_set_latest() {
3736        let (_server, _platform, _tmp_dir, mut rzup) = publish_fixture();
3737
3738        assert_eq!(
3739            rzup.get_latest_version(&Component::Risc0Groth16).unwrap(),
3740            Version::new(2, 0, 0)
3741        );
3742
3743        run_and_assert_events(
3744            &mut rzup,
3745            |rzup| {
3746                rzup.publish_set_latest(&Component::Risc0Groth16, &Version::new(1, 0, 0))
3747                    .unwrap();
3748            },
3749            vec![RzupEvent::Print {
3750                message: "Updating distribution_manifest.json for risc0-groth16, \
3751                    setting latest-version to 1.0.0"
3752                    .into(),
3753            }],
3754        );
3755
3756        assert_eq!(
3757            rzup.get_latest_version(&Component::Risc0Groth16).unwrap(),
3758            Version::new(1, 0, 0)
3759        );
3760    }
3761
3762    #[test]
3763    fn publish_set_latest_not_found() {
3764        let (_server, _platform, _tmp_dir, mut rzup) = publish_fixture();
3765
3766        assert_eq!(
3767            rzup.get_latest_version(&Component::Risc0Groth16).unwrap(),
3768            Version::new(2, 0, 0)
3769        );
3770
3771        let err = rzup
3772            .publish_set_latest(&Component::Risc0Groth16, &Version::new(7, 0, 0))
3773            .unwrap_err();
3774        assert_eq!(
3775            err,
3776            RzupError::Other("release for risc0-groth16 at version 7.0.0 not found".into())
3777        );
3778    }
3779
3780    #[test]
3781    fn publish_create_artifact_directory() {
3782        let (_server, _platform, tmp_dir, mut rzup) = publish_fixture();
3783
3784        // Create some test input files
3785        let input_path = tmp_dir.path().join("input-path");
3786        std::fs::create_dir_all(&input_path).unwrap();
3787        for p in [Path::new("a.txt"), Path::new("b/c.txt")] {
3788            let p = input_path.join(p);
3789            if let Some(parent) = p.parent() {
3790                std::fs::create_dir_all(parent).unwrap();
3791            }
3792            std::fs::write(p, "hello world").unwrap();
3793        }
3794
3795        let output_path = tmp_dir.path().join("output.tar.xz");
3796        rzup.publish_create_artifact(&input_path, &output_path, 6 /* compression_level */)
3797            .unwrap();
3798
3799        let file = std::fs::File::open(output_path).unwrap();
3800        let mut tar_reader = tar::Archive::new(liblzma::bufread::XzDecoder::new(
3801            std::io::BufReader::new(file),
3802        ));
3803        let mut paths: Vec<_> = tar_reader
3804            .entries()
3805            .unwrap()
3806            .map(|e| PathBuf::from(e.unwrap().path().unwrap()))
3807            .collect();
3808        paths.sort();
3809        assert_eq!(paths, vec![Path::new("a.txt"), Path::new("b/c.txt")]);
3810    }
3811
3812    #[test]
3813    fn publish_create_artifact_file() {
3814        let (_server, _platform, tmp_dir, mut rzup) = publish_fixture();
3815
3816        // Create a test input file
3817        let input_path = tmp_dir.path().join("input-path.txt");
3818        std::fs::write(&input_path, "hello world").unwrap();
3819
3820        let output_path = tmp_dir.path().join("output.tar.xz");
3821        rzup.publish_create_artifact(&input_path, &output_path, 6 /* compression_level */)
3822            .unwrap();
3823
3824        let file = std::fs::File::open(output_path).unwrap();
3825        let mut tar_reader = tar::Archive::new(liblzma::bufread::XzDecoder::new(
3826            std::io::BufReader::new(file),
3827        ));
3828        let mut paths: Vec<_> = tar_reader
3829            .entries()
3830            .unwrap()
3831            .map(|e| PathBuf::from(e.unwrap().path().unwrap()))
3832            .collect();
3833        paths.sort();
3834        assert_eq!(paths, vec![Path::new("input-path.txt")]);
3835    }
3836}