diff --git a/e2e/cli/test_prepare b/e2e/cli/test_prepare index 991e61010c..a1901fa471 100644 --- a/e2e/cli/test_prepare +++ b/e2e/cli/test_prepare @@ -492,3 +492,35 @@ assert_not_contains "cat tera_output.txt" "{{env.MY_TEMPLATE_VAR}}" # Clean up rm -f tera_input.txt tera_output.txt mise.toml rm -rf .mise + +# Test dir option affects source/output resolution for custom providers +mkdir -p subdir +echo '{"name": "test"}' >subdir/package.json +echo '{"lockfileVersion": 3}' >subdir/package-lock.json + +cat >mise.toml <<'EOF' +[prepare.subdir_custom] +dir = "subdir" +sources = ["package.json", "package-lock.json"] +outputs = ["build_output"] +run = "echo SUBDIR_RAN" +EOF + +# Sources should resolve against subdir, not project root +assert_contains "mise prepare --list" "subdir_custom" +assert_contains "mise prepare --list" "subdir/package.json" +assert_contains "mise prepare --list" "subdir/package-lock.json" + +# Run prepare and verify source hashes are tracked (provider becomes fresh) +mkdir -p subdir/build_output +assert_contains "mise prepare --only subdir_custom" "Prepared: subdir_custom" + +# Second run should be fresh (sources hashed correctly via subdir) +assert_contains "mise prepare --only subdir_custom" "up to date" + +# Modify source in subdir → should be stale +echo '{"name": "modified"}' >subdir/package.json +assert_contains "mise prepare --only subdir_custom" "Prepared: subdir_custom" + +# Clean up +rm -rf subdir mise.toml .mise diff --git a/src/prepare/providers/bun.rs b/src/prepare/providers/bun.rs index 5db39cf5d7..96660d672c 100644 --- a/src/prepare/providers/bun.rs +++ b/src/prepare/providers/bun.rs @@ -21,12 +21,13 @@ impl BunPrepareProvider { } fn lockfile_path(&self) -> Option { + let root = self.base.config_root(); // Bun supports both bun.lockb (binary) and bun.lock (text) - let binary_lock = self.base.project_root.join("bun.lockb"); + let binary_lock = root.join("bun.lockb"); if binary_lock.exists() { return Some(binary_lock); } - let text_lock = self.base.project_root.join("bun.lock"); + let text_lock = root.join("bun.lock"); if text_lock.exists() { return Some(text_lock); } @@ -44,12 +45,12 @@ impl PrepareProvider for BunPrepareProvider { if let Some(lockfile) = self.lockfile_path() { sources.push(lockfile); } - sources.push(self.base.project_root.join("package.json")); + sources.push(self.base.config_root().join("package.json")); sources } fn outputs(&self) -> Vec { - vec![self.base.project_root.join("node_modules")] + vec![self.base.config_root().join("node_modules")] } fn prepare_command(&self) -> Result { @@ -61,7 +62,7 @@ impl PrepareProvider for BunPrepareProvider { program: "bun".to_string(), args: vec!["install".to_string()], env: self.base.config.env.clone(), - cwd: Some(self.base.project_root.clone()), + cwd: Some(self.base.config_root()), description: self .base .config diff --git a/src/prepare/providers/bundler.rs b/src/prepare/providers/bundler.rs index 47c05935b0..2780a9cfaa 100644 --- a/src/prepare/providers/bundler.rs +++ b/src/prepare/providers/bundler.rs @@ -27,20 +27,19 @@ impl PrepareProvider for BundlerPrepareProvider { } fn sources(&self) -> Vec { - vec![ - self.base.project_root.join("Gemfile.lock"), - self.base.project_root.join("Gemfile"), - ] + let root = self.base.config_root(); + vec![root.join("Gemfile.lock"), root.join("Gemfile")] } fn outputs(&self) -> Vec { + let root = self.base.config_root(); // Check for vendor/bundle if using --path vendor/bundle - let vendor = self.base.project_root.join("vendor/bundle"); + let vendor = root.join("vendor/bundle"); if vendor.exists() { vec![vendor] } else { // Use .bundle directory as fallback indicator - vec![self.base.project_root.join(".bundle")] + vec![root.join(".bundle")] } } @@ -53,7 +52,7 @@ impl PrepareProvider for BundlerPrepareProvider { program: "bundle".to_string(), args: vec!["install".to_string()], env: self.base.config.env.clone(), - cwd: Some(self.base.project_root.clone()), + cwd: Some(self.base.config_root()), description: self .base .config @@ -64,6 +63,6 @@ impl PrepareProvider for BundlerPrepareProvider { } fn is_applicable(&self) -> bool { - self.base.project_root.join("Gemfile.lock").exists() + self.base.config_root().join("Gemfile.lock").exists() } } diff --git a/src/prepare/providers/composer.rs b/src/prepare/providers/composer.rs index 5bffef0225..a108597364 100644 --- a/src/prepare/providers/composer.rs +++ b/src/prepare/providers/composer.rs @@ -27,14 +27,12 @@ impl PrepareProvider for ComposerPrepareProvider { } fn sources(&self) -> Vec { - vec![ - self.base.project_root.join("composer.lock"), - self.base.project_root.join("composer.json"), - ] + let root = self.base.config_root(); + vec![root.join("composer.lock"), root.join("composer.json")] } fn outputs(&self) -> Vec { - vec![self.base.project_root.join("vendor")] + vec![self.base.config_root().join("vendor")] } fn prepare_command(&self) -> Result { @@ -46,7 +44,7 @@ impl PrepareProvider for ComposerPrepareProvider { program: "composer".to_string(), args: vec!["install".to_string()], env: self.base.config.env.clone(), - cwd: Some(self.base.project_root.clone()), + cwd: Some(self.base.config_root()), description: self .base .config @@ -57,6 +55,6 @@ impl PrepareProvider for ComposerPrepareProvider { } fn is_applicable(&self) -> bool { - self.base.project_root.join("composer.lock").exists() + self.base.config_root().join("composer.lock").exists() } } diff --git a/src/prepare/providers/custom.rs b/src/prepare/providers/custom.rs index a78988441b..18500d6813 100644 --- a/src/prepare/providers/custom.rs +++ b/src/prepare/providers/custom.rs @@ -26,8 +26,9 @@ impl CustomPrepareProvider { let mut paths = vec![]; for pattern in patterns { + let base_dir = self.base.config_root(); let full_pattern = if PathBuf::from(pattern).is_relative() { - self.base.project_root.join(pattern) + base_dir.join(pattern) } else { PathBuf::from(pattern) }; diff --git a/src/prepare/providers/git_submodule.rs b/src/prepare/providers/git_submodule.rs index 60cca1d803..999205c9c6 100644 --- a/src/prepare/providers/git_submodule.rs +++ b/src/prepare/providers/git_submodule.rs @@ -25,7 +25,7 @@ impl GitSubmodulePrepareProvider { /// Handles INI-style sections and comments. Only extracts `path` values /// from `[submodule "..."]` sections. fn submodule_paths(&self) -> Vec { - let gitmodules = self.base.project_root.join(".gitmodules"); + let gitmodules = self.base.config_root().join(".gitmodules"); let Ok(content) = std::fs::read_to_string(&gitmodules) else { return vec![]; }; @@ -53,7 +53,7 @@ impl GitSubmodulePrepareProvider { let value = value.trim_start(); value .strip_prefix('=') - .map(|value| self.base.project_root.join(value.trim())) + .map(|value| self.base.config_root().join(value.trim())) } else { None } @@ -68,7 +68,7 @@ impl PrepareProvider for GitSubmodulePrepareProvider { } fn sources(&self) -> Vec { - vec![self.base.project_root.join(".gitmodules")] + vec![self.base.config_root().join(".gitmodules")] } fn outputs(&self) -> Vec { @@ -89,7 +89,7 @@ impl PrepareProvider for GitSubmodulePrepareProvider { "--recursive".to_string(), ], env: self.base.config.env.clone(), - cwd: Some(self.base.project_root.clone()), + cwd: Some(self.base.config_root()), description: self .base .config @@ -100,7 +100,7 @@ impl PrepareProvider for GitSubmodulePrepareProvider { } fn is_applicable(&self) -> bool { - let gitmodules = self.base.project_root.join(".gitmodules"); + let gitmodules = self.base.config_root().join(".gitmodules"); gitmodules.exists() && gitmodules.metadata().map(|m| m.len() > 0).unwrap_or(false) } } diff --git a/src/prepare/providers/go.rs b/src/prepare/providers/go.rs index ecd58aa86a..9b449f3569 100644 --- a/src/prepare/providers/go.rs +++ b/src/prepare/providers/go.rs @@ -28,17 +28,18 @@ impl PrepareProvider for GoPrepareProvider { fn sources(&self) -> Vec { // go.mod defines dependencies - changes here trigger downloads - vec![self.base.project_root.join("go.mod")] + vec![self.base.config_root().join("go.mod")] } fn outputs(&self) -> Vec { + let root = self.base.config_root(); // Go downloads modules to GOPATH/pkg/mod, but we can check vendor/ if used - let vendor = self.base.project_root.join("vendor"); + let vendor = root.join("vendor"); if vendor.exists() { vec![vendor] } else { // go.sum gets updated after go mod download completes - vec![self.base.project_root.join("go.sum")] + vec![root.join("go.sum")] } } @@ -48,7 +49,7 @@ impl PrepareProvider for GoPrepareProvider { } // Use `go mod vendor` if vendor/ exists, otherwise `go mod download` - let vendor = self.base.project_root.join("vendor"); + let vendor = self.base.config_root().join("vendor"); let (args, desc) = if vendor.exists() { ( vec!["mod".to_string(), "vendor".to_string()], @@ -65,7 +66,7 @@ impl PrepareProvider for GoPrepareProvider { program: "go".to_string(), args, env: self.base.config.env.clone(), - cwd: Some(self.base.project_root.clone()), + cwd: Some(self.base.config_root()), description: self .base .config @@ -77,6 +78,6 @@ impl PrepareProvider for GoPrepareProvider { fn is_applicable(&self) -> bool { // Check for go.mod (the source/lockfile), not go.sum (which may be an output) - self.base.project_root.join("go.mod").exists() + self.base.config_root().join("go.mod").exists() } } diff --git a/src/prepare/providers/mod.rs b/src/prepare/providers/mod.rs index dd910e1737..3c2c8badf3 100644 --- a/src/prepare/providers/mod.rs +++ b/src/prepare/providers/mod.rs @@ -53,4 +53,13 @@ impl ProviderBase { 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 { + match &self.config.dir { + Some(dir) => self.project_root.join(dir), + None => self.project_root.clone(), + } + } } diff --git a/src/prepare/providers/npm.rs b/src/prepare/providers/npm.rs index 9e6dbb2152..7af2bbd858 100644 --- a/src/prepare/providers/npm.rs +++ b/src/prepare/providers/npm.rs @@ -27,14 +27,12 @@ impl PrepareProvider for NpmPrepareProvider { } fn sources(&self) -> Vec { - vec![ - self.base.project_root.join("package-lock.json"), - self.base.project_root.join("package.json"), - ] + let root = self.base.config_root(); + vec![root.join("package-lock.json"), root.join("package.json")] } fn outputs(&self) -> Vec { - vec![self.base.project_root.join("node_modules")] + vec![self.base.config_root().join("node_modules")] } fn prepare_command(&self) -> Result { @@ -46,7 +44,7 @@ impl PrepareProvider for NpmPrepareProvider { program: "npm".to_string(), args: vec!["install".to_string()], env: self.base.config.env.clone(), - cwd: Some(self.base.project_root.clone()), + cwd: Some(self.base.config_root()), description: self .base .config @@ -57,6 +55,6 @@ impl PrepareProvider for NpmPrepareProvider { } fn is_applicable(&self) -> bool { - self.base.project_root.join("package-lock.json").exists() + self.base.config_root().join("package-lock.json").exists() } } diff --git a/src/prepare/providers/pip.rs b/src/prepare/providers/pip.rs index 90e12a5e7d..f3c674e954 100644 --- a/src/prepare/providers/pip.rs +++ b/src/prepare/providers/pip.rs @@ -27,12 +27,12 @@ impl PrepareProvider for PipPrepareProvider { } fn sources(&self) -> Vec { - vec![self.base.project_root.join("requirements.txt")] + vec![self.base.config_root().join("requirements.txt")] } fn outputs(&self) -> Vec { // Check for .venv directory as output indicator - vec![self.base.project_root.join(".venv")] + vec![self.base.config_root().join(".venv")] } fn prepare_command(&self) -> Result { @@ -48,7 +48,7 @@ impl PrepareProvider for PipPrepareProvider { "requirements.txt".to_string(), ], env: self.base.config.env.clone(), - cwd: Some(self.base.project_root.clone()), + cwd: Some(self.base.config_root()), description: self .base .config @@ -59,6 +59,6 @@ impl PrepareProvider for PipPrepareProvider { } fn is_applicable(&self) -> bool { - self.base.project_root.join("requirements.txt").exists() + self.base.config_root().join("requirements.txt").exists() } } diff --git a/src/prepare/providers/pnpm.rs b/src/prepare/providers/pnpm.rs index db95d4adf1..4618fd6c07 100644 --- a/src/prepare/providers/pnpm.rs +++ b/src/prepare/providers/pnpm.rs @@ -27,14 +27,12 @@ impl PrepareProvider for PnpmPrepareProvider { } fn sources(&self) -> Vec { - vec![ - self.base.project_root.join("pnpm-lock.yaml"), - self.base.project_root.join("package.json"), - ] + let root = self.base.config_root(); + vec![root.join("pnpm-lock.yaml"), root.join("package.json")] } fn outputs(&self) -> Vec { - vec![self.base.project_root.join("node_modules")] + vec![self.base.config_root().join("node_modules")] } fn prepare_command(&self) -> Result { @@ -46,7 +44,7 @@ impl PrepareProvider for PnpmPrepareProvider { program: "pnpm".to_string(), args: vec!["install".to_string()], env: self.base.config.env.clone(), - cwd: Some(self.base.project_root.clone()), + cwd: Some(self.base.config_root()), description: self .base .config @@ -57,6 +55,6 @@ impl PrepareProvider for PnpmPrepareProvider { } fn is_applicable(&self) -> bool { - self.base.project_root.join("pnpm-lock.yaml").exists() + self.base.config_root().join("pnpm-lock.yaml").exists() } } diff --git a/src/prepare/providers/poetry.rs b/src/prepare/providers/poetry.rs index f07c69c711..f0effd7950 100644 --- a/src/prepare/providers/poetry.rs +++ b/src/prepare/providers/poetry.rs @@ -27,14 +27,12 @@ impl PrepareProvider for PoetryPrepareProvider { } fn sources(&self) -> Vec { - vec![ - self.base.project_root.join("poetry.lock"), - self.base.project_root.join("pyproject.toml"), - ] + let root = self.base.config_root(); + vec![root.join("poetry.lock"), root.join("pyproject.toml")] } fn outputs(&self) -> Vec { - vec![self.base.project_root.join(".venv")] + vec![self.base.config_root().join(".venv")] } fn prepare_command(&self) -> Result { @@ -46,7 +44,7 @@ impl PrepareProvider for PoetryPrepareProvider { program: "poetry".to_string(), args: vec!["install".to_string()], env: self.base.config.env.clone(), - cwd: Some(self.base.project_root.clone()), + cwd: Some(self.base.config_root()), description: self .base .config @@ -57,6 +55,6 @@ impl PrepareProvider for PoetryPrepareProvider { } fn is_applicable(&self) -> bool { - self.base.project_root.join("poetry.lock").exists() + self.base.config_root().join("poetry.lock").exists() } } diff --git a/src/prepare/providers/uv.rs b/src/prepare/providers/uv.rs index b34e577273..3ae803c5a4 100644 --- a/src/prepare/providers/uv.rs +++ b/src/prepare/providers/uv.rs @@ -27,14 +27,12 @@ impl PrepareProvider for UvPrepareProvider { } fn sources(&self) -> Vec { - vec![ - self.base.project_root.join("uv.lock"), - self.base.project_root.join("pyproject.toml"), - ] + let root = self.base.config_root(); + vec![root.join("uv.lock"), root.join("pyproject.toml")] } fn outputs(&self) -> Vec { - vec![self.base.project_root.join(".venv")] + vec![self.base.config_root().join(".venv")] } fn prepare_command(&self) -> Result { @@ -46,7 +44,7 @@ impl PrepareProvider for UvPrepareProvider { program: "uv".to_string(), args: vec!["sync".to_string()], env: self.base.config.env.clone(), - cwd: Some(self.base.project_root.clone()), + cwd: Some(self.base.config_root()), description: self .base .config @@ -57,6 +55,6 @@ impl PrepareProvider for UvPrepareProvider { } fn is_applicable(&self) -> bool { - self.base.project_root.join("uv.lock").exists() + self.base.config_root().join("uv.lock").exists() } } diff --git a/src/prepare/providers/yarn.rs b/src/prepare/providers/yarn.rs index 0ce74b5223..22ceb001b8 100644 --- a/src/prepare/providers/yarn.rs +++ b/src/prepare/providers/yarn.rs @@ -27,14 +27,12 @@ impl PrepareProvider for YarnPrepareProvider { } fn sources(&self) -> Vec { - vec![ - self.base.project_root.join("yarn.lock"), - self.base.project_root.join("package.json"), - ] + let root = self.base.config_root(); + vec![root.join("yarn.lock"), root.join("package.json")] } fn outputs(&self) -> Vec { - vec![self.base.project_root.join("node_modules")] + vec![self.base.config_root().join("node_modules")] } fn prepare_command(&self) -> Result { @@ -46,7 +44,7 @@ impl PrepareProvider for YarnPrepareProvider { program: "yarn".to_string(), args: vec!["install".to_string()], env: self.base.config.env.clone(), - cwd: Some(self.base.project_root.clone()), + cwd: Some(self.base.config_root()), description: self .base .config @@ -57,6 +55,6 @@ impl PrepareProvider for YarnPrepareProvider { } fn is_applicable(&self) -> bool { - self.base.project_root.join("yarn.lock").exists() + self.base.config_root().join("yarn.lock").exists() } }