Skip to main content

cargo/ops/
vendor.rs

1use crate::core::shell::Verbosity;
2use crate::core::{GitReference, Workspace};
3use crate::ops;
4use crate::sources::path::PathSource;
5use crate::util::Sha256;
6use crate::util::{paths, CargoResult, CargoResultExt, Config};
7use anyhow::bail;
8use serde::Serialize;
9use std::collections::HashSet;
10use std::collections::{BTreeMap, BTreeSet, HashMap};
11use std::fs::{self, File};
12use std::io::Write;
13use std::path::{Path, PathBuf};
14
15pub struct VendorOptions<'a> {
16    pub no_delete: bool,
17    pub versioned_dirs: bool,
18    pub destination: &'a Path,
19    pub extra: Vec<PathBuf>,
20}
21
22pub fn vendor(ws: &Workspace<'_>, opts: &VendorOptions<'_>) -> CargoResult<()> {
23    let mut extra_workspaces = Vec::new();
24    for extra in opts.extra.iter() {
25        let extra = ws.config().cwd().join(extra);
26        let ws = Workspace::new(&extra, ws.config())?;
27        extra_workspaces.push(ws);
28    }
29    let workspaces = extra_workspaces.iter().chain(Some(ws)).collect::<Vec<_>>();
30    let vendor_config =
31        sync(ws.config(), &workspaces, opts).chain_err(|| "failed to sync".to_string())?;
32
33    let shell = ws.config().shell();
34    if shell.verbosity() != Verbosity::Quiet {
35        eprint!("To use vendored sources, add this to your .cargo/config for this project:\n\n");
36        print!("{}", &toml::to_string(&vendor_config).unwrap());
37    }
38
39    Ok(())
40}
41
42#[derive(Serialize)]
43struct VendorConfig {
44    source: BTreeMap<String, VendorSource>,
45}
46
47#[derive(Serialize)]
48#[serde(rename_all = "lowercase", untagged)]
49enum VendorSource {
50    Directory {
51        directory: PathBuf,
52    },
53    Registry {
54        registry: Option<String>,
55        #[serde(rename = "replace-with")]
56        replace_with: String,
57    },
58    Git {
59        git: String,
60        branch: Option<String>,
61        tag: Option<String>,
62        rev: Option<String>,
63        #[serde(rename = "replace-with")]
64        replace_with: String,
65    },
66}
67
68fn sync(
69    config: &Config,
70    workspaces: &[&Workspace<'_>],
71    opts: &VendorOptions<'_>,
72) -> CargoResult<VendorConfig> {
73    let canonical_destination = opts.destination.canonicalize();
74    let canonical_destination = canonical_destination
75        .as_ref()
76        .map(|p| &**p)
77        .unwrap_or(opts.destination);
78
79    paths::create_dir_all(&canonical_destination)?;
80    let mut to_remove = HashSet::new();
81    if !opts.no_delete {
82        for entry in canonical_destination.read_dir()? {
83            let entry = entry?;
84            if !entry
85                .file_name()
86                .to_str()
87                .map_or(false, |s| s.starts_with('.'))
88            {
89                to_remove.insert(entry.path());
90            }
91        }
92    }
93
94    // First up attempt to work around rust-lang/cargo#5956. Apparently build
95    // artifacts sprout up in Cargo's global cache for whatever reason, although
96    // it's unsure what tool is causing these issues at this time. For now we
97    // apply a heavy-hammer approach which is to delete Cargo's unpacked version
98    // of each crate to start off with. After we do this we'll re-resolve and
99    // redownload again, which should trigger Cargo to re-extract all the
100    // crates.
101    //
102    // Note that errors are largely ignored here as this is a best-effort
103    // attempt. If anything fails here we basically just move on to the next
104    // crate to work with.
105    for ws in workspaces {
106        let (packages, resolve) =
107            ops::resolve_ws(ws).chain_err(|| "failed to load pkg lockfile")?;
108
109        packages
110            .get_many(resolve.iter())
111            .chain_err(|| "failed to download packages")?;
112
113        for pkg in resolve.iter() {
114            // Don't delete actual source code!
115            if pkg.source_id().is_path() {
116                if let Ok(path) = pkg.source_id().url().to_file_path() {
117                    if let Ok(path) = path.canonicalize() {
118                        to_remove.remove(&path);
119                    }
120                }
121                continue;
122            }
123            if pkg.source_id().is_git() {
124                continue;
125            }
126            if let Ok(pkg) = packages.get_one(pkg) {
127                drop(fs::remove_dir_all(pkg.manifest_path().parent().unwrap()));
128            }
129        }
130    }
131
132    let mut checksums = HashMap::new();
133    let mut ids = BTreeMap::new();
134
135    // Next up let's actually download all crates and start storing internal
136    // tables about them.
137    for ws in workspaces {
138        let (packages, resolve) =
139            ops::resolve_ws(ws).chain_err(|| "failed to load pkg lockfile")?;
140
141        packages
142            .get_many(resolve.iter())
143            .chain_err(|| "failed to download packages")?;
144
145        for pkg in resolve.iter() {
146            // No need to vendor path crates since they're already in the
147            // repository
148            if pkg.source_id().is_path() {
149                continue;
150            }
151            ids.insert(
152                pkg,
153                packages
154                    .get_one(pkg)
155                    .chain_err(|| "failed to fetch package")?
156                    .clone(),
157            );
158
159            checksums.insert(pkg, resolve.checksums().get(&pkg).cloned());
160        }
161    }
162
163    let mut versions = HashMap::new();
164    for id in ids.keys() {
165        let map = versions.entry(id.name()).or_insert_with(BTreeMap::default);
166        if let Some(prev) = map.get(&id.version()) {
167            bail!(
168                "found duplicate version of package `{} v{}` \
169                 vendored from two sources:\n\
170                 \n\
171                 \tsource 1: {}\n\
172                 \tsource 2: {}",
173                id.name(),
174                id.version(),
175                prev,
176                id.source_id()
177            );
178        }
179        map.insert(id.version(), id.source_id());
180    }
181
182    let mut sources = BTreeSet::new();
183    for (id, pkg) in ids.iter() {
184        // Next up, copy it to the vendor directory
185        let src = pkg
186            .manifest_path()
187            .parent()
188            .expect("manifest_path should point to a file");
189        let max_version = *versions[&id.name()].iter().rev().next().unwrap().0;
190        let dir_has_version_suffix = opts.versioned_dirs || id.version() != max_version;
191        let dst_name = if dir_has_version_suffix {
192            // Eg vendor/futures-0.1.13
193            format!("{}-{}", id.name(), id.version())
194        } else {
195            // Eg vendor/futures
196            id.name().to_string()
197        };
198
199        sources.insert(id.source_id());
200        let dst = canonical_destination.join(&dst_name);
201        to_remove.remove(&dst);
202        let cksum = dst.join(".cargo-checksum.json");
203        if dir_has_version_suffix && cksum.exists() {
204            // Always re-copy directory without version suffix in case the version changed
205            continue;
206        }
207
208        config.shell().status(
209            "Vendoring",
210            &format!("{} ({}) to {}", id, src.to_string_lossy(), dst.display()),
211        )?;
212
213        let _ = fs::remove_dir_all(&dst);
214        let pathsource = PathSource::new(src, id.source_id(), config);
215        let paths = pathsource.list_files(pkg)?;
216        let mut map = BTreeMap::new();
217        cp_sources(src, &paths, &dst, &mut map)
218            .chain_err(|| format!("failed to copy over vendored sources for: {}", id))?;
219
220        // Finally, emit the metadata about this package
221        let json = serde_json::json!({
222            "package": checksums.get(id),
223            "files": map,
224        });
225
226        File::create(&cksum)?.write_all(json.to_string().as_bytes())?;
227    }
228
229    for path in to_remove {
230        if path.is_dir() {
231            paths::remove_dir_all(&path)?;
232        } else {
233            paths::remove_file(&path)?;
234        }
235    }
236
237    // add our vendored source
238    let mut config = BTreeMap::new();
239
240    let merged_source_name = "vendored-sources";
241    config.insert(
242        merged_source_name.to_string(),
243        VendorSource::Directory {
244            directory: opts.destination.to_path_buf(),
245        },
246    );
247
248    // replace original sources with vendor
249    for source_id in sources {
250        let name = if source_id.is_default_registry() {
251            "crates-io".to_string()
252        } else {
253            source_id.url().to_string()
254        };
255
256        let source = if source_id.is_default_registry() {
257            VendorSource::Registry {
258                registry: None,
259                replace_with: merged_source_name.to_string(),
260            }
261        } else if source_id.is_remote_registry() {
262            let registry = source_id.url().to_string();
263            VendorSource::Registry {
264                registry: Some(registry),
265                replace_with: merged_source_name.to_string(),
266            }
267        } else if source_id.is_git() {
268            let mut branch = None;
269            let mut tag = None;
270            let mut rev = None;
271            if let Some(reference) = source_id.git_reference() {
272                match *reference {
273                    GitReference::Branch(ref b) => branch = Some(b.clone()),
274                    GitReference::Tag(ref t) => tag = Some(t.clone()),
275                    GitReference::Rev(ref r) => rev = Some(r.clone()),
276                }
277            }
278            VendorSource::Git {
279                git: source_id.url().to_string(),
280                branch,
281                tag,
282                rev,
283                replace_with: merged_source_name.to_string(),
284            }
285        } else {
286            panic!("Invalid source ID: {}", source_id)
287        };
288        config.insert(name, source);
289    }
290
291    Ok(VendorConfig { source: config })
292}
293
294fn cp_sources(
295    src: &Path,
296    paths: &[PathBuf],
297    dst: &Path,
298    cksums: &mut BTreeMap<String, String>,
299) -> CargoResult<()> {
300    for p in paths {
301        let relative = p.strip_prefix(&src).unwrap();
302
303        match relative.to_str() {
304            // Skip git config files as they're not relevant to builds most of
305            // the time and if we respect them (e.g.  in git) then it'll
306            // probably mess with the checksums when a vendor dir is checked
307            // into someone else's source control
308            Some(".gitattributes") | Some(".gitignore") | Some(".git") => continue,
309
310            // Temporary Cargo files
311            Some(".cargo-ok") => continue,
312
313            // Skip patch-style orig/rej files. Published crates on crates.io
314            // have `Cargo.toml.orig` which we don't want to use here and
315            // otherwise these are rarely used as part of the build process.
316            Some(filename) => {
317                if filename.ends_with(".orig") || filename.ends_with(".rej") {
318                    continue;
319                }
320            }
321            _ => {}
322        };
323
324        // Join pathname components individually to make sure that the joined
325        // path uses the correct directory separators everywhere, since
326        // `relative` may use Unix-style and `dst` may require Windows-style
327        // backslashes.
328        let dst = relative
329            .iter()
330            .fold(dst.to_owned(), |acc, component| acc.join(&component));
331
332        paths::create_dir_all(dst.parent().unwrap())?;
333
334        fs::copy(&p, &dst)
335            .chain_err(|| format!("failed to copy `{}` to `{}`", p.display(), dst.display()))?;
336        let cksum = Sha256::new().update_path(dst)?.finish_hex();
337        cksums.insert(relative.to_str().unwrap().replace("\\", "/"), cksum);
338    }
339    Ok(())
340}