Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
43 changes: 19 additions & 24 deletions docs/dev-tools/prepare.md
Original file line number Diff line number Diff line change
@@ -1,8 +1,7 @@
# Prepare <Badge type="warning" text="experimental" />

The `mise prepare` command ensures project dependencies are ready by checking if lockfiles
are newer than installed outputs (e.g., `package-lock.json` vs `node_modules/`) and running
install commands if needed.
The `mise prepare` command ensures project dependencies are ready by hashing source files
(e.g., `package-lock.json`) and running install commands when changes are detected.

## Quick Start

Expand Down Expand Up @@ -87,37 +86,33 @@ run = "npx prisma generate"

### Provider Options

| Option | Type | Description |
| --------------- | -------- | ------------------------------------------------------------------------------- |
| `auto` | bool | Auto-run before `mise x` and `mise run` (default: false) |
| `sources` | string[] | Files/patterns to check for changes |
| `outputs` | string[] | Files/directories that should be newer than sources |
| `run` | string | Command to run when stale |
| `env` | table | Environment variables to set |
| `dir` | string | Working directory for the command |
| `description` | string | Description shown in output |
| `touch_outputs` | bool | Touch output mtimes after a successful run so they appear fresh (default: true) |
| `depends` | string[] | Other provider names that must complete before this one runs |
| `timeout` | string | Timeout for the run command, e.g., `"30s"`, `"5m"` (default: no timeout) |
| Option | Type | Description |
| ------------- | -------- | ------------------------------------------------------------------------- |
| `auto` | bool | Auto-run before `mise x` and `mise run` (default: false) |
| `sources` | string[] | Files/patterns to check for changes |
| `outputs` | string[] | Files/directories that must exist for the provider to be considered fresh |
| `run` | string | Command to run when stale |
| `env` | table | Environment variables to set |
| `dir` | string | Working directory for the command |
| `description` | string | Description shown in output |
| `depends` | string[] | Other provider names that must complete before this one runs |
| `timeout` | string | Timeout for the run command, e.g., `"30s"`, `"5m"` (default: no timeout) |

## Freshness Checking

mise uses modification time (mtime) comparison to determine if outputs are stale:
mise uses blake3 content hashing to determine if sources have changed since the last
successful run. Hashes are stored in `.mise/prepare-state.toml`.

1. Find the most recent mtime among all source files
2. Find the most recent mtime among all output files
3. If any source is newer than all outputs, the provider is stale
1. Compute blake3 hashes of all source files
2. Compare against stored hashes from the last successful run
3. If any file was added, removed, or changed, the provider is stale
Comment on lines +103 to +108

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

The description of freshness checking is a bit simplified as it omits the check for output existence, which is a key part of the logic. For better accuracy and to avoid user confusion, I suggest clarifying that both output existence and source content hashing are used.

Suggested change
mise uses blake3 content hashing to determine if sources have changed since the last
successful run. Hashes are stored in `.mise/prepare-state.toml`.
1. Find the most recent mtime among all source files
2. Find the most recent mtime among all output files
3. If any source is newer than all outputs, the provider is stale
1. Compute blake3 hashes of all source files
2. Compare against stored hashes from the last successful run
3. If any file was added, removed, or changed, the provider is stale
mise uses output existence and blake3 content hashing to determine if a provider is stale.
Hashes of source files are stored in '.mise/prepare-state.toml' after a successful run.
1. Check if all output files/directories exist. If not, the provider is stale.
2. Compute blake3 hashes of all source files.
3. Compare against stored hashes. If any file was added, removed, or changed, the provider is stale.


This means:

- If you modify `package-lock.json`, `node_modules/` will be considered stale

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

This point could be rephrased for better clarity. With content hashing, the staleness is determined by changes in source files, not by a comparison with outputs. Mentioning node_modules/ might be confusing as it's an output, not part of the source hash check.

Suggested change
- If you modify `package-lock.json`, `node_modules/` will be considered stale
- If you modify a source file like 'package-lock.json', its content hash will change, and the provider will be considered stale.

- If `node_modules/` doesn't exist, the provider is always stale
- If sources don't exist, the provider is considered fresh (nothing to do)

After a successful run, mise touches the mtime of each output to now (controlled by
`touch_outputs`, default `true`). This ensures that commands which are no-ops when
dependencies are already satisfied (e.g. `uv sync` when the venv is up to date) still
mark outputs as fresh, preventing repeated stale warnings on subsequent invocations.
- On first run (no stored state), the provider is always considered stale

## Auto-Prepare

Expand Down
5 changes: 0 additions & 5 deletions schema/mise.json
Original file line number Diff line number Diff line change
Expand Up @@ -2247,11 +2247,6 @@
},
"description": "Other prepare providers that must complete before this one runs"
},
"touch_outputs": {
"type": "boolean",
"default": true,
"description": "Whether to update mtime of output files/dirs after a successful run"
},
"timeout": {
"type": "string",
"description": "Timeout for the run command (e.g., \"30s\", \"5m\", \"1h\")"
Expand Down
22 changes: 0 additions & 22 deletions src/prepare/engine.rs
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,6 @@ use std::path::{Path, PathBuf};
use std::sync::Arc;

use eyre::Result;
use filetime::FileTime;
use tokio::sync::Semaphore;
use tokio::task::JoinSet;

Expand Down Expand Up @@ -69,7 +68,6 @@ struct PrepareJob {
id: String,
cmd: super::PrepareCommand,
outputs: Vec<PathBuf>,
touch: bool,
depends: Vec<String>,
timeout: Option<std::time::Duration>,
}
Expand Down Expand Up @@ -346,7 +344,6 @@ impl PrepareEngine {
if !freshness.is_fresh() {
let cmd = provider.prepare_command()?;
let outputs = provider.outputs();
let touch = provider.touch_outputs();
let depends = provider.depends();
let timeout = provider.timeout();
let reason = freshness.reason().to_string();
Expand All @@ -358,7 +355,6 @@ impl PrepareEngine {
id,
cmd,
outputs,
touch,
depends,
timeout,
});
Expand Down Expand Up @@ -433,9 +429,6 @@ impl PrepareEngine {
let pr = mpr.add(&job.cmd.description);
match Self::execute_prepare_static(&job.cmd, &toolset_env, job.timeout) {
Ok(()) => {
if job.touch {
Self::touch_outputs(&job.outputs);
}
pr.finish_with_message(format!("{} done", job.cmd.description));
Ok((PrepareStepResult::Ran(job.id), job.outputs))
}
Expand Down Expand Up @@ -587,9 +580,6 @@ impl PrepareEngine {

match result {
Ok(Ok(())) => {
if job.touch {
Self::touch_outputs(&job.outputs);
}
pr.finish_with_message(format!("{} done", job.cmd.description));
let step = PrepareStepResult::Ran(id.clone());
Ok((id, step, job.outputs))
Expand Down Expand Up @@ -776,16 +766,4 @@ impl PrepareEngine {
runner.execute()?;
Ok(())
}

/// Update the mtime of output files/directories to now
fn touch_outputs(outputs: &[PathBuf]) {
let now = FileTime::now();
for path in outputs {
if path.exists()
&& let Err(e) = filetime::set_file_mtime(path, now)
{
warn!("failed to touch {}: {e}", path.display());
}
}
}
}
5 changes: 0 additions & 5 deletions src/prepare/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -129,11 +129,6 @@ pub trait PrepareProvider: Debug + Send + Sync {
self.base().is_auto()
}

/// Whether to update mtime of output files/dirs after a successful run
fn touch_outputs(&self) -> bool {
self.base().touch_outputs()
}

/// Other prepare providers that must complete before this one runs
fn depends(&self) -> Vec<String> {
self.base().config.depends.clone()
Expand Down
6 changes: 1 addition & 5 deletions src/prepare/providers/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -29,7 +29,7 @@ use std::path::{Path, PathBuf};
use crate::prepare::rule::PrepareProviderConfig;

/// Shared base for all prepare providers, holding the id, project root, and config.
/// Provides common implementations for `id`, `is_auto`, and `touch_outputs`.
/// Provides common implementations for `id` and `is_auto`.
#[derive(Debug)]
pub struct ProviderBase {
pub(crate) id: String,
Expand All @@ -50,10 +50,6 @@ impl ProviderBase {
self.config.auto
}

pub fn touch_outputs(&self) -> bool {
self.config.touch_outputs.unwrap_or(true)
}

/// Returns the effective root directory for resolving sources/outputs.
/// When `dir` is set in config, returns `project_root/dir`; otherwise `project_root`.
pub fn config_root(&self) -> PathBuf {
Expand Down
4 changes: 0 additions & 4 deletions src/prepare/rule.rs
Original file line number Diff line number Diff line change
Expand Up @@ -42,10 +42,6 @@ pub struct PrepareProviderConfig {
pub dir: Option<String>,
/// Optional description
pub description: Option<String>,
/// Whether to update mtime of output files/dirs after a successful run (default: true)
/// This is useful when the prepare command is a no-op (e.g., `uv sync` when all is well)
/// so that the outputs appear fresh for subsequent freshness checks.
pub touch_outputs: Option<bool>,

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Removing field with deny_unknown_fields breaks existing configs

High Severity

PrepareProviderConfig has #[serde(deny_unknown_fields)], so removing the touch_outputs field means any existing user config that includes touch_outputs = true (or false) will fail deserialization with an "unknown field" error. This silently breaks existing configurations rather than gracefully ignoring the deprecated option.

Fix in Cursor Fix in Web

Copy link
Copy Markdown
Owner Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

it's experimental so we don't need to worry about breaking changes

/// Other prepare providers that must complete before this one runs
#[serde(default)]
pub depends: Vec<String>,
Expand Down
Loading