Skip to content

Commit f97a61c

Browse files
Add no-merge-sources of cargo vendor to handle duplicates
1 parent 2b302a5 commit f97a61c

File tree

4 files changed

+204
-26
lines changed

4 files changed

+204
-26
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(gctx: &mut GlobalContext, 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

+110-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, GlobalContext};
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<()> {
@@ -84,8 +89,16 @@ fn sync(
8489
let canonical_destination = try_canonicalize(opts.destination);
8590
let canonical_destination = canonical_destination.as_deref().unwrap_or(opts.destination);
8691
let dest_dir_already_exists = canonical_destination.exists();
92+
let merge_sources = !opts.no_merge_sources;
93+
let sources_file = canonical_destination.join(SOURCES_FILE_NAME);
8794

8895
paths::create_dir_all(&canonical_destination)?;
96+
97+
if !merge_sources {
98+
let mut file = File::create(sources_file)?;
99+
file.write_all(serde_json::json!([]).to_string().as_bytes())?;
100+
}
101+
89102
let mut to_remove = HashSet::new();
90103
if !opts.no_delete {
91104
for entry in canonical_destination.read_dir()? {
@@ -172,8 +185,9 @@ fn sync(
172185
let mut versions = HashMap::new();
173186
for id in ids.keys() {
174187
let map = versions.entry(id.name()).or_insert_with(BTreeMap::default);
175-
if let Some(prev) = map.get(&id.version()) {
176-
bail!(
188+
189+
match map.get(&id.version()) {
190+
Some(prev) if merge_sources => bail!(
177191
"found duplicate version of package `{} v{}` \
178192
vendored from two sources:\n\
179193
\n\
@@ -183,7 +197,8 @@ fn sync(
183197
id.version(),
184198
prev,
185199
id.source_id()
186-
);
200+
),
201+
_ => {}
187202
}
188203
map.insert(id.version(), id.source_id());
189204
}
@@ -207,7 +222,17 @@ fn sync(
207222
};
208223

209224
sources.insert(id.source_id());
210-
let dst = canonical_destination.join(&dst_name);
225+
let source_dir = if merge_sources {
226+
PathBuf::from(canonical_destination).clone()
227+
} else {
228+
PathBuf::from(canonical_destination).join(source_id_to_dir_name(id.source_id()))
229+
};
230+
if sources.insert(id.source_id()) && !merge_sources {
231+
if fs::create_dir_all(&source_dir).is_err() {
232+
panic!("failed to create: `{}`", source_dir.display())
233+
}
234+
}
235+
let dst = source_dir.join(&dst_name);
211236
to_remove.remove(&dst);
212237
let cksum = dst.join(".cargo-checksum.json");
213238
if dir_has_version_suffix && cksum.exists() {
@@ -244,6 +269,31 @@ fn sync(
244269
}
245270
}
246271

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

@@ -259,16 +309,32 @@ fn sync(
259309
source_id.without_precise().as_url().to_string()
260310
};
261311

312+
let replace_name = if !merge_sources {
313+
format!("vendor+{}", name)
314+
} else {
315+
merged_source_name.to_string()
316+
};
317+
318+
if !merge_sources {
319+
let src_id_string = source_id_to_dir_name(source_id);
320+
let src_dir = PathBuf::from(canonical_destination).join(src_id_string.clone());
321+
let string = src_dir.to_str().unwrap().to_string();
322+
config.insert(
323+
replace_name.clone(),
324+
VendorSource::Directory { directory: string },
325+
);
326+
}
327+
262328
let source = if source_id.is_crates_io() {
263329
VendorSource::Registry {
264330
registry: None,
265-
replace_with: merged_source_name.to_string(),
331+
replace_with: replace_name,
266332
}
267333
} else if source_id.is_remote_registry() {
268334
let registry = source_id.url().to_string();
269335
VendorSource::Registry {
270336
registry: Some(registry),
271-
replace_with: merged_source_name.to_string(),
337+
replace_with: replace_name,
272338
}
273339
} else if source_id.is_git() {
274340
let mut branch = None;
@@ -287,7 +353,7 @@ fn sync(
287353
branch,
288354
tag,
289355
rev,
290-
replace_with: merged_source_name.to_string(),
356+
replace_with: replace_name,
291357
}
292358
} else {
293359
panic!("Invalid source ID: {}", source_id)
@@ -396,6 +462,42 @@ fn cp_sources(
396462
Ok(())
397463
}
398464

465+
fn source_id_to_dir_name(src_id: SourceId) -> String {
466+
let src_type = if src_id.is_registry() {
467+
"registry"
468+
} else if src_id.is_git() {
469+
"git"
470+
} else {
471+
panic!()
472+
};
473+
let mut hasher = DefaultHasher::new();
474+
src_id.stable_hash(Path::new(""), &mut hasher);
475+
let src_hash = hasher.finish();
476+
let mut bytes = [0; 8];
477+
for i in 0..7 {
478+
bytes[i] = (src_hash >> i * 8) as u8
479+
}
480+
format!("{}-{}", src_type, hex(&bytes))
481+
}
482+
483+
fn hex(bytes: &[u8]) -> String {
484+
let mut s = String::with_capacity(bytes.len() * 2);
485+
for &byte in bytes {
486+
s.push(hex((byte >> 4) & 0xf));
487+
s.push(hex((byte >> 0) & 0xf));
488+
}
489+
490+
return s;
491+
492+
fn hex(b: u8) -> char {
493+
if b < 10 {
494+
(b'0' + b) as char
495+
} else {
496+
(b'a' + b - 10) as char
497+
}
498+
}
499+
}
500+
399501
fn copy_and_checksum<T: Read>(
400502
dst_path: &Path,
401503
dst_opts: &mut OpenOptions,

tests/testsuite/cargo_vendor/help/stdout.term.svg

+19-17
Loading

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)