Skip to content

Fix: cargo vendor can't handle duplicates. #13271

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Open
wants to merge 1 commit into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 2 additions & 1 deletion src/bin/cargo/commands/vendor.rs
Original file line number Diff line number Diff line change
Expand Up @@ -32,7 +32,7 @@ pub fn cli() -> Command {
"versioned-dirs",
"Always include version in subdir name",
))
.arg(unsupported("no-merge-sources"))
.arg(flag("no-merge-sources", "Keep sources separate"))
.arg(unsupported("relative-path"))
.arg(unsupported("only-git-deps"))
.arg(unsupported("disallow-duplicates"))
Expand Down Expand Up @@ -79,6 +79,7 @@ pub fn exec(gctx: &mut GlobalContext, args: &ArgMatches) -> CliResult {
.unwrap_or_default()
.cloned()
.collect(),
no_merge_sources: args.flag("no-merge-sources"),
},
)?;
Ok(())
Expand Down
118 changes: 110 additions & 8 deletions src/cargo/ops/vendor.rs
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
use crate::core::package::MANIFEST_PREAMBLE;
use crate::core::shell::Verbosity;
use crate::core::{GitReference, Package, Workspace};
use crate::core::{GitReference, Package, SourceId, Workspace};
use crate::ops;
use crate::sources::path::PathSource;
use crate::sources::CRATES_IO_REGISTRY;
Expand All @@ -9,18 +9,23 @@ use crate::util::{try_canonicalize, CargoResult, GlobalContext};
use anyhow::{bail, Context as _};
use cargo_util::{paths, Sha256};
use serde::Serialize;
use std::collections::hash_map::DefaultHasher;
use std::collections::HashSet;
use std::collections::{BTreeMap, BTreeSet, HashMap};
use std::ffi::OsStr;
use std::fs::{self, File, OpenOptions};
use std::hash::Hasher;
use std::io::{Read, Write};
use std::path::{Path, PathBuf};

const SOURCES_FILE_NAME: &str = ".sources";

pub struct VendorOptions<'a> {
pub no_delete: bool,
pub versioned_dirs: bool,
pub destination: &'a Path,
pub extra: Vec<PathBuf>,
pub no_merge_sources: bool,
}

pub fn vendor(ws: &Workspace<'_>, opts: &VendorOptions<'_>) -> CargoResult<()> {
Expand Down Expand Up @@ -84,8 +89,16 @@ fn sync(
let canonical_destination = try_canonicalize(opts.destination);
let canonical_destination = canonical_destination.as_deref().unwrap_or(opts.destination);
let dest_dir_already_exists = canonical_destination.exists();
let merge_sources = !opts.no_merge_sources;
let sources_file = canonical_destination.join(SOURCES_FILE_NAME);

paths::create_dir_all(&canonical_destination)?;

if !merge_sources {
let mut file = File::create(sources_file)?;
file.write_all(serde_json::json!([]).to_string().as_bytes())?;
}

let mut to_remove = HashSet::new();
if !opts.no_delete {
for entry in canonical_destination.read_dir()? {
Expand Down Expand Up @@ -172,8 +185,9 @@ fn sync(
let mut versions = HashMap::new();
for id in ids.keys() {
let map = versions.entry(id.name()).or_insert_with(BTreeMap::default);
if let Some(prev) = map.get(&id.version()) {
bail!(

match map.get(&id.version()) {
Some(prev) if merge_sources => bail!(
"found duplicate version of package `{} v{}` \
vendored from two sources:\n\
\n\
Expand All @@ -183,7 +197,8 @@ fn sync(
id.version(),
prev,
id.source_id()
);
),
_ => {}
}
map.insert(id.version(), id.source_id());
}
Expand All @@ -207,7 +222,17 @@ fn sync(
};

sources.insert(id.source_id());
let dst = canonical_destination.join(&dst_name);
let source_dir = if merge_sources {
PathBuf::from(canonical_destination).clone()
} else {
PathBuf::from(canonical_destination).join(source_id_to_dir_name(id.source_id()))
};
if sources.insert(id.source_id()) && !merge_sources {
if fs::create_dir_all(&source_dir).is_err() {
panic!("failed to create: `{}`", source_dir.display())
}
}
let dst = source_dir.join(&dst_name);
to_remove.remove(&dst);
let cksum = dst.join(".cargo-checksum.json");
if dir_has_version_suffix && cksum.exists() {
Expand Down Expand Up @@ -244,6 +269,31 @@ fn sync(
}
}

if !merge_sources {
let sources_file = PathBuf::from(canonical_destination).join(SOURCES_FILE_NAME);
let file = File::open(&sources_file)?;
let mut new_sources: BTreeSet<String> = sources
.iter()
.map(|src_id| source_id_to_dir_name(*src_id))
.collect();
let old_sources: BTreeSet<String> = serde_json::from_reader::<_, BTreeSet<String>>(file)?
.difference(&new_sources)
.map(|e| e.clone())
.collect();
for dir_name in old_sources {
let path = PathBuf::from(canonical_destination).join(dir_name.clone());
if path.is_dir() {
if path.read_dir()?.next().is_none() {
fs::remove_dir(path)?;
} else {
new_sources.insert(dir_name.clone());
}
}
}
let file = File::create(sources_file)?;
serde_json::to_writer(file, &new_sources)?;
}

// add our vendored source
let mut config = BTreeMap::new();

Expand All @@ -259,16 +309,32 @@ fn sync(
source_id.without_precise().as_url().to_string()
};

let replace_name = if !merge_sources {
format!("vendor+{}", name)
} else {
merged_source_name.to_string()
};

if !merge_sources {
let src_id_string = source_id_to_dir_name(source_id);
let src_dir = PathBuf::from(canonical_destination).join(src_id_string.clone());
let string = src_dir.to_str().unwrap().to_string();
config.insert(
replace_name.clone(),
VendorSource::Directory { directory: string },
);
}

let source = if source_id.is_crates_io() {
VendorSource::Registry {
registry: None,
replace_with: merged_source_name.to_string(),
replace_with: replace_name,
}
} else if source_id.is_remote_registry() {
let registry = source_id.url().to_string();
VendorSource::Registry {
registry: Some(registry),
replace_with: merged_source_name.to_string(),
replace_with: replace_name,
}
} else if source_id.is_git() {
let mut branch = None;
Expand All @@ -287,7 +353,7 @@ fn sync(
branch,
tag,
rev,
replace_with: merged_source_name.to_string(),
replace_with: replace_name,
}
} else {
panic!("Invalid source ID: {}", source_id)
Expand Down Expand Up @@ -396,6 +462,42 @@ fn cp_sources(
Ok(())
}

fn source_id_to_dir_name(src_id: SourceId) -> String {
let src_type = if src_id.is_registry() {
"registry"
} else if src_id.is_git() {
"git"
} else {
panic!()
};
let mut hasher = DefaultHasher::new();
src_id.stable_hash(Path::new(""), &mut hasher);
let src_hash = hasher.finish();
let mut bytes = [0; 8];
for i in 0..7 {
bytes[i] = (src_hash >> i * 8) as u8
}
format!("{}-{}", src_type, hex(&bytes))
}

fn hex(bytes: &[u8]) -> String {
let mut s = String::with_capacity(bytes.len() * 2);
for &byte in bytes {
s.push(hex((byte >> 4) & 0xf));
s.push(hex((byte >> 0) & 0xf));
}

return s;

fn hex(b: u8) -> char {
if b < 10 {
(b'0' + b) as char
} else {
(b'a' + b - 10) as char
}
}
}

fn copy_and_checksum<T: Read>(
dst_path: &Path,
dst_opts: &mut OpenOptions,
Expand Down
36 changes: 19 additions & 17 deletions tests/testsuite/cargo_vendor/help/stdout.term.svg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
73 changes: 73 additions & 0 deletions tests/testsuite/vendor.rs
Original file line number Diff line number Diff line change
Expand Up @@ -1151,3 +1151,76 @@ fn vendor_crate_with_ws_inherit() {
.with_stderr_contains("[..]foo/vendor/bar/src/lib.rs[..]")
.run();
}

#[cargo_test]
fn replace_section() {
let p = project()
.file(
"Cargo.toml",
r#"
[package]
name = "foo"
version = "0.1.0"

[dependencies]
libc = "0.2.43"
[replace."libc:0.2.43"]
git = "https://github.com/rust-lang/libc"
rev = "add1a320b4e1b454794a034e3f4218f877c393fc"
"#,
)
.file("src/lib.rs", "")
.build();

Package::new("libc", "0.2.43").publish();

let output = p
.cargo("vendor --no-merge-sources")
.exec_with_output()
.unwrap();
p.change_file(".cargo/config", &String::from_utf8(output.stdout).unwrap());
assert!(p.root().join("vendor/.sources").exists());
p.cargo("check").run();
}

#[cargo_test]
fn switch_merged_source() {
let p = project()
.file(
"Cargo.toml",
r#"
[package]
name = "foo"
version = "0.1.0"
[dependencies]
log = "0.3.5"
"#,
)
.file("src/lib.rs", "")
.build();

Package::new("log", "0.3.5").publish();

// Start with multi sources
let output = p
.cargo("vendor --no-merge-sources")
.exec_with_output()
.unwrap();
assert!(p.root().join("vendor/.sources").exists());
p.change_file(".cargo/config", &String::from_utf8(output.stdout).unwrap());
p.cargo("check").run();

// Switch to merged source
let output = p.cargo("vendor").exec_with_output().unwrap();
p.change_file(".cargo/config", &String::from_utf8(output.stdout).unwrap());
p.cargo("check").run();

// Switch back to multi sources
let output = p
.cargo("vendor --no-merge-sources")
.exec_with_output()
.unwrap();
p.change_file(".cargo/config", &String::from_utf8(output.stdout).unwrap());
assert!(p.root().join("vendor/.sources").exists());
p.cargo("check").run();
}