Skip to content

Commit edf2ba7

Browse files
Add no-merge-sources of cargo vendor to handle duplicates
1 parent 312ad26 commit edf2ba7

File tree

4 files changed

+196
-9
lines changed

4 files changed

+196
-9
lines changed

src/bin/cargo/commands/vendor.rs

+2-1
Original file line numberDiff line numberDiff line change
@@ -32,7 +32,7 @@ pub fn cli() -> Command {
3232
"versioned-dirs",
3333
"Always include version in subdir name",
3434
))
35-
.arg(unsupported("no-merge-sources"))
35+
.arg(flag("no-merge-sources", "Keep sources separate"))
3636
.arg(unsupported("relative-path"))
3737
.arg(unsupported("only-git-deps"))
3838
.arg(unsupported("disallow-duplicates"))
@@ -79,6 +79,7 @@ pub fn exec(config: &mut Config, args: &ArgMatches) -> CliResult {
7979
.unwrap_or_default()
8080
.cloned()
8181
.collect(),
82+
no_merge_sources: args.flag("no-merge-sources"),
8283
},
8384
)?;
8485
Ok(())

src/cargo/ops/vendor.rs

+120-8
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
use crate::core::package::MANIFEST_PREAMBLE;
22
use crate::core::shell::Verbosity;
3-
use crate::core::{GitReference, Package, Workspace};
3+
use crate::core::{GitReference, Package, SourceId, Workspace};
44
use crate::ops;
55
use crate::sources::path::PathSource;
66
use crate::sources::CRATES_IO_REGISTRY;
@@ -9,18 +9,23 @@ use crate::util::{try_canonicalize, CargoResult, Config};
99
use anyhow::{bail, Context as _};
1010
use cargo_util::{paths, Sha256};
1111
use serde::Serialize;
12+
use std::collections::hash_map::DefaultHasher;
1213
use std::collections::HashSet;
1314
use std::collections::{BTreeMap, BTreeSet, HashMap};
1415
use std::ffi::OsStr;
1516
use std::fs::{self, File, OpenOptions};
17+
use std::hash::Hasher;
1618
use std::io::{Read, Write};
1719
use std::path::{Path, PathBuf};
1820

21+
const SOURCES_FILE_NAME: &str = ".sources";
22+
1923
pub struct VendorOptions<'a> {
2024
pub no_delete: bool,
2125
pub versioned_dirs: bool,
2226
pub destination: &'a Path,
2327
pub extra: Vec<PathBuf>,
28+
pub no_merge_sources: bool,
2429
}
2530

2631
pub fn vendor(ws: &Workspace<'_>, opts: &VendorOptions<'_>) -> CargoResult<()> {
@@ -88,8 +93,16 @@ fn sync(
8893
let canonical_destination = try_canonicalize(opts.destination);
8994
let canonical_destination = canonical_destination.as_deref().unwrap_or(opts.destination);
9095
let dest_dir_already_exists = canonical_destination.exists();
96+
let merge_sources = !opts.no_merge_sources;
97+
let sources_file = canonical_destination.join(SOURCES_FILE_NAME);
9198

9299
paths::create_dir_all(&canonical_destination)?;
100+
101+
if !merge_sources {
102+
let mut file = File::create(sources_file)?;
103+
file.write_all(serde_json::json!([]).to_string().as_bytes())?;
104+
}
105+
93106
let mut to_remove = HashSet::new();
94107
if !opts.no_delete {
95108
for entry in canonical_destination.read_dir()? {
@@ -176,8 +189,9 @@ fn sync(
176189
let mut versions = HashMap::new();
177190
for id in ids.keys() {
178191
let map = versions.entry(id.name()).or_insert_with(BTreeMap::default);
179-
if let Some(prev) = map.get(&id.version()) {
180-
bail!(
192+
193+
match map.get(&id.version()) {
194+
Some(prev) if merge_sources => bail!(
181195
"found duplicate version of package `{} v{}` \
182196
vendored from two sources:\n\
183197
\n\
@@ -187,7 +201,8 @@ fn sync(
187201
id.version(),
188202
prev,
189203
id.source_id()
190-
);
204+
),
205+
_ => {}
191206
}
192207
map.insert(id.version(), id.source_id());
193208
}
@@ -211,7 +226,17 @@ fn sync(
211226
};
212227

213228
sources.insert(id.source_id());
214-
let dst = canonical_destination.join(&dst_name);
229+
let source_dir = if merge_sources {
230+
PathBuf::from(canonical_destination).clone()
231+
} else {
232+
PathBuf::from(canonical_destination).join(source_id_to_dir_name(id.source_id()))
233+
};
234+
if sources.insert(id.source_id()) && !merge_sources {
235+
if fs::create_dir_all(&source_dir).is_err() {
236+
panic!("failed to create: `{}`", source_dir.display())
237+
}
238+
}
239+
let dst = source_dir.join(&dst_name);
215240
to_remove.remove(&dst);
216241
let cksum = dst.join(".cargo-checksum.json");
217242
if dir_has_version_suffix && cksum.exists() {
@@ -248,6 +273,31 @@ fn sync(
248273
}
249274
}
250275

276+
if !merge_sources {
277+
let sources_file = PathBuf::from(canonical_destination).join(SOURCES_FILE_NAME);
278+
let file = File::open(&sources_file)?;
279+
let mut new_sources: BTreeSet<String> = sources
280+
.iter()
281+
.map(|src_id| source_id_to_dir_name(*src_id))
282+
.collect();
283+
let old_sources: BTreeSet<String> = serde_json::from_reader::<_, BTreeSet<String>>(file)?
284+
.difference(&new_sources)
285+
.map(|e| e.clone())
286+
.collect();
287+
for dir_name in old_sources {
288+
let path = PathBuf::from(canonical_destination).join(dir_name.clone());
289+
if path.is_dir() {
290+
if path.read_dir()?.next().is_none() {
291+
fs::remove_dir(path)?;
292+
} else {
293+
new_sources.insert(dir_name.clone());
294+
}
295+
}
296+
}
297+
let file = File::create(sources_file)?;
298+
serde_json::to_writer(file, &new_sources)?;
299+
}
300+
251301
// add our vendored source
252302
let mut config = BTreeMap::new();
253303

@@ -263,16 +313,42 @@ fn sync(
263313
source_id.without_precise().as_url().to_string()
264314
};
265315

316+
let replace_name = if !merge_sources {
317+
format!("vendor+{}", name)
318+
} else {
319+
merged_source_name.to_string()
320+
};
321+
322+
if !merge_sources {
323+
let src_id_string = source_id_to_dir_name(source_id);
324+
let src_dir = PathBuf::from(canonical_destination).join(src_id_string.clone());
325+
match src_dir.to_str() {
326+
Some(s) => {
327+
let string = s.to_string();
328+
config.insert(
329+
replace_name.clone(),
330+
VendorSource::Directory { directory: string },
331+
);
332+
}
333+
None => println!("PathBuf of src_dir contains invalid UTF-8 characters"),
334+
}
335+
}
336+
337+
// if source id is a path, skip the source replacement
338+
if source_id.is_path() {
339+
continue;
340+
}
341+
266342
let source = if source_id.is_crates_io() {
267343
VendorSource::Registry {
268344
registry: None,
269-
replace_with: merged_source_name.to_string(),
345+
replace_with: replace_name,
270346
}
271347
} else if source_id.is_remote_registry() {
272348
let registry = source_id.url().to_string();
273349
VendorSource::Registry {
274350
registry: Some(registry),
275-
replace_with: merged_source_name.to_string(),
351+
replace_with: replace_name,
276352
}
277353
} else if source_id.is_git() {
278354
let mut branch = None;
@@ -291,7 +367,7 @@ fn sync(
291367
branch,
292368
tag,
293369
rev,
294-
replace_with: merged_source_name.to_string(),
370+
replace_with: replace_name,
295371
}
296372
} else {
297373
panic!("Invalid source ID: {}", source_id)
@@ -400,6 +476,42 @@ fn cp_sources(
400476
Ok(())
401477
}
402478

479+
fn source_id_to_dir_name(src_id: SourceId) -> String {
480+
let src_type = if src_id.is_registry() {
481+
"registry"
482+
} else if src_id.is_git() {
483+
"git"
484+
} else {
485+
panic!()
486+
};
487+
let mut hasher = DefaultHasher::new();
488+
src_id.stable_hash(Path::new(""), &mut hasher);
489+
let src_hash = hasher.finish();
490+
let mut bytes = [0; 8];
491+
for i in 0..7 {
492+
bytes[i] = (src_hash >> i * 8) as u8
493+
}
494+
format!("{}-{}", src_type, hex(&bytes))
495+
}
496+
497+
fn hex(bytes: &[u8]) -> String {
498+
let mut s = String::with_capacity(bytes.len() * 2);
499+
for &byte in bytes {
500+
s.push(hex((byte >> 4) & 0xf));
501+
s.push(hex((byte >> 0) & 0xf));
502+
}
503+
504+
return s;
505+
506+
fn hex(b: u8) -> char {
507+
if b < 10 {
508+
(b'0' + b) as char
509+
} else {
510+
(b'a' + b - 10) as char
511+
}
512+
}
513+
}
514+
403515
fn copy_and_checksum<T: Read>(
404516
dst_path: &Path,
405517
dst_opts: &mut OpenOptions,

tests/testsuite/cargo_vendor/help/stdout.log

+1
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@ Options:
1010
-s, --sync <TOML> Additional `Cargo.toml` to sync and vendor
1111
--respect-source-config Respect `[source]` config in `.cargo/config`
1212
--versioned-dirs Always include version in subdir name
13+
--no-merge-sources Keep sources separate
1314
-v, --verbose... Use verbose output (-vv very verbose/build.rs output)
1415
-q, --quiet Do not print cargo log messages
1516
--color <WHEN> Coloring: auto, always, never

tests/testsuite/vendor.rs

+73
Original file line numberDiff line numberDiff line change
@@ -1151,3 +1151,76 @@ fn vendor_crate_with_ws_inherit() {
11511151
.with_stderr_contains("[..]foo/vendor/bar/src/lib.rs[..]")
11521152
.run();
11531153
}
1154+
1155+
#[cargo_test]
1156+
fn replace_section() {
1157+
let p = project()
1158+
.file(
1159+
"Cargo.toml",
1160+
r#"
1161+
[package]
1162+
name = "foo"
1163+
version = "0.1.0"
1164+
1165+
[dependencies]
1166+
libc = "0.2.43"
1167+
[replace."libc:0.2.43"]
1168+
git = "https://github.com/rust-lang/libc"
1169+
rev = "add1a320b4e1b454794a034e3f4218f877c393fc"
1170+
"#,
1171+
)
1172+
.file("src/lib.rs", "")
1173+
.build();
1174+
1175+
Package::new("libc", "0.2.43").publish();
1176+
1177+
let output = p
1178+
.cargo("vendor --no-merge-sources")
1179+
.exec_with_output()
1180+
.unwrap();
1181+
p.change_file(".cargo/config", &String::from_utf8(output.stdout).unwrap());
1182+
assert!(p.root().join("vendor/.sources").exists());
1183+
p.cargo("check").run();
1184+
}
1185+
1186+
#[cargo_test]
1187+
fn switch_merged_source() {
1188+
let p = project()
1189+
.file(
1190+
"Cargo.toml",
1191+
r#"
1192+
[package]
1193+
name = "foo"
1194+
version = "0.1.0"
1195+
[dependencies]
1196+
log = "0.3.5"
1197+
"#,
1198+
)
1199+
.file("src/lib.rs", "")
1200+
.build();
1201+
1202+
Package::new("log", "0.3.5").publish();
1203+
1204+
// Start with multi sources
1205+
let output = p
1206+
.cargo("vendor --no-merge-sources")
1207+
.exec_with_output()
1208+
.unwrap();
1209+
assert!(p.root().join("vendor/.sources").exists());
1210+
p.change_file(".cargo/config", &String::from_utf8(output.stdout).unwrap());
1211+
p.cargo("check").run();
1212+
1213+
// Switch to merged source
1214+
let output = p.cargo("vendor").exec_with_output().unwrap();
1215+
p.change_file(".cargo/config", &String::from_utf8(output.stdout).unwrap());
1216+
p.cargo("check").run();
1217+
1218+
// Switch back to multi sources
1219+
let output = p
1220+
.cargo("vendor --no-merge-sources")
1221+
.exec_with_output()
1222+
.unwrap();
1223+
p.change_file(".cargo/config", &String::from_utf8(output.stdout).unwrap());
1224+
assert!(p.root().join("vendor/.sources").exists());
1225+
p.cargo("check").run();
1226+
}

0 commit comments

Comments
 (0)