cargo_clone_core/
lib.rs

1// Copyright 2015 Jan Likar.
2//
3// Licensed under the Apache License, Version 2.0 <LICENSE-APACHE or
4// http://www.apache.org/licenses/LICENSE-2.0> or the MIT license
5// <LICENSE-MIT or http://opensource.org/licenses/MIT>, at your
6// option. This file may not be copied, modified, or distributed
7// except according to those terms.
8
9//! Fetch the source code of a Rust crate from a registry.
10
11#![warn(missing_docs)]
12
13mod cloner_builder;
14mod source;
15
16pub use cloner_builder::*;
17pub use source::*;
18
19use std::fs;
20use std::path::Path;
21use std::path::PathBuf;
22use std::process::Command;
23
24use anyhow::{Context, bail};
25
26use cargo::core::Package;
27use cargo::core::dependency::Dependency;
28use cargo::sources::registry::IndexSummary;
29use cargo::sources::source::QueryKind;
30use cargo::sources::source::Source;
31use cargo::sources::{PathSource, SourceConfigMap};
32use cargo::util::cache_lock::CacheLockMode;
33use cargo::util::context::GlobalContext;
34use semver::VersionReq;
35
36use walkdir::WalkDir;
37
38// Re-export cargo types.
39pub use cargo::{core::SourceId, util::CargoResult};
40
41/// Rust crate.
42#[derive(PartialEq, Eq, Debug)]
43pub struct Crate {
44    name: String,
45    version: Option<String>,
46}
47
48impl Crate {
49    /// Create a new [`Crate`].
50    /// If `version` is not specified, the latest version is chosen.
51    pub fn new(name: String, version: Option<String>) -> Crate {
52        Crate { name, version }
53    }
54}
55
56/// Clones a crate.
57pub struct Cloner {
58    /// Cargo context.
59    pub(crate) context: GlobalContext,
60    /// Directory where the crates will be cloned.
61    /// Each crate is cloned into a subdirectory of this directory.
62    pub(crate) directory: PathBuf,
63    /// Where the crates will be cloned from.
64    pub(crate) srcid: SourceId,
65    /// If true, use `git` to clone the git repository present in the manifest metadata.
66    pub(crate) use_git: bool,
67}
68
69impl Cloner {
70    /// Creates a new [`ClonerBuilder`] that:
71    /// - Uses crates.io as source.
72    /// - Clones the crates into the current directory.
73    pub fn builder() -> ClonerBuilder {
74        ClonerBuilder::new()
75    }
76
77    /// Clone the specified crate from registry or git repository.
78    /// The crate is cloned in the directory specified by the [`ClonerBuilder`].
79    pub fn clone_in_dir(&self, crate_: &Crate) -> CargoResult<()> {
80        let _lock = self
81            .context
82            .acquire_package_cache_lock(CacheLockMode::DownloadExclusive)?;
83
84        let mut src = get_source(&self.srcid, &self.context)?;
85
86        self.clone_in(crate_, &self.directory, &mut src)
87    }
88
89    /// Clone the specified crates from registry or git repository.
90    /// Each crate is cloned in a subdirectory named as the crate name.
91    pub fn clone(&self, crates: &[Crate]) -> CargoResult<()> {
92        let _lock = self
93            .context
94            .acquire_package_cache_lock(CacheLockMode::DownloadExclusive)?;
95
96        let mut src = get_source(&self.srcid, &self.context)?;
97
98        for crate_ in crates {
99            let mut dest_path = self.directory.clone();
100
101            dest_path.push(&crate_.name);
102
103            self.clone_in(crate_, &dest_path, &mut src)?;
104        }
105
106        Ok(())
107    }
108
109    fn clone_in<'a, T>(&self, crate_: &Crate, dest_path: &Path, src: &mut T) -> CargoResult<()>
110    where
111        T: Source + 'a,
112    {
113        if !dest_path.exists() {
114            fs::create_dir_all(dest_path)?;
115        }
116
117        self.context
118            .shell()
119            .verbose(|s| s.note(format!("Cloning into {:?}", &self.directory)))?;
120
121        // Cloning into an existing directory is only allowed if the directory is empty.
122        let is_empty = dest_path.read_dir()?.next().is_none();
123        if !is_empty {
124            bail!(
125                "destination path '{}' already exists and is not an empty directory.",
126                dest_path.display()
127            );
128        }
129
130        self.clone_single(crate_, dest_path, src)
131    }
132
133    fn clone_single<'a, T>(&self, crate_: &Crate, dest_path: &Path, src: &mut T) -> CargoResult<()>
134    where
135        T: Source + 'a,
136    {
137        let pkg = select_pkg(&self.context, src, &crate_.name, crate_.version.as_deref())?;
138
139        if self.use_git {
140            let repo = &pkg.manifest().metadata().repository;
141
142            if repo.is_none() {
143                bail!(
144                    "Cannot clone {} from git repo because it is not specified in package's manifest.",
145                    &crate_.name
146                )
147            }
148
149            clone_git_repo(repo.as_ref().unwrap(), dest_path)?;
150        } else {
151            clone_directory(pkg.root(), dest_path)?;
152        }
153
154        Ok(())
155    }
156}
157
158fn get_source<'a>(
159    srcid: &SourceId,
160    context: &'a GlobalContext,
161) -> CargoResult<Box<dyn Source + 'a>> {
162    let mut source = if srcid.is_path() {
163        let path = srcid.url().to_file_path().expect("path must be valid");
164        Box::new(PathSource::new(&path, *srcid, context))
165    } else {
166        let map = SourceConfigMap::new(context)?;
167        map.load(*srcid, &Default::default())?
168    };
169
170    source.invalidate_cache();
171    Ok(source)
172}
173
174fn select_pkg<'a, T>(
175    context: &GlobalContext,
176    src: &mut T,
177    name: &str,
178    vers: Option<&str>,
179) -> CargoResult<Package>
180where
181    T: Source + 'a,
182{
183    let dep = Dependency::parse(name, vers, src.source_id())?;
184    let mut summaries = vec![];
185
186    loop {
187        match src.query(&dep, QueryKind::Exact, &mut |summary| {
188            summaries.push(summary)
189        })? {
190            std::task::Poll::Ready(()) => break,
191            std::task::Poll::Pending => src.block_until_ready()?,
192        }
193    }
194
195    let latest = summaries
196        .iter()
197        .filter_map(|idxs| match idxs {
198            IndexSummary::Candidate(s) => Some(s),
199            _ => None,
200        })
201        .max_by_key(|s| s.version());
202
203    match latest {
204        Some(l) => {
205            context
206                .shell()
207                .note(format!("Downloading {} {}", name, l.version()))?;
208            let pkg = Box::new(src).download_now(l.package_id(), context)?;
209            Ok(pkg)
210        }
211        None => bail!("Package `{}@{}` not found", name, vers.unwrap_or("*.*.*")),
212    }
213}
214
215fn parse_version_req(version: &str) -> CargoResult<String> {
216    // This function's main purpose is to treat "x.y.z" as "=x.y.z"
217    // so specifying the version in CLI works as expected.
218    let first = version.chars().next();
219
220    if first.is_none() {
221        bail!("Version cannot be empty.")
222    };
223
224    let is_req = "<>=^~".contains(first.unwrap()) || version.contains('*');
225
226    if is_req {
227        let vers = VersionReq::parse(version)
228            .with_context(|| format!("Invalid version requirement: `{version}`."))?;
229        Ok(vers.to_string())
230    } else {
231        Ok(format!("={version}"))
232    }
233}
234
235// clone_directory copies the contents of one directory into another directory, which must
236// already exist.
237fn clone_directory(from: &Path, to: &Path) -> CargoResult<()> {
238    if !to.is_dir() {
239        bail!("Not a directory: {}", to.to_string_lossy());
240    }
241    for entry in WalkDir::new(from) {
242        let entry = entry.unwrap();
243        let file_type = entry.file_type();
244        let mut dest_path = to.to_owned();
245        dest_path.push(entry.path().strip_prefix(from).unwrap());
246
247        if entry.file_name() == ".cargo-ok" {
248            continue;
249        }
250
251        if file_type.is_file() {
252            // .cargo-ok is not wanted in this context
253            fs::copy(entry.path(), &dest_path)?;
254        } else if file_type.is_dir() {
255            if dest_path == to {
256                continue;
257            }
258            fs::create_dir(&dest_path)?;
259        }
260    }
261
262    Ok(())
263}
264
265fn clone_git_repo(repo: &str, to: &Path) -> CargoResult<()> {
266    let status = Command::new("git")
267        .arg("clone")
268        .arg(repo)
269        .arg(to.to_str().unwrap())
270        .status()
271        .context("Failed to clone from git repo.")?;
272
273    if !status.success() {
274        bail!("Failed to clone from git repo.")
275    }
276
277    Ok(())
278}
279
280/// Parses crate specifications like: crate, crate@x.y.z, crate@~23.4.5.
281pub fn parse_name_and_version(spec: &str) -> CargoResult<Crate> {
282    if !spec.contains('@') {
283        return Ok(Crate::new(spec.to_owned(), None));
284    }
285
286    let mut parts = spec.split('@');
287    let crate_ = parts
288        .next()
289        .context(format!("Crate name missing in `{spec}`."))?;
290    let version = parts
291        .next()
292        .context(format!("Crate version missing in `{spec}`."))?;
293
294    Ok(Crate::new(
295        crate_.to_owned(),
296        Some(parse_version_req(version)?),
297    ))
298}
299
300#[cfg(test)]
301mod tests {
302    use std::{env, path::PathBuf};
303
304    use super::*;
305    use tempfile::tempdir;
306
307    #[test]
308    fn test_parse_version_req() {
309        assert_eq!("=12.4.5", parse_version_req("12.4.5").unwrap());
310        assert_eq!("=12.4.5", parse_version_req("=12.4.5").unwrap());
311        assert_eq!("12.2.*", parse_version_req("12.2.*").unwrap());
312    }
313
314    #[test]
315    fn test_parse_version_req_invalid_req() {
316        assert_eq!(
317            "Invalid version requirement: `=foo`.",
318            parse_version_req("=foo").unwrap_err().to_string()
319        );
320    }
321
322    #[test]
323    fn test_clone_directory() {
324        let manifest_dir = env::var("CARGO_MANIFEST_DIR").unwrap();
325        let from = PathBuf::from(manifest_dir).join("tests/data");
326        let to = tempdir().unwrap();
327        let to_path = to.path();
328
329        clone_directory(&from, to_path).unwrap();
330
331        assert!(to_path.join("Cargo.toml").exists());
332        assert!(!to_path.join("cargo-ok").exists());
333    }
334
335    #[test]
336    fn test_clone_repo() {
337        let to = tempdir().unwrap();
338        let to_path = to.path();
339
340        clone_git_repo("https://github.com/janlikar/cargo-clone", to_path).unwrap();
341
342        assert!(to_path.exists());
343        assert!(to_path.join(".git").exists());
344    }
345
346    #[test]
347    fn test_parse_name_and_version() {
348        assert_eq!(
349            parse_name_and_version("foo").unwrap(),
350            Crate::new(String::from("foo"), None)
351        );
352        assert_eq!(
353            parse_name_and_version("foo@1.1.3").unwrap(),
354            Crate::new(String::from("foo"), Some(String::from("=1.1.3")))
355        );
356        assert_eq!(
357            parse_name_and_version("foo@~1.1.3").unwrap(),
358            Crate::new(String::from("foo"), Some(String::from("~1.1.3")))
359        );
360        assert_eq!(
361            parse_name_and_version("foo@1.1.*").unwrap(),
362            Crate::new(String::from("foo"), Some(String::from("1.1.*")))
363        );
364    }
365}