release_plz_core/
download.rs

1//! Download packages from cargo registry, similar to the `git clone` behavior.
2
3use std::path::Path;
4
5use anyhow::{Context, anyhow};
6use cargo_metadata::{Package, camino::Utf8PathBuf};
7use cargo_utils::CARGO_TOML;
8use tracing::{info, instrument, warn};
9
10use crate::clone::{Cloner, ClonerSource, Crate};
11
12#[derive(Debug)]
13pub struct PackageDownloader {
14    packages: Vec<String>,
15    directory: String,
16    registry: Option<String>,
17    cargo_cwd: Option<Utf8PathBuf>,
18}
19
20impl PackageDownloader {
21    pub fn new(
22        packages: impl IntoIterator<Item = impl Into<String>>,
23        directory: impl Into<String>,
24    ) -> Self {
25        Self {
26            packages: packages.into_iter().map(Into::into).collect(),
27            directory: directory.into(),
28            registry: None,
29            cargo_cwd: None,
30        }
31    }
32
33    pub fn with_registry(self, registry: String) -> Self {
34        Self {
35            registry: Some(registry),
36            ..self
37        }
38    }
39
40    pub fn with_cargo_cwd(self, cargo_cwd: Utf8PathBuf) -> Self {
41        Self {
42            cargo_cwd: Some(cargo_cwd),
43            ..self
44        }
45    }
46
47    #[instrument]
48    pub fn download(&self) -> anyhow::Result<Vec<Package>> {
49        let source: ClonerSource = match &self.registry {
50            Some(registry) => ClonerSource::registry(registry),
51            None => ClonerSource::crates_io(),
52        };
53        info!(
54            "downloading packages from cargo registry {}",
55            source.cargo_source
56        );
57        let crates: Vec<Crate> = self
58            .packages
59            .iter()
60            .map(|package_name| Crate::new(package_name.clone(), None))
61            .collect();
62        let mut cloner_builder = Cloner::builder()
63            .with_directory(&self.directory)
64            .with_source(source);
65        if let Some(cwd) = &self.cargo_cwd {
66            cloner_builder = cloner_builder.with_cargo_cwd(cwd.clone());
67        }
68        let downloaded_packages = cloner_builder
69            .build()
70            .context("can't build cloner")?
71            .clone(&crates)
72            .context("error while downloading packages")?;
73
74        downloaded_packages
75            .iter()
76            .map(|(_package, path)| read_package(path))
77            .collect()
78    }
79}
80
81/// Read a package from file system
82pub fn read_package(directory: impl AsRef<Path>) -> anyhow::Result<Package> {
83    let manifest_path = directory.as_ref().join(CARGO_TOML);
84    let metadata = cargo_metadata::MetadataCommand::new()
85        .no_deps()
86        .manifest_path(manifest_path)
87        .exec()
88        .context("failed to execute cargo_metadata")?;
89    let package = metadata
90        .packages
91        .first()
92        .ok_or_else(|| anyhow!("cannot retrieve package at {:?}", directory.as_ref()))?;
93    Ok(package.clone())
94}
95
96#[cfg(test)]
97mod tests {
98    use fake::Fake;
99    use tempfile::tempdir;
100
101    use super::*;
102
103    #[test]
104    #[ignore = "requires network"]
105    fn one_package_is_downloaded() {
106        let package_name = "rand";
107        let temp_dir = tempdir().unwrap();
108        let directory = temp_dir.as_ref().to_str().expect("invalid tempdir path");
109        let packages = PackageDownloader::new([package_name], directory)
110            .download()
111            .unwrap();
112        let rand = &packages[0];
113        assert_eq!(rand.name, package_name);
114    }
115
116    #[test]
117    #[ignore = "requires network"]
118    fn two_packages_are_downloaded() {
119        let first_package = "rand";
120        let second_package = "rust-gh-example";
121        let temp_dir = tempdir().unwrap();
122        let directory = temp_dir.as_ref().to_str().expect("invalid tempdir path");
123        let packages = PackageDownloader::new([first_package, second_package], directory)
124            .download()
125            .unwrap();
126        assert_eq!(&packages[0].name, first_package);
127        assert_eq!(&packages[1].name, second_package);
128    }
129
130    #[test]
131    #[ignore = "requires network"]
132    fn downloading_non_existing_package_does_not_error() {
133        // Generate random string 15 characters long.
134        let package: String = 15.fake();
135        let temp_dir = tempdir().unwrap();
136        let directory = temp_dir.as_ref().to_str().expect("invalid tempdir path");
137        PackageDownloader::new([&package], directory)
138            .download()
139            .unwrap();
140    }
141}