Skip to content

Commit cc6b6c9

Browse files
committed
fix(package): Avoid multiple package list entries
To keep things simple, especially in getting a `Hash` implementation correct, I'm leveraging `unicase` for case-insensitive comparisons which is an existing dependency and I've been using for years on other projects. This also opens the door for us to add cross-platform compatibility hazard warnings about multiple paths that would write to the same location on a case insensitive file system. I held off on that because I assume we would want #12235 first. This does mean we can't test the "no manifest" case anymore because the one case (no pun intended) I knew of for hitting it is now gone.
1 parent 9b14e39 commit cc6b6c9

File tree

4 files changed

+62
-104
lines changed

4 files changed

+62
-104
lines changed

Cargo.lock

Lines changed: 1 addition & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

Cargo.toml

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -94,6 +94,7 @@ thiserror = "1.0.40"
9494
time = { version = "0.3", features = ["parsing", "formatting", "serde"] }
9595
toml = "0.7.0"
9696
toml_edit = "0.19.0"
97+
unicase = "2.6.0"
9798
unicode-width = "0.1.5"
9899
unicode-xid = "0.2.0"
99100
url = "2.2.2"
@@ -177,6 +178,7 @@ termcolor.workspace = true
177178
time.workspace = true
178179
toml.workspace = true
179180
toml_edit.workspace = true
181+
unicase.workspace = true
180182
unicode-width.workspace = true
181183
unicode-xid.workspace = true
182184
url.workspace = true

src/cargo/ops/cargo_package.rs

Lines changed: 58 additions & 43 deletions
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,7 @@ use flate2::{Compression, GzBuilder};
2525
use log::debug;
2626
use serde::Serialize;
2727
use tar::{Archive, Builder, EntryType, Header, HeaderMode};
28+
use unicase::Ascii as UncasedAscii;
2829

2930
pub struct PackageOpts<'cfg> {
3031
pub config: &'cfg Config,
@@ -227,70 +228,84 @@ fn build_ar_list(
227228
src_files: Vec<PathBuf>,
228229
vcs_info: Option<VcsInfo>,
229230
) -> CargoResult<Vec<ArchiveFile>> {
230-
let mut result = Vec::new();
231+
let mut result = HashMap::new();
231232
let root = pkg.root();
232233

233-
let mut manifest_found = false;
234-
for src_file in src_files {
235-
let rel_path = src_file.strip_prefix(&root)?.to_path_buf();
236-
check_filename(&rel_path, &mut ws.config().shell())?;
237-
let rel_str = rel_path
238-
.to_str()
239-
.ok_or_else(|| {
240-
anyhow::format_err!("non-utf8 path in source directory: {}", rel_path.display())
241-
})?
242-
.to_string();
234+
for src_file in &src_files {
235+
let rel_path = src_file.strip_prefix(&root)?;
236+
check_filename(rel_path, &mut ws.config().shell())?;
237+
let rel_str = rel_path.to_str().ok_or_else(|| {
238+
anyhow::format_err!("non-utf8 path in source directory: {}", rel_path.display())
239+
})?;
243240
match rel_str.as_ref() {
244-
"Cargo.toml" |
245-
// normalize for case insensitive filesystems (like on Windows)
246-
"cargo.toml" => {
247-
manifest_found = true;
248-
result.push(ArchiveFile {
249-
rel_path: PathBuf::from(ORIGINAL_MANIFEST_FILE),
250-
rel_str: ORIGINAL_MANIFEST_FILE.to_string(),
251-
contents: FileContents::OnDisk(src_file),
252-
});
253-
result.push(ArchiveFile {
254-
rel_path: PathBuf::from("Cargo.toml"),
255-
rel_str: "Cargo.toml".to_string(),
256-
contents: FileContents::Generated(GeneratedFile::Manifest),
257-
});
258-
}
259241
"Cargo.lock" => continue,
260242
VCS_INFO_FILE | ORIGINAL_MANIFEST_FILE => anyhow::bail!(
261243
"invalid inclusion of reserved file name {} in package source",
262244
rel_str
263245
),
264246
_ => {
265-
result.push(ArchiveFile {
266-
rel_path,
267-
rel_str,
268-
contents: FileContents::OnDisk(src_file),
269-
});
247+
result
248+
.entry(UncasedAscii::new(rel_str))
249+
.or_insert_with(Vec::new)
250+
.push(ArchiveFile {
251+
rel_path: rel_path.to_owned(),
252+
rel_str: rel_str.to_owned(),
253+
contents: FileContents::OnDisk(src_file.clone()),
254+
});
270255
}
271256
}
272257
}
273-
if !manifest_found {
258+
259+
// Ensure we normalize for case insensitive filesystems (like on Windows) by removing the
260+
// existing entry, regardless of case, and adding in with the correct case
261+
if result.remove(&UncasedAscii::new("Cargo.toml")).is_some() {
262+
result
263+
.entry(UncasedAscii::new(ORIGINAL_MANIFEST_FILE))
264+
.or_insert_with(Vec::new)
265+
.push(ArchiveFile {
266+
rel_path: PathBuf::from(ORIGINAL_MANIFEST_FILE),
267+
rel_str: ORIGINAL_MANIFEST_FILE.to_string(),
268+
contents: FileContents::OnDisk(pkg.manifest_path().to_owned()),
269+
});
270+
result
271+
.entry(UncasedAscii::new("Cargo.toml"))
272+
.or_insert_with(Vec::new)
273+
.push(ArchiveFile {
274+
rel_path: PathBuf::from("Cargo.toml"),
275+
rel_str: "Cargo.toml".to_string(),
276+
contents: FileContents::Generated(GeneratedFile::Manifest),
277+
});
278+
} else {
274279
ws.config().shell().warn(&format!(
275280
"no `Cargo.toml` file found when packaging `{}` (note the case of the file name).",
276281
pkg.name()
277282
))?;
278283
}
279284

280285
if pkg.include_lockfile() {
281-
result.push(ArchiveFile {
282-
rel_path: PathBuf::from("Cargo.lock"),
283-
rel_str: "Cargo.lock".to_string(),
284-
contents: FileContents::Generated(GeneratedFile::Lockfile),
285-
});
286+
let rel_str = "Cargo.lock";
287+
result
288+
.entry(UncasedAscii::new(rel_str))
289+
.or_insert_with(Vec::new)
290+
.push(ArchiveFile {
291+
rel_path: PathBuf::from(rel_str),
292+
rel_str: rel_str.to_string(),
293+
contents: FileContents::Generated(GeneratedFile::Lockfile),
294+
});
286295
}
287296
if let Some(vcs_info) = vcs_info {
288-
result.push(ArchiveFile {
289-
rel_path: PathBuf::from(VCS_INFO_FILE),
290-
rel_str: VCS_INFO_FILE.to_string(),
291-
contents: FileContents::Generated(GeneratedFile::VcsInfo(vcs_info)),
292-
});
293-
}
297+
let rel_str = VCS_INFO_FILE;
298+
result
299+
.entry(UncasedAscii::new(rel_str))
300+
.or_insert_with(Vec::new)
301+
.push(ArchiveFile {
302+
rel_path: PathBuf::from(rel_str),
303+
rel_str: rel_str.to_string(),
304+
contents: FileContents::Generated(GeneratedFile::VcsInfo(vcs_info)),
305+
});
306+
}
307+
308+
let mut result = result.into_values().flatten().collect();
294309
if let Some(license_file) = &pkg.manifest().metadata().license_file {
295310
let license_path = Path::new(license_file);
296311
let abs_file_path = paths::normalize_path(&pkg.root().join(license_path));

tests/testsuite/package.rs

Lines changed: 1 addition & 61 deletions
Original file line numberDiff line numberDiff line change
@@ -3042,64 +3042,6 @@ src/main.rs
30423042
);
30433043
}
30443044

3045-
#[cargo_test]
3046-
#[cfg(windows)] // windows is the platform that is most consistently configured for case insensitive filesystems
3047-
fn no_manifest_found() {
3048-
let p = project()
3049-
.file("src/main.rs", r#"fn main() { println!("hello"); }"#)
3050-
.file("src/bar.txt", "") // should be ignored when packaging
3051-
.build();
3052-
// Workaround `project()` making a `Cargo.toml` on our behalf
3053-
std::fs::remove_file(p.root().join("Cargo.toml")).unwrap();
3054-
std::fs::write(
3055-
p.root().join("CARGO.TOML"),
3056-
r#"
3057-
[package]
3058-
name = "foo"
3059-
version = "0.0.1"
3060-
authors = []
3061-
exclude = ["*.txt"]
3062-
license = "MIT"
3063-
description = "foo"
3064-
"#,
3065-
)
3066-
.unwrap();
3067-
3068-
p.cargo("package")
3069-
.with_stderr(
3070-
"\
3071-
[WARNING] manifest has no documentation[..]
3072-
See [..]
3073-
[WARNING] no `Cargo.toml` file found when packaging `foo` (note the case of the file name).
3074-
[PACKAGING] foo v0.0.1 ([CWD])
3075-
[VERIFYING] foo v0.0.1 ([CWD])
3076-
[COMPILING] foo v0.0.1 ([CWD][..])
3077-
[FINISHED] dev [unoptimized + debuginfo] target(s) in [..]
3078-
[PACKAGED] 3 files, [..] ([..] compressed)
3079-
",
3080-
)
3081-
.run();
3082-
assert!(p.root().join("target/package/foo-0.0.1.crate").is_file());
3083-
p.cargo("package -l")
3084-
.with_stdout(
3085-
"\
3086-
CARGO.TOML
3087-
Cargo.lock
3088-
src/main.rs
3089-
",
3090-
)
3091-
.run();
3092-
p.cargo("package").with_stdout("").run();
3093-
3094-
let f = File::open(&p.root().join("target/package/foo-0.0.1.crate")).unwrap();
3095-
validate_crate_contents(
3096-
f,
3097-
"foo-0.0.1.crate",
3098-
&["CARGO.TOML", "Cargo.lock", "src/main.rs"],
3099-
&[],
3100-
);
3101-
}
3102-
31033045
#[cargo_test]
31043046
#[cfg(target_os = "linux")] // linux is generally configured to be case sensitive
31053047
fn mixed_case() {
@@ -3128,7 +3070,7 @@ See [..]
31283070
[VERIFYING] foo v0.0.1 ([CWD])
31293071
[COMPILING] foo v0.0.1 ([CWD][..])
31303072
[FINISHED] dev [unoptimized + debuginfo] target(s) in [..]
3131-
[PACKAGED] 6 files, [..] ([..] compressed)
3073+
[PACKAGED] 4 files, [..] ([..] compressed)
31323074
",
31333075
)
31343076
.run();
@@ -3138,8 +3080,6 @@ See [..]
31383080
"\
31393081
Cargo.lock
31403082
Cargo.toml
3141-
Cargo.toml
3142-
Cargo.toml.orig
31433083
Cargo.toml.orig
31443084
src/main.rs
31453085
",

0 commit comments

Comments
 (0)