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 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 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 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 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 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 format!("{}-{}", id.name(), id.version())
194 } else {
195 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 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 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 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 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 Some(".gitattributes") | Some(".gitignore") | Some(".git") => continue,
309
310 Some(".cargo-ok") => continue,
312
313 Some(filename) => {
317 if filename.ends_with(".orig") || filename.ends_with(".rej") {
318 continue;
319 }
320 }
321 _ => {}
322 };
323
324 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}