diff --git a/src/cargo/ops/cargo_package/mod.rs b/src/cargo/ops/cargo_package/mod.rs index 472acf41cb1..08077926c94 100644 --- a/src/cargo/ops/cargo_package/mod.rs +++ b/src/cargo/ops/cargo_package/mod.rs @@ -299,6 +299,21 @@ fn do_package<'a>( let ar_files = prepare_archive(ws, &pkg, &opts)?; if opts.list { + // Check file accessibility before listing to warn about potential issues + for ar_file in &ar_files { + if let FileContents::OnDisk(ref path) = ar_file.contents { + // Use the same File::open logic that the archiver uses to maintain sync + if let Err(e) = File::open(path) { + // Emit a warning but continue processing + let _ = ws.gctx().shell().warn(format!( + "cannot access `{}`: {}. `cargo package` will fail.", + path.display(), + e + )); + } + } + } + match opts.fmt { PackageMessageFormat::Human => { // While this form is called "human", @@ -894,8 +909,19 @@ fn tar( let mut header = Header::new_gnu(); match contents { FileContents::OnDisk(disk_path) => { - let mut file = File::open(&disk_path).with_context(|| { - format!("failed to open for archiving: `{}`", disk_path.display()) + let mut file = File::open(&disk_path).map_err(|e| { + // If this is a permission denied error, add a helpful hint + if e.kind() == std::io::ErrorKind::PermissionDenied { + anyhow::Error::from(e).context(format!( + "failed to open for archiving: `{}`\n\nNote: If this is a local artifact, add it to .gitignore or the 'exclude' list in Cargo.toml.", + disk_path.display() + )) + } else { + anyhow::Error::from(e).context(format!( + "failed to open for archiving: `{}`", + disk_path.display() + )) + } })?; let metadata = file.metadata().with_context(|| { format!("could not learn metadata for: `{}`", disk_path.display()) diff --git a/tests/testsuite/package.rs b/tests/testsuite/package.rs index 80d3811e96f..052c35a3230 100644 --- a/tests/testsuite/package.rs +++ b/tests/testsuite/package.rs @@ -7895,3 +7895,81 @@ fn package_dir_not_excluded_from_backups() { "CACHEDIR.TAG should exist in target directory to exclude it from backups" ); } + +// Unix-only test because Windows has different permission handling +#[cfg(unix)] +#[cargo_test] +fn unreadable_directory_error_message() { + use std::os::unix::fs::PermissionsExt; + + let p = project().file("src/lib.rs", "").build(); + + // Create an unreadable directory + let unreadable_dir = p.root().join("unreadable-dir"); + fs::create_dir(&unreadable_dir).unwrap(); + fs::set_permissions(&unreadable_dir, fs::Permissions::from_mode(0o000)).unwrap(); + + // Ensure cleanup happens even if test fails + struct Cleanup(std::path::PathBuf); + impl Drop for Cleanup { + fn drop(&mut self) { + let _ = fs::set_permissions(&self.0, fs::Permissions::from_mode(0o755)); + } + } + let _cleanup = Cleanup(unreadable_dir.clone()); + + // cargo package should fail with Permission Denied error and helpful hint + p.cargo("package --no-verify") + .with_status(101) + .with_stderr_data(str![[r#" +[WARNING] manifest has no description, license, license-file, documentation, homepage or repository + | + = [NOTE] see https://doc.rust-lang.org/cargo/reference/manifest.html#package-metadata for more info +[PACKAGING] foo v0.0.1 ([ROOT]/foo) +[ERROR] failed to prepare local package for uploading + +Caused by: + failed to open for archiving: `[ROOT]/foo/unreadable-dir` + + Note: If this is a local artifact, add it to .gitignore or the 'exclude' list in Cargo.toml. + +Caused by: + Permission denied (os error 13) + +"#]]) + .run(); +} + +#[cfg(unix)] +#[cargo_test] +fn unreadable_directory_list_warning() { + use std::os::unix::fs::PermissionsExt; + + let p = project().file("src/lib.rs", "").build(); + + // Create an unreadable directory + let unreadable_dir = p.root().join("unreadable-dir"); + fs::create_dir(&unreadable_dir).unwrap(); + fs::set_permissions(&unreadable_dir, fs::Permissions::from_mode(0o000)).unwrap(); + + // Ensure cleanup happens even if test fails + struct Cleanup(std::path::PathBuf); + impl Drop for Cleanup { + fn drop(&mut self) { + let _ = fs::set_permissions(&self.0, fs::Permissions::from_mode(0o755)); + } + } + let _cleanup = Cleanup(unreadable_dir.clone()); + + // cargo package --list should succeed but warn about unreadable directory + p.cargo("package --list") + .with_status(0) // Should still succeed + .with_stderr_data(str![[r#" +[WARNING] manifest has no description, license, license-file, documentation, homepage or repository + | + = [NOTE] see https://doc.rust-lang.org/cargo/reference/manifest.html#package-metadata for more info +[WARNING] cannot access `[ROOT]/foo/unreadable-dir`: Permission denied (os error 13). `cargo package` will fail. + +"#]]) + .run(); +}