From aba8c043423f25044c4a9f0715650756fc58d6d8 Mon Sep 17 00:00:00 2001 From: jdx <216188+jdx@users.noreply.github.com> Date: Wed, 27 Nov 2024 13:56:35 -0600 Subject: [PATCH] feat: standalone tasks (#3240) Allows running tasks with `mise run ./foo` --- docs/tasks/file-tasks.md | 22 +++++++++++++++ e2e/tasks/test_task_standalone | 24 ++++++++++++++++ src/cli/mod.rs | 1 + src/cli/run.rs | 50 ++++++++++++++++++++++++++++++++-- 4 files changed, 94 insertions(+), 3 deletions(-) create mode 100644 e2e/tasks/test_task_standalone diff --git a/docs/tasks/file-tasks.md b/docs/tasks/file-tasks.md index cebe08a36e..06e0461ecd 100644 --- a/docs/tasks/file-tasks.md +++ b/docs/tasks/file-tasks.md @@ -120,3 +120,25 @@ Also, the original working directory is available in the `MISE_ORIGINAL_CWD` env #!/usr/bin/env bash cd "$MISE_ORIGINAL_CWD" ``` + +## Running tasks directly + +Tasks don't need to be configured as part of a config, you can just run them directly by passing the path to the script: + +```bash +mise run ./path/to/script.sh +``` + +Note that the path must start with `/` or `./` to be considered a file path. (On Windows it can be `C:\` or `.\`) + +## Remote tasks + +Task files can be fetched via http: + +```toml +[tasks.build] +file = "https://example.com/build.sh" +``` + +Currently, they're fetched everytime they're executed, but we may add some cache support later. +This could be extended with other protocols like mentioned in [this ticket](https://github.com/jdx/mise/issues/2488) if there were interest. diff --git a/e2e/tasks/test_task_standalone b/e2e/tasks/test_task_standalone new file mode 100644 index 0000000000..103c6d511b --- /dev/null +++ b/e2e/tasks/test_task_standalone @@ -0,0 +1,24 @@ +#!/usr/bin/env bash + +cat <mytask +#!/usr/bin/env bash +echo "running mytask" +EOF +chmod +x mytask + +assert "mise run ./mytask" "running mytask" + +cat <mise.toml +[tasks.mytask] +file = "./mytask" +EOF + +mkdir -p subdir +cd subdir || exit 1 +assert "mise run mytask" "running mytask" +cd .. || exit 1 + +cat <mise.toml +tasks.mytask.file = "https://mise.jdx.dev/test/mytask" +EOF +assert "mise run mytask" "running mytask" diff --git a/src/cli/mod.rs b/src/cli/mod.rs index 8e4e9791b6..4f70442ab9 100644 --- a/src/cli/mod.rs +++ b/src/cli/mod.rs @@ -328,6 +328,7 @@ impl Cli { raw: self.raw, timings: self.timings, tool: Default::default(), + tmpdir: Default::default(), })); } else if let Some(cmd) = external::COMMANDS.get(&task) { external::execute( diff --git a/src/cli/run.rs b/src/cli/run.rs index 96523c156e..08c6435050 100644 --- a/src/cli/run.rs +++ b/src/cli/run.rs @@ -12,6 +12,7 @@ use crate::cmd::CmdLineRunner; use crate::config::{CONFIG, SETTINGS}; use crate::errors::Error; use crate::file::display_path; +use crate::http::HTTP; use crate::task::{Deps, EitherIntOrBool, GetMatchingExt, Task}; use crate::toolset::{InstallOptions, ToolsetBuilder}; use crate::ui::{ctrlc, prompt, style, time}; @@ -26,6 +27,7 @@ use glob::glob; use itertools::Itertools; #[cfg(unix)] use nix::sys::signal::SIGTERM; +use xx::regex; /// Run task(s) /// @@ -130,10 +132,13 @@ pub struct Run { #[clap(skip)] pub output: TaskOutput, + + #[clap(skip)] + pub tmpdir: PathBuf, } impl Run { - pub fn run(self) -> Result<()> { + pub fn run(mut self) -> Result<()> { if self.task == "-h" { self.get_clap_command().print_help()?; return Ok(()); @@ -143,6 +148,8 @@ impl Run { return Ok(()); } time!("run init"); + let tmpdir = tempfile::tempdir()?; + self.tmpdir = tmpdir.path().to_path_buf(); let task_list = self.get_task_lists()?; time!("run get_task_lists"); self.parallelize_tasks(task_list)?; @@ -173,6 +180,25 @@ impl Run { }) .flat_map(|args| args.split_first().map(|(t, a)| (t.clone(), a.to_vec()))) .map(|(t, args)| { + // can be any of the following: + // - ./path/to/script + // - ~/path/to/script + // - /path/to/script + // - ../path/to/script + // - C:\path\to\script + // - .\path\to\script + if regex!(r#"^((\.*|~)(/|\\)|\w:\\)"#).is_match(&t) { + let path = PathBuf::from(&t); + if path.exists() { + let config_root = CONFIG + .project_root + .clone() + .or_else(|| dirs::CWD.clone()) + .unwrap_or_default(); + let task = Task::from_path(&path, &PathBuf::new(), &config_root)?; + return Ok(vec![task.with_args(args)]); + } + } let tasks = CONFIG .tasks_with_aliases()? .get_matching(&t)? @@ -196,7 +222,7 @@ impl Run { .collect() } - fn parallelize_tasks(mut self, tasks: Vec) -> Result<()> { + fn parallelize_tasks(mut self, mut tasks: Vec) -> Result<()> { time!("paralellize_tasks start"); ctrlc::exit_on_ctrl_c(false); @@ -216,6 +242,7 @@ impl Run { env.insert("root".into(), root.display().to_string()); } + self.fetch_tasks(&mut tasks)?; let tasks = Deps::new(tasks)?; for task in tasks.all() { self.validate_task(task)?; @@ -606,7 +633,7 @@ impl Run { fn validate_task(&self, task: &Task) -> Result<()> { if let Some(path) = &task.file { - if !file::is_executable(path) { + if path.exists() && !file::is_executable(path) { let dp = display_path(path); let msg = format!("Script `{dp}` is not executable. Make it executable?"); if ui::confirm(msg)? { @@ -732,6 +759,23 @@ impl Run { .task_timings .unwrap_or(self.output == TaskOutput::Prefix) } + + fn fetch_tasks(&self, tasks: &mut Vec) -> Result<()> { + let http_re = regex!("https?://"); + for t in tasks { + if let Some(file) = t.file.clone() { + let source = file.to_string_lossy().to_string(); + if http_re.is_match(&source) { + let filename = file.file_name().unwrap().to_string_lossy().to_string(); + let tmp_path = self.tmpdir.join(&filename); + HTTP.download_file(&source, &tmp_path, None)?; + file::make_executable(&tmp_path)?; + t.file = Some(tmp_path); + } + } + } + Ok(()) + } } fn is_glob_pattern(path: &str) -> bool {