diff --git a/src/cargo/sources/registry/mod.rs b/src/cargo/sources/registry/mod.rs index 4a70531df99..ca9d514552b 100644 --- a/src/cargo/sources/registry/mod.rs +++ b/src/cargo/sources/registry/mod.rs @@ -449,15 +449,16 @@ impl<'cfg> RegistrySource<'cfg> { let path = dst.join(PACKAGE_SOURCE_LOCK); let path = self.config.assert_package_cache_locked(&path); let unpack_dir = path.parent().unwrap(); + if let Ok(meta) = path.metadata() { + if meta.len() > 0 { + return Ok(unpack_dir.to_path_buf()); + } + } let mut ok = OpenOptions::new() .create(true) .read(true) .write(true) .open(&path)?; - let meta = ok.metadata()?; - if meta.len() > 0 { - return Ok(unpack_dir.to_path_buf()); - } let gz = GzDecoder::new(tarball); let mut tar = Archive::new(gz); diff --git a/src/cargo/util/config.rs b/src/cargo/util/config.rs index a303da3a402..2bea77525bc 100644 --- a/src/cargo/util/config.rs +++ b/src/cargo/util/config.rs @@ -6,7 +6,7 @@ use std::env; use std::fmt; use std::fs::{self, File}; use std::io::prelude::*; -use std::io::SeekFrom; +use std::io::{self, SeekFrom}; use std::mem; use std::path::{Path, PathBuf}; use std::str::FromStr; @@ -860,21 +860,71 @@ impl Config { return ret; } + /// Acquires an exclusive lock on the global "package cache" + /// + /// This lock is global per-process and can be acquired recursively. An RAII + /// structure is returned to release the lock, and if this process + /// abnormally terminates the lock is also released. pub fn acquire_package_cache_lock<'a>(&'a self) -> CargoResult> { let mut slot = self.package_cache_lock.borrow_mut(); match *slot { + // We've already acquired the lock in this process, so simply bump + // the count and continue. Some((_, ref mut cnt)) => { *cnt += 1; } None => { - let lock = self - .home_path - .open_rw(".package-cache", self, "package cache lock") - .chain_err(|| "failed to acquire package cache lock")?; - *slot = Some((lock, 1)); + let path = ".package-cache"; + let desc = "package cache lock"; + + // First, attempt to open an exclusive lock which is in general + // the purpose of this lock! + // + // If that fails because of a readonly filesystem, though, then + // we don't want to fail because it's a readonly filesystem. In + // some situations Cargo is prepared to have a readonly + // filesystem yet still work since it's all been pre-downloaded + // and/or pre-unpacked. In these situations we want to keep + // Cargo running if possible, so if it's a readonly filesystem + // switch to a shared lock which should hopefully succeed so we + // can continue. + // + // Note that the package cache lock protects files in the same + // directory, so if it's a readonly filesystem we assume that + // the entire package cache is readonly, so we're just acquiring + // something to prove it works, we're not actually doing any + // synchronization at that point. + match self.home_path.open_rw(path, self, desc) { + Ok(lock) => *slot = Some((lock, 1)), + Err(e) => { + if maybe_readonly(&e) { + if let Ok(lock) = self.home_path.open_ro(path, self, desc) { + *slot = Some((lock, 1)); + return Ok(PackageCacheLock(self)); + } + } + + Err(e).chain_err(|| "failed to acquire package cache lock")?; + } + } } } - Ok(PackageCacheLock(self)) + return Ok(PackageCacheLock(self)); + + fn maybe_readonly(err: &failure::Error) -> bool { + err.iter_chain().any(|err| { + if let Some(io) = err.downcast_ref::() { + if io.kind() == io::ErrorKind::PermissionDenied { + return true; + } + + #[cfg(unix)] + return io.raw_os_error() == Some(libc::EROFS); + } + + false + }) + } } pub fn release_package_cache_lock(&self) {} diff --git a/tests/testsuite/registry.rs b/tests/testsuite/registry.rs index 1c2af7abbfb..b846d85eece 100644 --- a/tests/testsuite/registry.rs +++ b/tests/testsuite/registry.rs @@ -1,5 +1,6 @@ use std::fs::{self, File}; use std::io::prelude::*; +use std::path::Path; use crate::support::cargo_process; use crate::support::git; @@ -1979,3 +1980,48 @@ fn ignore_invalid_json_lines() { p.cargo("build").run(); } + +#[test] +fn readonly_registry_still_works() { + Package::new("foo", "0.1.0").publish(); + + let p = project() + .file( + "Cargo.toml", + r#" + [project] + name = "a" + version = "0.5.0" + authors = [] + + [dependencies] + foo = '0.1.0' + "#, + ) + .file("src/lib.rs", "") + .build(); + + p.cargo("generate-lockfile").run(); + p.cargo("fetch --locked").run(); + chmod_readonly(&paths::home()); + p.cargo("build").run(); + + fn chmod_readonly(path: &Path) { + for entry in t!(path.read_dir()) { + let entry = t!(entry); + let path = entry.path(); + if t!(entry.file_type()).is_dir() { + chmod_readonly(&path); + } else { + set_readonly(&path); + } + } + set_readonly(path); + } + + fn set_readonly(path: &Path) { + let mut perms = t!(path.metadata()).permissions(); + perms.set_readonly(true); + t!(fs::set_permissions(path, perms)); + } +} diff --git a/tests/testsuite/support/paths.rs b/tests/testsuite/support/paths.rs index cdc3e541e47..ac57e54b76a 100644 --- a/tests/testsuite/support/paths.rs +++ b/tests/testsuite/support/paths.rs @@ -158,10 +158,18 @@ where { match f(path) { Ok(()) => {} - Err(ref e) if cfg!(windows) && e.kind() == ErrorKind::PermissionDenied => { + Err(ref e) if e.kind() == ErrorKind::PermissionDenied => { let mut p = t!(path.metadata()).permissions(); p.set_readonly(false); t!(fs::set_permissions(path, p)); + + // Unix also requires the parent to not be readonly for example when + // removing files + let parent = path.parent().unwrap(); + let mut p = t!(parent.metadata()).permissions(); + p.set_readonly(false); + t!(fs::set_permissions(parent, p)); + f(path).unwrap_or_else(|e| { panic!("failed to {} {}: {}", desc, path.display(), e); })