diff --git a/e2e/tasks/test_task_config_includes_templates b/e2e/tasks/test_task_config_includes_templates new file mode 100644 index 0000000000..2ab4dfe0d7 --- /dev/null +++ b/e2e/tasks/test_task_config_includes_templates @@ -0,0 +1,37 @@ +#!/usr/bin/env bash + +# Test Tera rendering and home expansion for task_config.includes. + +mkdir -p root-tasks "$HOME/home-tasks" "$HOME/env-home-tasks" + +cat >root-tasks/task.toml <<'EOF' +[root_include] +run = 'echo "root include"' +EOF + +cat >"$HOME/home-tasks/task.toml" <<'EOF' +[tilde_include] +run = 'echo "tilde include"' +EOF + +cat >"$HOME/env-home-tasks/task.toml" <<'EOF' +[env_home_include] +run = 'echo "env home include"' +EOF + +cat >mise.toml <<'EOF' +[task_config] +includes = [ + "{{ config_root }}/root-tasks/*.toml", + "~/home-tasks/*.toml", + "{{ env.HOME }}/env-home-tasks/*.toml", +] +EOF + +assert_contains "mise tasks" "root_include" +assert_contains "mise tasks" "tilde_include" +assert_contains "mise tasks" "env_home_include" + +assert "mise run root_include" "root include" +assert "mise run tilde_include" "tilde include" +assert "mise run env_home_include" "env home include" diff --git a/src/cli/tasks/ls.rs b/src/cli/tasks/ls.rs index b24e04a372..38d3d82a9e 100644 --- a/src/cli/tasks/ls.rs +++ b/src/cli/tasks/ls.rs @@ -139,7 +139,7 @@ impl TasksLs { && !cfg!(windows) && let Some(cwd) = &*dirs::CWD { - let includes = config::task_includes_for_dir(cwd, &config.config_files); + let includes = config::task_includes_for_dir(cwd, &config.config_files)?; if !find_non_executable_task_files(&includes).is_empty() { warn!( "no tasks found, but non-executable files exist in task directories.\nFiles must be executable to be detected as tasks. Run `chmod +x` on the task files to fix this." diff --git a/src/config/config_file/mise_toml.rs b/src/config/config_file/mise_toml.rs index dd5765935a..61138f63ce 100644 --- a/src/config/config_file/mise_toml.rs +++ b/src/config/config_file/mise_toml.rs @@ -915,6 +915,19 @@ impl ConfigFile for MiseToml { &self.task_config } + fn task_config_includes(&self) -> eyre::Result>> { + self.task_config + .includes + .as_ref() + .map(|includes| { + includes + .iter() + .map(|include| self.parse_template(include)) + .collect() + }) + .transpose() + } + fn experimental_monorepo_root(&self) -> Option { self.experimental_monorepo_root } diff --git a/src/config/config_file/mod.rs b/src/config/config_file/mod.rs index 20f006b5ad..b8d324d5f6 100644 --- a/src/config/config_file/mod.rs +++ b/src/config/config_file/mod.rs @@ -113,6 +113,10 @@ pub trait ConfigFile: Debug + Send + Sync { &DEFAULT_TASK_CONFIG } + fn task_config_includes(&self) -> eyre::Result>> { + Ok(self.task_config().includes.clone()) + } + fn task_templates(&self) -> IndexMap { IndexMap::new() } diff --git a/src/config/mod.rs b/src/config/mod.rs index 581976c074..f1896f28dc 100644 --- a/src/config/mod.rs +++ b/src/config/mod.rs @@ -1928,7 +1928,7 @@ async fn load_local_tasks_with_context( // If no config file exists, still load default task include dirs if !found_config { - let includes = task_includes_for_dir(&subdir, &config.config_files); + let includes = task_includes_for_dir(&subdir, &config.config_files)?; for include in includes { let mut subdir_tasks = load_tasks_includes(&config, &include, &subdir, &None).await?; @@ -2376,8 +2376,10 @@ fn is_glob_pattern(pattern: &str) -> bool { /// Expand a task include pattern (which may be a glob) to a list of paths fn expand_task_include(dir: &Path, pattern: &str) -> Vec { - if is_glob_pattern(pattern) { - match glob(dir, pattern) { + let pattern = file::replace_path(pattern); + let pattern = pattern.to_string_lossy(); + if is_glob_pattern(&pattern) { + match glob(dir, &pattern) { Ok(paths) => paths, Err(err) => { warn!( @@ -2391,7 +2393,7 @@ fn expand_task_include(dir: &Path, pattern: &str) -> Vec { } } else { // Literal path - let path = PathBuf::from(pattern); + let path = PathBuf::from(&*pattern); let resolved = if path.is_absolute() { path } else { @@ -2411,9 +2413,7 @@ async fn load_file_tasks( config_root: &Path, ) -> Result> { let includes = cf - .task_config() - .includes - .clone() + .task_config_includes()? .unwrap_or_else(default_task_includes); let mut tasks = vec![]; @@ -2439,25 +2439,28 @@ async fn load_file_tasks( Ok(tasks) } -pub fn task_includes_for_dir(dir: &Path, config_files: &ConfigMap) -> Vec { +pub fn task_includes_for_dir(dir: &Path, config_files: &ConfigMap) -> Result> { let configs = configs_at_root(dir, config_files); // Find the highest-precedence config that has explicit task_config.includes // and resolve paths relative to that config file's directory let (includes, resolve_dir) = configs .iter() - .find_map(|cf| { - cf.task_config().includes.clone().map(|includes| { + .find_map(|cf| match cf.task_config_includes() { + Ok(Some(includes)) => Some(Ok({ // Resolve relative paths from the config root, not the config file's directory (includes, cf.config_root()) - }) + })), + Ok(None) => None, + Err(err) => Some(Err(err)), }) + .transpose()? .unwrap_or_else(|| { // Default includes should be resolved relative to the search directory (default_task_includes(), dir.to_path_buf()) }); - includes + Ok(includes .into_iter() .flat_map(|p| { // Git URLs are handled by load_file_tasks, not here @@ -2467,7 +2470,7 @@ pub fn task_includes_for_dir(dir: &Path, config_files: &ConfigMap) -> Vec>() + .collect::>()) } pub async fn load_tasks_in_dir( @@ -2480,12 +2483,12 @@ pub async fn load_tasks_in_dir( let (includes, resolve_dir) = configs .iter() - .find_map(|cf| { - cf.task_config() - .includes - .clone() - .map(|includes| (includes, cf.config_root())) + .find_map(|cf| match cf.task_config_includes() { + Ok(Some(includes)) => Some(Ok((includes, cf.config_root()))), + Ok(None) => None, + Err(err) => Some(Err(err)), }) + .transpose()? .unwrap_or_else(|| (default_task_includes(), dir.to_path_buf())); let mut config_tasks = vec![]; diff --git a/src/task/mod.rs b/src/task/mod.rs index 6fc7a919e8..3de564bdd5 100644 --- a/src/task/mod.rs +++ b/src/task/mod.rs @@ -741,7 +741,15 @@ impl Task { let config = Config::get().await.unwrap(); let cwd = dirs::CWD.clone().unwrap_or_default(); let project_root = config.project_root.clone().unwrap_or(cwd); - for dir in config::task_includes_for_dir(&project_root, &config.config_files) { + let task_includes = match config::task_includes_for_dir(&project_root, &config.config_files) + { + Ok(includes) => includes, + Err(err) => { + warn!("failed to resolve task include paths: {err:#}"); + Vec::new() + } + }; + for dir in task_includes { if dir.is_dir() && project_root.join(&dir).exists() { return project_root.join(dir); } diff --git a/src/task/task_list.rs b/src/task/task_list.rs index 7df4abdb35..695a251fc0 100644 --- a/src/task/task_list.rs +++ b/src/task/task_list.rs @@ -240,7 +240,7 @@ async fn err_no_task(config: &Config, name: &str) -> Result<()> { if !cfg!(windows) && let Some(cwd) = &*dirs::CWD { - let includes = config::task_includes_for_dir(cwd, &config.config_files); + let includes = config::task_includes_for_dir(cwd, &config.config_files)?; let non_exec_files = find_non_executable_task_files(&includes); if !non_exec_files.is_empty() { let dirs_with_files: Vec = includes @@ -271,7 +271,7 @@ async fn err_no_task(config: &Config, name: &str) -> Result<()> { ); } if let Some(cwd) = &*dirs::CWD { - let includes = config::task_includes_for_dir(cwd, &config.config_files); + let includes = config::task_includes_for_dir(cwd, &config.config_files)?; let path = includes .iter() .map(|d| d.join(name))