fix(prepare): handle freshness check for auto-created venvs#7770
Conversation
When a venv is auto-created during config loading but before prepare runs, the timestamp comparison incorrectly concluded dependencies were "fresh" because the venv directory was newer than the lock file. This fix adds in-memory tracking of directories created during the current session. When check_freshness() runs, it checks if any output directory was created this session - if so, it's considered stale and prepare will run. Fixes: #7762 Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
There was a problem hiding this comment.
Pull request overview
This PR fixes an issue where mise prepare incorrectly skips dependency installation when a Python virtual environment is auto-created during config loading. The problem occurs because the newly created venv has a timestamp newer than the lock file, causing the freshness check to incorrectly conclude that dependencies are already installed.
Changes:
- Adds in-memory tracking of directories created during the current session using a static
LazyLock<Mutex<HashSet<PathBuf>>> - Modifies freshness check logic to treat outputs created during the current session as stale
- Marks auto-created venvs as stale and clears this status after successful prepare execution
Reviewed changes
Copilot reviewed 3 out of 3 changed files in this pull request and generated 2 comments.
| File | Description |
|---|---|
| src/prepare/mod.rs | Adds global state tracking and helper functions for managing stale output paths |
| src/prepare/engine.rs | Updates prepare execution flow to check stale status and clear it after successful runs |
| src/config/env_directive/venv.rs | Marks newly created venvs as stale for prepare freshness checks |
Comments suppressed due to low confidence (1)
src/prepare/mod.rs:1
- Silent failure on lock poisoning could mask issues. Consider logging a warning or error when the mutex lock fails, similar to the concern in mark_output_stale.
use std::collections::{BTreeMap, HashSet};
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
| if let Ok(mut set) = STALE_OUTPUTS.lock() { | ||
| set.insert(path); | ||
| } | ||
| } | ||
|
|
||
| /// Check if a directory was created this session | ||
| pub fn is_output_stale(path: &PathBuf) -> bool { | ||
| STALE_OUTPUTS | ||
| .lock() | ||
| .map(|set| set.contains(path)) | ||
| .unwrap_or(false) | ||
| } | ||
|
|
||
| /// Clear stale status for a path (after prepare runs successfully) | ||
| pub fn clear_output_stale(path: &PathBuf) { | ||
| if let Ok(mut set) = STALE_OUTPUTS.lock() { | ||
| set.remove(path); |
There was a problem hiding this comment.
Silent failure on lock poisoning could mask issues. Consider logging a warning or error when the mutex lock fails, as this would indicate a serious problem with the stale tracking mechanism.
| if let Ok(mut set) = STALE_OUTPUTS.lock() { | |
| set.insert(path); | |
| } | |
| } | |
| /// Check if a directory was created this session | |
| pub fn is_output_stale(path: &PathBuf) -> bool { | |
| STALE_OUTPUTS | |
| .lock() | |
| .map(|set| set.contains(path)) | |
| .unwrap_or(false) | |
| } | |
| /// Clear stale status for a path (after prepare runs successfully) | |
| pub fn clear_output_stale(path: &PathBuf) { | |
| if let Ok(mut set) = STALE_OUTPUTS.lock() { | |
| set.remove(path); | |
| match STALE_OUTPUTS.lock() { | |
| Ok(mut set) => { | |
| set.insert(path); | |
| } | |
| Err(err) => { | |
| warn!("prepare: failed to mark output as stale due to poisoned mutex: {err}"); | |
| } | |
| } | |
| } | |
| /// Check if a directory was created this session | |
| pub fn is_output_stale(path: &PathBuf) -> bool { | |
| match STALE_OUTPUTS.lock() { | |
| Ok(set) => set.contains(path), | |
| Err(err) => { | |
| warn!("prepare: failed to check stale output due to poisoned mutex: {err}"); | |
| false | |
| } | |
| } | |
| } | |
| /// Clear stale status for a path (after prepare runs successfully) | |
| pub fn clear_output_stale(path: &PathBuf) { | |
| match STALE_OUTPUTS.lock() { | |
| Ok(mut set) => { | |
| set.remove(path); | |
| } | |
| Err(err) => { | |
| warn!("prepare: failed to clear stale output due to poisoned mutex: {err}"); | |
| } |
| for output in &outputs { | ||
| super::clear_output_stale(output); | ||
| } |
There was a problem hiding this comment.
Clearing stale status happens after prepare succeeds, but if multiple providers share the same output path and one fails while another succeeds, the output might be incorrectly marked as non-stale. Consider whether clearing should only happen after all prepare steps complete successfully.
Hyperfine Performance
|
| Command | Mean [ms] | Min [ms] | Max [ms] | Relative |
|---|---|---|---|---|
mise-2026.1.5 x -- echo |
19.7 ± 0.5 | 19.1 | 24.8 | 1.00 |
mise x -- echo |
19.8 ± 0.3 | 19.1 | 23.2 | 1.00 ± 0.03 |
mise env
| Command | Mean [ms] | Min [ms] | Max [ms] | Relative |
|---|---|---|---|---|
mise-2026.1.5 env |
19.1 ± 0.4 | 18.6 | 25.2 | 1.00 |
mise env |
19.5 ± 0.6 | 18.6 | 26.2 | 1.02 ± 0.04 |
mise hook-env
| Command | Mean [ms] | Min [ms] | Max [ms] | Relative |
|---|---|---|---|---|
mise-2026.1.5 hook-env |
19.3 ± 0.2 | 18.7 | 20.2 | 1.00 |
mise hook-env |
19.5 ± 0.3 | 18.9 | 20.6 | 1.01 ± 0.02 |
mise ls
| Command | Mean [ms] | Min [ms] | Max [ms] | Relative |
|---|---|---|---|---|
mise-2026.1.5 ls |
17.3 ± 0.4 | 16.6 | 18.6 | 1.00 |
mise ls |
17.3 ± 0.3 | 16.7 | 21.7 | 1.00 ± 0.03 |
xtasks/test/perf
| Command | mise-2026.1.5 | mise | Variance |
|---|---|---|---|
| install (cached) | 109ms | 108ms | +0% |
| ls (cached) | 66ms | 71ms | -7% |
| bin-paths (cached) | 70ms | 70ms | +0% |
| task-ls (cached) | 282ms | 283ms | +0% |
Tests the basic timestamp-based freshness logic: 1. No output directory exists → stale 2. Output newer than sources → fresh 3. Sources newer than output → stale Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
If multiple providers share the same output path and one fails while another succeeds, the output could be incorrectly marked as non-stale. Now stale status is only cleared after ALL prepare steps complete successfully. Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
### 🚀 Features - **(config)** add miette diagnostics for TOML parsing errors by @jdx in [#7764](#7764) - **(env)** add environment caching with module cacheability support by @jdx in [#7761](#7761) ### 🐛 Bug Fixes - **(prepare)** handle freshness check for auto-created venvs by @jdx in [#7770](#7770) - **(release)** use colon separator in release titles by @jdx in [#7765](#7765) - **(release)** drop Fedora 41 from COPR build (EOL) by @TobiX in [#7771](#7771) ### 🚜 Refactor - improve filetask field parsing tests and parser by @makp0 in [#7751](#7751) ### 📚 Documentation - improve CLAUDE.md with additional development guidance by @jdx in [#7763](#7763) - drop architecture from Debian sources.list by @TobiX in [#7772](#7772) ### 📦 Registry - use aqua for zprint by @scop in [#7767](#7767) ### Security - remove insecure registry-comment workflow by @jdx in [#7769](#7769) ## 📦 Aqua Registry Updates #### New Packages (2) - [`cameron-martin/bazel-lsp`](https://github.com/cameron-martin/bazel-lsp) - [`micro-editor/micro`](https://github.com/micro-editor/micro)
## Context With the current `python.uv_venv_auto` behavior, there are two annoying things: 1. it always creates venvs if they not exist This is not needed for `uv`, since that usually creates/updates it's venvs transparently (when doing `uv run` or `uv sync`, ...). And it breaks `mise prepare` for `uv` since it creates the `.venv` folder but does not install any deps. So `mise prepare` thinks everything is up-to-date when it isn't. (the uv-native venv creation appears to not have been covered by #7770) 2. it exports the mise python version to the env var `UV_PYTHON`, which forces uv to use that _version_. ## Changes - Upfront, some reworking of the schema of `settings.toml`, specifically the `type` and `enum` definitions. Added the `BooleanOrString` type needed for this and also made the `enum` definitions more flexible (e.g. with mixed types) - Introducing new values for `python.uv_venv_auto` - `"source"` only sources existing venvs - `"create|source"` creates if not exists and sources venvs (but does not set UV_PYTHON anymore) - `false` does nothing – like now - `true` (legacy, will be deprecated in 6 months) – like now, also sets UV_PYTHON - Unified the code creating/sourcing venvs between `settings.python.uv_venv_auto` and `env._.python.venv`, which should now also mark these as stale at creation. I think with this changes, `python.uv_venv_auto` becomes much more useful.
## Context With the current `python.uv_venv_auto` behavior, there are two annoying things: 1. it always creates venvs if they not exist This is not needed for `uv`, since that usually creates/updates it's venvs transparently (when doing `uv run` or `uv sync`, ...). And it breaks `mise prepare` for `uv` since it creates the `.venv` folder but does not install any deps. So `mise prepare` thinks everything is up-to-date when it isn't. (the uv-native venv creation appears to not have been covered by jdx#7770) 2. it exports the mise python version to the env var `UV_PYTHON`, which forces uv to use that _version_. ## Changes - Upfront, some reworking of the schema of `settings.toml`, specifically the `type` and `enum` definitions. Added the `BooleanOrString` type needed for this and also made the `enum` definitions more flexible (e.g. with mixed types) - Introducing new values for `python.uv_venv_auto` - `"source"` only sources existing venvs - `"create|source"` creates if not exists and sources venvs (but does not set UV_PYTHON anymore) - `false` does nothing – like now - `true` (legacy, will be deprecated in 6 months) – like now, also sets UV_PYTHON - Unified the code creating/sourcing venvs between `settings.python.uv_venv_auto` and `env._.python.venv`, which should now also mark these as stale at creation. I think with this changes, `python.uv_venv_auto` becomes much more useful.
Summary
mise prepareincorrectly skips dependency installation when a venv is auto-createdcheck_freshness()runs, outputs created this session are considered staleFixes: #7762
Test plan
[env._.python.venv]and[prepare.uv]configuredmise prepareon first entry - should runuv syncmise prepareagain - should report "fresh" (skip sync)uv.lockand runmise prepare- should runuv syncagain🤖 Generated with Claude Code
Note
Ensures
prepareruns when outputs like.venvare auto-created during env resolution, preventing false "fresh" detections.mark_output_stale,is_output_stale, andclear_output_staleinsrc/prepare/mod.rscheck_freshness()to consideris_output_stale(output)before mtime comparisoncrate::prepare::mark_output_stale(venv)insrc/config/env_directive/venv.rsclear_output_staleinsrc/prepare/engine.rs.venvvsrequirements.txt)Written by Cursor Bugbot for commit 2195b47. This will update automatically on new commits. Configure here.