Invalidate interpreter marker cache (#529)

In a refactor, we lost the cache invalidation behavior for interpreter
markers, leading to stale interpreter errors for me when creating
environments with different Python versions. Specifically, the
modification timestamp used to be part of the _cache key_ when we used
`cacache`. Now it's not -- but it's stored within the cache. So we need
to validate the key after-the-fact.
This commit is contained in:
Charlie Marsh 2023-12-03 17:44:43 -05:00 committed by GitHub
parent ee2fca3a48
commit 2613382747
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
2 changed files with 33 additions and 25 deletions

View File

@ -133,6 +133,7 @@ pub struct PyProjectToml {
} }
/// `[build-backend]` from pyproject.toml /// `[build-backend]` from pyproject.toml
#[derive(Debug)]
struct Pep517Backend { struct Pep517Backend {
/// The build backend string such as `setuptools.build_meta:__legacy__` or `maturin` from /// The build backend string such as `setuptools.build_meta:__legacy__` or `maturin` from
/// `build-backend.backend` in pyproject.toml /// `build-backend.backend` in pyproject.toml
@ -252,14 +253,15 @@ impl SourceBuild {
let venv = gourgeist::create_venv(&temp_dir.path().join(".venv"), interpreter)?; let venv = gourgeist::create_venv(&temp_dir.path().join(".venv"), interpreter)?;
// There are packages such as DTLSSocket 0.1.16 that say // There are packages such as DTLSSocket 0.1.16 that say:
// ```toml // ```toml
// [build-system] // [build-system]
// requires = ["Cython<3", "setuptools", "wheel"] // requires = ["Cython<3", "setuptools", "wheel"]
// ``` // ```
// In this case we need to install requires PEP 517 style but then call setup.py in the // In this case, we need to install requires PEP 517 style but then call setup.py in the
// legacy way // legacy way.
let requirements = if let Some(build_system) = &build_system { let requirements = if let Some(build_system) = build_system.as_ref() {
// .filter(|build_system| build_system.build_backend.is_some()) {
let resolved_requirements = build_context let resolved_requirements = build_context
.resolve(&build_system.requires) .resolve(&build_system.requires)
.await .await
@ -324,14 +326,12 @@ impl SourceBuild {
source_dist, source_dist,
) )
.await?; .await?;
} else { } else if !source_tree.join("setup.py").is_file() {
if !source_tree.join("setup.py").is_file() {
return Err(Error::InvalidSourceDist( return Err(Error::InvalidSourceDist(
"The archive contains neither a pyproject.toml or a setup.py at the top level" "The archive contains neither a `pyproject.toml` nor a `setup.py` file at the top level"
.to_string(), .to_string(),
)); ));
} }
}
Ok(Self { Ok(Self {
temp_dir, temp_dir,
@ -598,8 +598,9 @@ async fn create_pep517_build_environment(
} }
Ok(()) Ok(())
} }
/// Returns the directory with the `pyproject.toml`/`setup.py` /// Returns the directory with the `pyproject.toml`/`setup.py`
#[instrument(skip_all, fields(sdist = ?sdist.file_name().unwrap_or(sdist.as_os_str())))] #[instrument(skip_all, fields(sdist = ? sdist.file_name().unwrap_or(sdist.as_os_str())))]
fn extract_archive(sdist: &Path, extracted: &PathBuf) -> Result<PathBuf, Error> { fn extract_archive(sdist: &Path, extracted: &PathBuf) -> Result<PathBuf, Error> {
if sdist if sdist
.extension() .extension()

View File

@ -103,7 +103,7 @@ impl Interpreter {
} }
} }
#[derive(Deserialize, Serialize, Clone)] #[derive(Debug, Deserialize, Serialize, Clone)]
pub(crate) struct InterpreterQueryResult { pub(crate) struct InterpreterQueryResult {
pub(crate) markers: MarkerEnvironment, pub(crate) markers: MarkerEnvironment,
pub(crate) base_exec_prefix: PathBuf, pub(crate) base_exec_prefix: PathBuf,
@ -166,24 +166,31 @@ impl InterpreterQueryResult {
let cache_dir = cache.bucket(CacheBucket::Interpreter); let cache_dir = cache.bucket(CacheBucket::Interpreter);
let cache_path = cache_dir.join(format!("{}.json", digest(&executable_bytes))); let cache_path = cache_dir.join(format!("{}.json", digest(&executable_bytes)));
let modified = fs_err::metadata(executable)?
// Note: This is infallible on windows and unix (i.e., all platforms we support).
.modified()?
.duration_since(UNIX_EPOCH)
.map_err(|err| Error::SystemTime(executable.to_path_buf(), err))?;
// Read from the cache. // Read from the cache.
if let Ok(data) = fs::read(&cache_path) { if let Ok(data) = fs::read(&cache_path) {
if let Ok(cached) = serde_json::from_slice::<CachedByTimestamp<Self>>(&data) { if let Ok(cached) = serde_json::from_slice::<CachedByTimestamp<Self>>(&data) {
debug!("Using cached markers for {}", executable.display()); if cached.timestamp == modified.as_millis() {
debug!("Using cached markers for: {}", executable.display());
return Ok(cached.data); return Ok(cached.data);
} }
debug!(
"Ignoring stale cached markers for: {}",
executable.display()
);
}
} }
// Otherwise, run the Python script. // Otherwise, run the Python script.
debug!("Detecting markers for {}", executable.display()); debug!("Detecting markers for: {}", executable.display());
let info = Self::query(executable)?; let info = Self::query(executable)?;
let modified = fs_err::metadata(executable)?
// Note: This is infallible on windows and unix (i.e. all platforms we support)
.modified()?
.duration_since(UNIX_EPOCH)
.map_err(|err| Error::SystemTime(executable.to_path_buf(), err))?;
// If `executable` is a pyenv shim, a bash script that redirects to the activated // If `executable` is a pyenv shim, a bash script that redirects to the activated
// python executable at another path, we're not allowed to cache the interpreter info // python executable at another path, we're not allowed to cache the interpreter info
if executable == info.sys_executable { if executable == info.sys_executable {