Skip to content

Commit c8524eb

Browse files
junjihashimotoim-0
authored andcommitted
Add no-merge-sources of cargo vendor to handle duplicates
1 parent 74c051f commit c8524eb

File tree

4 files changed

+206
-29
lines changed

4 files changed

+206
-29
lines changed

src/bin/cargo/commands/vendor.rs

Lines changed: 2 additions & 1 deletion
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"))
@@ -82,6 +82,7 @@ pub fn exec(gctx: &mut GlobalContext, args: &ArgMatches) -> CliResult {
8282
.cloned()
8383
.collect(),
8484
respect_source_config,
85+
no_merge_sources: args.flag("no-merge-sources"),
8586
},
8687
)?;
8788
Ok(())

src/cargo/ops/vendor.rs

Lines changed: 110 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,5 @@
11
use crate::core::shell::Verbosity;
2-
use crate::core::SourceId;
3-
use crate::core::{GitReference, Package, Workspace};
2+
use crate::core::{GitReference, Package, SourceId, Workspace};
43
use crate::ops;
54
use crate::sources::path::PathSource;
65
use crate::sources::PathEntry;
@@ -11,19 +10,24 @@ use crate::util::{try_canonicalize, CargoResult, GlobalContext};
1110
use anyhow::{bail, Context as _};
1211
use cargo_util::{paths, Sha256};
1312
use serde::Serialize;
13+
use std::collections::hash_map::DefaultHasher;
1414
use std::collections::HashSet;
1515
use std::collections::{BTreeMap, BTreeSet, HashMap};
1616
use std::ffi::OsStr;
1717
use std::fs::{self, File, OpenOptions};
18+
use std::hash::Hasher;
1819
use std::io::{Read, Write};
1920
use std::path::{Path, PathBuf};
2021

22+
const SOURCES_FILE_NAME: &str = ".sources";
23+
2124
pub struct VendorOptions<'a> {
2225
pub no_delete: bool,
2326
pub versioned_dirs: bool,
2427
pub destination: &'a Path,
2528
pub extra: Vec<PathBuf>,
2629
pub respect_source_config: bool,
30+
pub no_merge_sources: bool,
2731
}
2832

2933
pub fn vendor(ws: &Workspace<'_>, opts: &VendorOptions<'_>) -> CargoResult<()> {
@@ -114,8 +118,16 @@ fn sync(
114118
let canonical_destination = try_canonicalize(opts.destination);
115119
let canonical_destination = canonical_destination.as_deref().unwrap_or(opts.destination);
116120
let dest_dir_already_exists = canonical_destination.exists();
121+
let merge_sources = !opts.no_merge_sources;
122+
let sources_file = canonical_destination.join(SOURCES_FILE_NAME);
117123

118124
paths::create_dir_all(&canonical_destination)?;
125+
126+
if !merge_sources {
127+
let mut file = File::create(sources_file)?;
128+
file.write_all(serde_json::json!([]).to_string().as_bytes())?;
129+
}
130+
119131
let mut to_remove = HashSet::new();
120132
if !opts.no_delete {
121133
for entry in canonical_destination.read_dir()? {
@@ -215,8 +227,9 @@ fn sync(
215227
let mut versions = HashMap::new();
216228
for id in ids.keys() {
217229
let map = versions.entry(id.name()).or_insert_with(BTreeMap::default);
218-
if let Some(prev) = map.get(&id.version()) {
219-
bail!(
230+
231+
match map.get(&id.version()) {
232+
Some(prev) if merge_sources => bail!(
220233
"found duplicate version of package `{} v{}` \
221234
vendored from two sources:\n\
222235
\n\
@@ -226,7 +239,8 @@ fn sync(
226239
id.version(),
227240
prev,
228241
id.source_id()
229-
);
242+
),
243+
_ => {}
230244
}
231245
map.insert(id.version(), id.source_id());
232246
}
@@ -247,7 +261,17 @@ fn sync(
247261
};
248262

249263
sources.insert(id.source_id());
250-
let dst = canonical_destination.join(&dst_name);
264+
let source_dir = if merge_sources {
265+
PathBuf::from(canonical_destination).clone()
266+
} else {
267+
PathBuf::from(canonical_destination).join(source_id_to_dir_name(id.source_id()))
268+
};
269+
if sources.insert(id.source_id()) && !merge_sources {
270+
if fs::create_dir_all(&source_dir).is_err() {
271+
panic!("failed to create: `{}`", source_dir.display())
272+
}
273+
}
274+
let dst = source_dir.join(&dst_name);
251275
to_remove.remove(&dst);
252276
let cksum = dst.join(".cargo-checksum.json");
253277
// Registries are the only immutable sources,
@@ -286,6 +310,31 @@ fn sync(
286310
}
287311
}
288312

313+
if !merge_sources {
314+
let sources_file = PathBuf::from(canonical_destination).join(SOURCES_FILE_NAME);
315+
let file = File::open(&sources_file)?;
316+
let mut new_sources: BTreeSet<String> = sources
317+
.iter()
318+
.map(|src_id| source_id_to_dir_name(*src_id))
319+
.collect();
320+
let old_sources: BTreeSet<String> = serde_json::from_reader::<_, BTreeSet<String>>(file)?
321+
.difference(&new_sources)
322+
.map(|e| e.clone())
323+
.collect();
324+
for dir_name in old_sources {
325+
let path = PathBuf::from(canonical_destination).join(dir_name.clone());
326+
if path.is_dir() {
327+
if path.read_dir()?.next().is_none() {
328+
fs::remove_dir(path)?;
329+
} else {
330+
new_sources.insert(dir_name.clone());
331+
}
332+
}
333+
}
334+
let file = File::create(sources_file)?;
335+
serde_json::to_writer(file, &new_sources)?;
336+
}
337+
289338
// add our vendored source
290339
let mut config = BTreeMap::new();
291340

@@ -301,16 +350,32 @@ fn sync(
301350
source_id.without_precise().as_url().to_string()
302351
};
303352

353+
let replace_name = if !merge_sources {
354+
format!("vendor+{}", name)
355+
} else {
356+
merged_source_name.to_string()
357+
};
358+
359+
if !merge_sources {
360+
let src_id_string = source_id_to_dir_name(source_id);
361+
let src_dir = PathBuf::from(canonical_destination).join(src_id_string.clone());
362+
let string = src_dir.to_str().unwrap().to_string();
363+
config.insert(
364+
replace_name.clone(),
365+
VendorSource::Directory { directory: string },
366+
);
367+
}
368+
304369
let source = if source_id.is_crates_io() {
305370
VendorSource::Registry {
306371
registry: None,
307-
replace_with: merged_source_name.to_string(),
372+
replace_with: replace_name,
308373
}
309374
} else if source_id.is_remote_registry() {
310375
let registry = source_id.url().to_string();
311376
VendorSource::Registry {
312377
registry: Some(registry),
313-
replace_with: merged_source_name.to_string(),
378+
replace_with: replace_name,
314379
}
315380
} else if source_id.is_git() {
316381
let mut branch = None;
@@ -329,7 +394,7 @@ fn sync(
329394
branch,
330395
tag,
331396
rev,
332-
replace_with: merged_source_name.to_string(),
397+
replace_with: replace_name,
333398
}
334399
} else {
335400
panic!("Invalid source ID: {}", source_id)
@@ -558,6 +623,42 @@ fn prepare_toml_for_vendor(
558623
Ok(me)
559624
}
560625

626+
fn source_id_to_dir_name(src_id: SourceId) -> String {
627+
let src_type = if src_id.is_registry() {
628+
"registry"
629+
} else if src_id.is_git() {
630+
"git"
631+
} else {
632+
panic!()
633+
};
634+
let mut hasher = DefaultHasher::new();
635+
src_id.stable_hash(Path::new(""), &mut hasher);
636+
let src_hash = hasher.finish();
637+
let mut bytes = [0; 8];
638+
for i in 0..7 {
639+
bytes[i] = (src_hash >> i * 8) as u8
640+
}
641+
format!("{}-{}", src_type, hex(&bytes))
642+
}
643+
644+
fn hex(bytes: &[u8]) -> String {
645+
let mut s = String::with_capacity(bytes.len() * 2);
646+
for &byte in bytes {
647+
s.push(hex((byte >> 4) & 0xf));
648+
s.push(hex((byte >> 0) & 0xf));
649+
}
650+
651+
return s;
652+
653+
fn hex(b: u8) -> char {
654+
if b < 10 {
655+
(b'0' + b) as char
656+
} else {
657+
(b'a' + b - 10) as char
658+
}
659+
}
660+
}
661+
561662
fn copy_and_checksum<T: Read>(
562663
dst_path: &Path,
563664
dst_opts: &mut OpenOptions,

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

Lines changed: 21 additions & 19 deletions
Loading

tests/testsuite/vendor.rs

Lines changed: 73 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2067,3 +2067,76 @@ Caused by:
20672067
"#]])
20682068
.run();
20692069
}
2070+
2071+
#[cargo_test]
2072+
fn replace_section() {
2073+
let p = project()
2074+
.file(
2075+
"Cargo.toml",
2076+
r#"
2077+
[package]
2078+
name = "foo"
2079+
version = "0.1.0"
2080+
2081+
[dependencies]
2082+
libc = "0.2.43"
2083+
[replace."libc:0.2.43"]
2084+
git = "https://github.com/rust-lang/libc"
2085+
rev = "add1a320b4e1b454794a034e3f4218f877c393fc"
2086+
"#,
2087+
)
2088+
.file("src/lib.rs", "")
2089+
.build();
2090+
2091+
Package::new("libc", "0.2.43").publish();
2092+
2093+
let output = p
2094+
.cargo("vendor --no-merge-sources")
2095+
.exec_with_output()
2096+
.unwrap();
2097+
p.change_file(".cargo/config", &String::from_utf8(output.stdout).unwrap());
2098+
assert!(p.root().join("vendor/.sources").exists());
2099+
p.cargo("check").run();
2100+
}
2101+
2102+
#[cargo_test]
2103+
fn switch_merged_source() {
2104+
let p = project()
2105+
.file(
2106+
"Cargo.toml",
2107+
r#"
2108+
[package]
2109+
name = "foo"
2110+
version = "0.1.0"
2111+
[dependencies]
2112+
log = "0.3.5"
2113+
"#,
2114+
)
2115+
.file("src/lib.rs", "")
2116+
.build();
2117+
2118+
Package::new("log", "0.3.5").publish();
2119+
2120+
// Start with multi sources
2121+
let output = p
2122+
.cargo("vendor --no-merge-sources")
2123+
.exec_with_output()
2124+
.unwrap();
2125+
assert!(p.root().join("vendor/.sources").exists());
2126+
p.change_file(".cargo/config", &String::from_utf8(output.stdout).unwrap());
2127+
p.cargo("check").run();
2128+
2129+
// Switch to merged source
2130+
let output = p.cargo("vendor").exec_with_output().unwrap();
2131+
p.change_file(".cargo/config", &String::from_utf8(output.stdout).unwrap());
2132+
p.cargo("check").run();
2133+
2134+
// Switch back to multi sources
2135+
let output = p
2136+
.cargo("vendor --no-merge-sources")
2137+
.exec_with_output()
2138+
.unwrap();
2139+
p.change_file(".cargo/config", &String::from_utf8(output.stdout).unwrap());
2140+
assert!(p.root().join("vendor/.sources").exists());
2141+
p.cargo("check").run();
2142+
}

0 commit comments

Comments
 (0)