Skip to main content

cargo_fetch/
lib.rs

1#![doc = include_str!("../README.md")]
2
3use cargo::{
4    core::{PackageId, PackageSet, SourceId, SourceMap},
5    sources::CRATES_IO_INDEX,
6    util::IntoUrl,
7};
8use semver::Version;
9use std::{collections::HashSet, io::Write, path::PathBuf, str::FromStr, task::Poll};
10use url::Url;
11
12/// Main API of this library.
13///
14/// Contains cargo config to drive package fetching.
15///
16/// You can construct default instance of this struct by using [`PackageFetcher::new`].
17///
18/// With default [`PackageFetcher`], cargo will try to output status and errors to the `stdout` and `stderr` of
19/// current process. If that is not desirable, you can construct it with
20/// [`PackageFetcher::with_out`], to intercept cargo `write` calls.
21///
22/// After constructing, you can:
23/// * Resolve package versions with [`PackageFetcher::resolve_package`] and [`PackageFetcher::resolve_first`]
24/// * Fetch packages with [`PackageFetcher::fetch`], or [`PackageFetcher::fetch_many`]
25#[derive(Debug)]
26pub struct PackageFetcher {
27    config: cargo::Config,
28}
29
30impl PackageFetcher {
31    /// Constructs [`PackageFetcher`] with default cargo configuration.
32    ///
33    /// Cargo will output its colored status to the `stdout` and `stderr` of the current process by default, if that is not desirable, see
34    /// [`PackageFetcher::with_out`].
35    pub fn new() -> Result<Self, String> {
36        Ok(Self {
37            config: cargo::Config::default().map_err(|e| e.to_string())?,
38        })
39    }
40
41    /// Constructs [`PackageFetcher`] with user-provided stream for cargo to output status to.
42    ///
43    /// Optionally also accepts [`Verbosity`], which is set to [`Verbosity::Verbose`] if [`None`] is provided.
44    pub fn with_out(out: Box<dyn Write>, verbosity: Option<Verbosity>) -> Result<Self, String> {
45        let mut shell = cargo::core::Shell::from_write(out);
46        shell.set_verbosity(verbosity.unwrap_or_default().into());
47        let new_self = Self::new()?;
48        {
49            let mut sh = new_self.config.shell();
50            *sh = shell;
51        }
52        Ok(new_self)
53    }
54
55    /// Resolves all available package versions, given a version requirement and a name of the package.
56    ///
57    /// [`None`] in the `version` parameter means any version, or "*" semver requirement.
58    ///
59    /// `yanked_whitelist` field allows explicitly whitelist specific yanked versions.
60    pub fn resolve_package<N: AsRef<str>>(
61        &self,
62        name: N,
63        version: Option<&str>,
64        source: &PackageSource,
65        yanked_whitelist: Option<HashSet<Package>>,
66    ) -> Result<Vec<Package>, String> {
67        let _lock = self.config.acquire_package_cache_lock().map_err(|e| e.to_string())?;
68        let src = source.to_source_id().map_err(|e| e.to_string())?;
69
70        let whitelist: HashSet<PackageId>;
71
72        if let Some(wl) = yanked_whitelist {
73            whitelist = wl.iter().map(|p| p.package_id).collect();
74        } else {
75            whitelist = Default::default();
76        };
77
78        let mut src = src.load(&self.config, &whitelist).map_err(|e| e.to_string())?;
79
80        let dep = cargo::core::Dependency::parse(name.as_ref(), version, src.source_id())
81            .map_err(|e| e.to_string())?;
82
83        let mut pkgs = vec![];
84
85        src.block_until_ready().map_err(|e| e.to_string())?;
86        let Poll::Ready(res) = src.query(&dep, cargo::core::QueryKind::Exact, &mut |sum| {pkgs.push(Package {package_id: sum.package_id()})}) else {
87			return Err("cargo returned a `Poll::Pending` after `block_until_ready`".into());
88		};
89
90        res.map_err(|e| e.to_string())?;
91
92        Ok(pkgs)
93    }
94
95    /// Resolves first available package version, given a version requirement and a name of the package.
96    ///
97    /// For more information see: [`Self::resolve_package`].
98    pub fn resolve_first<N: AsRef<str>>(
99        &self,
100        name: N,
101        version: Option<&str>,
102        source: &PackageSource,
103        yanked_whitelist: Option<HashSet<Package>>,
104    ) -> Result<Package, String> {
105        let _lock = self.config.acquire_package_cache_lock().map_err(|e| e.to_string())?;
106        let src = source.to_source_id().map_err(|e| e.to_string())?;
107
108        let whitelist: HashSet<PackageId>;
109
110        if let Some(wl) = yanked_whitelist {
111            whitelist = wl.iter().map(|p| p.package_id).collect();
112        } else {
113            whitelist = Default::default();
114        };
115
116        let mut src = src.load(&self.config, &whitelist).map_err(|e| e.to_string())?;
117
118        let dep = cargo::core::Dependency::parse(name.as_ref(), version, src.source_id())
119            .map_err(|e| e.to_string())?;
120
121        let mut pkg: Option<PackageId> = None;
122
123        src.block_until_ready().map_err(|e| e.to_string())?;
124        let Poll::Ready(res) = src.query(&dep, cargo::core::QueryKind::Exact, &mut |sum| {pkg = Some(sum.package_id())}) else {
125			return Err("cargo returned a `Poll::Pending` after `block_until_ready`".into());
126		};
127
128        res.map_err(|e| e.to_string())?;
129
130        if let Some(pkg) = pkg {
131            Ok(Package { package_id: pkg })
132        } else {
133            Err("cargo wasn't able to find the requested package".into())
134        }
135    }
136
137    /// Fetches a single package, and returns the [`PathBuf`] to the root of it.
138    pub fn fetch(&mut self, package: Package) -> Result<PathBuf, String> {
139        let _lock = self.config.acquire_package_cache_lock().map_err(|e| e.to_string())?;
140        let mut map = SourceMap::new();
141
142        let whitelist: HashSet<PackageId> = std::iter::once(package.package_id).collect();
143
144        let mut source = package
145            .package_id
146            .source_id()
147            .load(&self.config, &whitelist)
148            .map_err(|e| e.to_string())?;
149
150        source.block_until_ready().map_err(|e| e.to_string())?;
151
152        map.insert(source);
153
154        let package_set = PackageSet::new(&[package.package_id], map, &self.config).map_err(|e| e.to_string())?;
155        Ok(package_set
156            .get_one(package.package_id)
157            .map_err(|e| e.to_string())?
158            .root()
159            .into())
160    }
161
162    /// Fetches multiple packages, and returns the [`PathBuf`]s to their roots.
163    ///
164    /// **Warning**
165    ///
166    /// This is not guaranteed to return the same amount of roots as requested packages,
167    /// as packages passed in might be from the same source, with the same version, in
168    /// which case cargo will cache the package sources, and return the root only once,
169    /// no matter what amount of duplicate packages was passed.
170    ///
171    /// Errors, if any of the requested packages cannot be fetched.
172    pub fn fetch_many(
173        &mut self,
174        packages: &[Package],
175    ) -> Result<Vec<PathBuf>, String> {
176        let _lock = self.config.acquire_package_cache_lock().map_err(|e| e.to_string())?;
177        let mut map = SourceMap::new();
178
179        let whitelist: HashSet<PackageId> = packages.iter().map(|p| p.package_id).collect();
180
181        for package in packages {
182            let mut source = package
183                .package_id
184                .source_id()
185                .load(&self.config, &whitelist)
186                .map_err(|e| e.to_string())?;
187            source.block_until_ready().map_err(|e| e.to_string())?;
188            map.insert(source);
189        }
190
191        let packages: Vec<PackageId> = packages.iter().map(|p| p.package_id).collect();
192        let package_set = PackageSet::new(&packages, map, &self.config).map_err(|e| e.to_string())?;
193        Ok(package_set
194            .get_many(package_set.package_ids())
195            .map_err(|e| e.to_string())?
196            .iter()
197            .map(|p| p.root().to_owned())
198            .collect())
199    }
200}
201
202/// Cargo verbosity for use with [`PackageFetcher::with_out`].
203#[derive(Debug, Default, Clone, Copy, PartialEq, Eq)]
204pub enum Verbosity {
205    #[default]
206    Verbose,
207    Normal,
208    Quiet,
209}
210
211impl From<Verbosity> for cargo::core::Verbosity {
212    fn from(value: Verbosity) -> Self {
213        match value {
214            Verbosity::Verbose => Self::Verbose,
215            Verbosity::Normal => Self::Normal,
216            Verbosity::Quiet => Self::Quiet,
217        }
218    }
219}
220
221/// Package definition to be fetched by cargo.
222///
223/// This type can either be construct from associated functions, if you have concrete versions of a package.
224/// Or by using [`PackageFetcher::resolve_package`] and [`PackageFetcher::resolve_first`] functions on [`PackageFetcher`]
225/// struct if you need to resolve a package from name and a version requirement, without requiring a specific version.
226///
227/// This type is cheap to copy.
228#[derive(Debug, Clone, Copy, PartialEq, Eq)]
229pub struct Package {
230    package_id: PackageId,
231}
232
233impl Package {
234    /// Constructs a [`Package`], from package name, its [`semver::Version`], and source where to
235    /// fetch it from (crates.io, git, ...).
236    pub fn new<S: AsRef<str>>(name: S, version: Version, source: &PackageSource) -> Result<Self, String> {
237        Ok(Package {
238            package_id: PackageId::new(
239                name.as_ref(),
240                version,
241                source.to_source_id().map_err(|e| e.to_string())?,
242            )
243            .map_err(|e| e.to_string())?,
244        })
245    }
246
247    /// Same as [`Package::new`], but parses [`semver::Version`] from a [`str`].
248    pub fn from_str_ver<S: AsRef<str>, V: AsRef<str>>(
249        name: S,
250        version: V,
251        source: &PackageSource,
252    ) -> Result<Self, String> {
253        Ok(Package {
254            package_id: PackageId::new(
255                name.as_ref(),
256                Version::from_str(version.as_ref()).map_err(|e| e.to_string())?,
257                source.to_source_id().map_err(|e| e.to_string())?,
258            )
259            .map_err(|e| e.to_string())?,
260        })
261    }
262}
263
264/// Git reference for [`PackageSource::Git`]
265#[derive(Debug, Clone, PartialEq, Eq)]
266pub enum GitReference {
267    DefaultBranch,
268    Branch(String),
269    Revision(String),
270    Tag(String),
271}
272
273impl From<GitReference> for cargo::core::GitReference {
274    fn from(value: GitReference) -> Self {
275        match value {
276            GitReference::DefaultBranch => Self::DefaultBranch,
277            GitReference::Branch(branch) => Self::Branch(branch),
278            GitReference::Revision(rev) => Self::Rev(rev),
279            GitReference::Tag(tag) => Self::Tag(tag),
280        }
281    }
282}
283
284/// Defines a source from which a package can be fetched.
285///
286/// This enum can either be constructed manually, or with associated helper functions on it.
287#[derive(Debug, Clone, PartialEq, Eq)]
288pub enum PackageSource {
289    /// Path source:
290    /// ```toml
291    /// dep = { path = "some/local/dependency" }
292    /// ```
293    Path(PathBuf),
294    /// Git source:
295    /// ```toml
296    /// regex = { git = "https://github.com/rust-lang/regex", branch = "next" }
297    /// ```
298    Git { url: Url, git_ref: GitReference },
299    /// Remote registry:
300    /// ```toml
301    /// some-crate = { version = "1.0", registry = "my-registry" }
302    /// ```
303    RemoteRegistry(Url),
304    /// Local registry:
305    /// ```toml
306    /// some-crate = { version = "1.0", registry = "my-local-registry" }
307    /// ```
308    LocalRegistry(PathBuf),
309    /// `crates.io`:
310    /// ```toml
311    /// foo = "1.0.0"
312    /// ```
313    ///
314    /// Note that this does *not* respect `.cargo/config.toml`, so if `default-registry` or `crates-io`
315    /// are overriden, this would still fetch from `crates.io`
316    CratesIo,
317}
318
319impl PackageSource {
320    /// Constructs a new [`PackageSource::Path`] from path.
321    pub fn path<P: Into<PathBuf>>(path: P) -> Result<Self, String> {
322		let mut p = path.into();
323		if !p.is_absolute() {
324			p = p.canonicalize().map_err(|e| e.to_string())?;
325		}
326		Ok(Self::Path(p))
327    }
328
329    /// Constructs a new [`PackageSource::Git`] from repository url and an optional [`GitReference`], if [`None`] is provided, [`GitReference::DefaultBranch`] will be assumed.
330    pub fn git<U: AsRef<str>>(url: U, git_ref: Option<GitReference>) -> Result<Self, <Url as FromStr>::Err> {
331        Ok(Self::Git {
332            url: Url::from_str(url.as_ref())?,
333            git_ref: git_ref.unwrap_or(GitReference::DefaultBranch),
334        })
335    }
336
337    /// Constructs a new [`PackageSource::RemoteRegistry`] from a registry index url.
338    pub fn remote<U: TryInto<Url>>(url: U) -> Result<Self, U::Error> {
339        Ok(Self::RemoteRegistry(url.try_into()?))
340    }
341
342    /// Constructs a new [`PackageSource::LocalRegistry`] from a registry index path.
343    pub fn local<P: Into<PathBuf>>(path: P) -> Self {
344        Self::LocalRegistry(path.into())
345    }
346
347    /// Returns [`PackageSource::CratesIo`].
348    pub fn crates_io() -> Self {
349        Self::CratesIo
350    }
351
352    fn to_source_id(&self) -> cargo::CargoResult<SourceId> {
353        match self {
354            PackageSource::Path(path) => SourceId::for_path(path),
355            PackageSource::Git { url, git_ref } => SourceId::for_git(url, git_ref.clone().into()),
356            PackageSource::RemoteRegistry(url) => SourceId::for_registry(url),
357            PackageSource::LocalRegistry(path) => SourceId::for_local_registry(path),
358            PackageSource::CratesIo => SourceId::for_registry(&CRATES_IO_INDEX.into_url().unwrap()),
359        }
360    }
361}