diff --git a/docs/cli/index.md b/docs/cli/index.md index e906200cb..b6669ef81 100644 --- a/docs/cli/index.md +++ b/docs/cli/index.md @@ -42,6 +42,12 @@ Show extra output (use -vv for even more) Answer yes to all confirmation prompts +## Flags + +### `--silent` + +Suppress all task output and mise non-error messages + ## Subcommands - [`mise activate [FLAGS] [SHELL_TYPE]`](/cli/activate.md) diff --git a/docs/cli/run.md b/docs/cli/run.md index fdf26e19f..9b6b0281e 100644 --- a/docs/cli/run.md +++ b/docs/cli/run.md @@ -89,6 +89,10 @@ Default to always hide with `MISE_TASK_TIMINGS=0` Don't show extra output +### `-S --silent` + +Don't show any output except for errors + Examples: # Runs the "lint" tasks. This needs to either be defined in mise.toml diff --git a/docs/cli/tasks/run.md b/docs/cli/tasks/run.md index 570f3b9cf..ec908627b 100644 --- a/docs/cli/tasks/run.md +++ b/docs/cli/tasks/run.md @@ -103,6 +103,10 @@ Default to always hide with `MISE_TASK_TIMINGS=0` Don't show extra output +### `-S --silent` + +Don't show any output except for errors + Examples: # Runs the "lint" tasks. This needs to either be defined in mise.toml diff --git a/docs/tasks/index.md b/docs/tasks/index.md index 367e8cb4b..3a4ecedcc 100644 --- a/docs/tasks/index.md +++ b/docs/tasks/index.md @@ -55,69 +55,6 @@ cargo build You can then run the task with `mise run build` like for TOML tasks. See the [file tasks reference](./file-tasks.html) for more information. -## Task Configuration - -The `[task_config]` section of `mise.toml` allows you to customize how `mise` executes and organizes task. - -### Changing the default directory tasks are run from - -```toml -[task_config] -# change the default directory tasks are run from -dir = "{{cwd}}" -``` - -### Including toml tasks files or other directories of file tasks - -```toml -[task_config] -# add toml files containing toml tasks, or file tasks to include when looking for tasks -includes = [ - "tasks.toml", # a task toml file - "mytasks" # a directory containing file tasks (in addition to the default file tasks directories) -] -``` - -If using included task toml files, note that they have a different format than the `mise.toml` file. They are just a list of tasks. -The file should be the same format as the `[tasks]` section of `mise.toml` but without the `[task]` prefix: - -::: code-group - -```toml [tasks.toml] -task1 = "echo task1" -task2 = "echo task2" -task3 = "echo task3" - -[task4] -run = "echo task4" -``` - -::: - -If you want auto-completion/validation in included toml tasks files, you can use the following JSON schema: - -## Vars - -Vars are variables that can be shared between tasks like environment variables but they are not -passed as environment variables to the scripts. They are defined in the `vars` section of the -`mise.toml` file. - -```toml -[vars] -e2e_args = '--headless' - -[tasks.test] -run = './scripts/test-e2e.sh {{vars.e2e_args}}' -``` - -Like most configuration in mise, vars can be defined across several files. So for example, you could -put some vars in your global mise config `~/.config/mise/config.toml`, use them in a task at -`~/src/work/myproject/mise.toml`. You can also override those vars in "later" config files such -as `~/src/work/myproject/mise.local.toml` and they will be used inside tasks of any config file. - -As of this writing vars are only supported in TOML tasks. I want to add support for file tasks, but -I don't want to turn all file tasks into tera templates just for this feature. - ## Environment variables passed to tasks The following environment variables are passed to the task: diff --git a/docs/tasks/task-configuration.md b/docs/tasks/task-configuration.md new file mode 100644 index 000000000..29cb1a3a0 --- /dev/null +++ b/docs/tasks/task-configuration.md @@ -0,0 +1,309 @@ +# Task Configuration + +This is an exhaustive list of all the configuration options available for tasks in `mise.toml` or +as file tasks. + +## Task properties + +All examples are in toml-task format instead of file, however they apply in both except where otherwise noted. + +### `run` + +- **Type**: `string | string[]` + +The command to run. This is the only required property for a task. Note that tasks can be defined in +`mise.toml` in various ways in order to simplify the config, e.g.: these are all equal: + +```toml +tasks.a = "echo hello" +tasks.b = ["echo hello"] +tasks.c.run = "echo hello" +[tasks.d] +run = "echo hello" +[tasks.e] +run = ["echo hello"] +``` + +### `run_windows` + +An alterative script to run when `mise run` is executed on windows: + +```toml +[tasks.build] +run = "cargo build" +run_windows = "cargo build --features windows" +``` + +### `description` + +- **Type**: `string` + +A description of the task. This is used in (among other places) +the help output, completions, `mise run` (without arguments), and `mise tasks`. + +```toml +[tasks.build] +description = "Build the CLI" +run = "cargo build" +``` + +### `alias` + +- **Type**: `string | string[]` + +An alias for the task so you can run it with `mise run ` instead of the full task name. + +```toml +[tasks.build] +alias = "b" # run with `mise run b` or `mise b` +run = "cargo build" +``` + +### `depends` + +- **Type**: `string | string[]` + +Tasks that must be run before this task. This is a list of task names or aliases. Arguments can be +passed to the task, e.g.: `depends = ["build --release"]`. If multiple tasks have the same dependency, +that dependency will only be run once. mise will run whatever it can in parallel (up to [`--jobs`](/cli/run)) +through the use of `depends` and related properties. + +```toml +[tasks.build] +run = "cargo build" +[tasks.test] +depends = ["build"] +run = "cargo test" +``` + +### `depends_post` + +- **Type**: `string | string[]` + +Like `depends` but these tasks run _after_ this task and its dependencies complete. For example, you +may want a `postlint` task that you can run individually without also running `lint`: + +```toml +[tasks.lint] +run = "eslint ." +depends_post = ["postlint"] +[tasks.postlint] +run = "echo 'linting complete'" +``` + +### `wait_for` + +- **Type**: `string | string[]` + +Similar to `depends`, it will wait for these tasks to complete before running however they won't be +added to the list of tasks to run. This is essentially optional dependencies. + +```toml +[tasks.lint] +wait_for = ["render"] # creates some js files, so if it's running, wait for it to finish +run = "eslint ." +``` + +### `env` + +- **Type**: `{ [key]: string | int | bool }` + +Environment variables specific to this task. These will not be passed to `depends` tasks. + +```toml +[tasks.test] +env.TEST_ENV_VAR = "ABC" +run = [ + "echo $TEST_ENV_VAR", + "mise run some-other-task", # running tasks this will _will_ have TEST_ENV_VAR set of course +] +``` + +### `dir` + +- **Type**: `string` +- **Default**: `{{config_root}}` - the directory containing `mise.toml`, or in the case of something like `~/src/myproj/.config/mise.toml`, it will be `~/src/myproj`. + +The directory to run the task from. The most common way this is used is when you want the task to execute +in the user's current directory: + +```toml +[tasks.test] +dir = "{{cwd}}" +run = "cargo test" +``` + +### `hide` + +- **Type**: `bool` +- **Default**: `false` + +Hide the task from help, completion, and other output like `mise tasks`. Useful for deprecated or internal +tasks you don't want others to easily see. + +```toml +[tasks.internal] +hide = true +run = "echo my internal task" +``` + +### `raw` + +- **Type**: `bool` +- **Default**: `false` + +Connects the task directly to the shell's stdin/stdout/stderr. This is useful for tasks that need to +accept input or output in a way that mise's normal task handling doesn't support. This is not recommended +to use because it really screws up the output whenever mise runs tasks in parallel. Ensure when using +this that no other tasks are running at the same time. + +In the future we could have a property like `single = true` or something that prevents multiple tasks +from running at the same time. If that sounds useful, search/file a ticket. + +### `sources` + +- **Type**: `string | string[]` + +Files or directories that this task uses as input, if this and `outputs` is defined, mise will skip +executing tasks where the modification time of the oldest output file is newer than the modification +time of the newest source file. This is useful for tasks that are expensive to run and only need to +be run when their inputs change. + +The task itself will be automatically added as a source, so if you edit the definition that will also +cause the task to be run. + +This is also used in `mise watch` to know which files/directories to watch. + +This can be specified with relative paths to the config file and/or with glob patterns, e.g.: `src/**/*.rs`. +Ensure you don't go crazy with adding a ton of files in a glob though—mise has to scan each and every one to check +the timestamp. + +```toml +[tasks.build] +run = "cargo build" +sources = ["Cargo.toml", "src/**/*.rs"] +outputs = ["target/debug/mycli"] +``` + +Running the above will only execute `cargo build` if `mise.toml`, `Cargo.toml`, or any ".rs" file in the `src` directory +has changed since the last build. + +### `outputs` + +- **Type**: `string | string[] | { auto = true }` + +The counterpart to `sources`, these are the files or directories that the task will create/modify after +it executes. + +`auto = true` is an altnernative to specifying output files manually. In that case, mise will touch +an internally tracked file based on the hash of the task definition (stored in `~/.local/state/mise/task-outputs/` if you're curious). +This is useful if you want `mise run` to execute when sources change but don't want to have to manually `touch` +a file for `sources` to work. + +```toml +[tasks.build] +run = "cargo build" +sources = ["Cargo.toml", "src/**/*.rs"] +outputs = { auto = true } +``` + +### `shell` + +- **Type**: `string` +- **Default**: [`unix_default_inline_shell_args`](/configuration/settings.html#unix_default_inline_shell_args) or [`windows_default_inline_shell_args`](/configuration/settings.html#windows_default_inline_shell_args) +- **Note**: Only applies to toml-tasks. + +The shell to use to run the task. This is useful if you want to run a task with a different shell than +the default such as `fish`, `zsh`, or `pwsh`. Generally though, it's recommended to use a [shebang](/tasks/toml-tasks.html#shell-shebang) instead +because that will allow IDEs with mise support to show syntax highlighting and linting for the script. + +```toml +[tasks.hello] +run = ''' +#!/usr/bin/env node +console.log('hello world') +''' +``` + +### `quiet` + +- **Type**: `bool` +- **Default**: `false` + +Suppress mise's output for the task such as showing the command that is run, e.g.: `[build] $ cargo build`. +When this is set, mise won't show any output other than what the script itself outputs. If you'd also +like to hide even the output that the task emits, use [`silent`](#silent). + +### `silent` + +- **Type**: `bool | "stdout" | "stderr"` +- **Default**: `false` + +Suppress all output from the task. If set to `"stdout"` or `"stderr"`, only that stream will be suppressed. + +## Vars + +Vars are variables that can be shared between tasks like environment variables but they are not +passed as environment variables to the scripts. They are defined in the `vars` section of the +`mise.toml` file. + +```toml +[vars] +e2e_args = '--headless' + +[tasks.test] +run = './scripts/test-e2e.sh {{vars.e2e_args}}' +``` + +Like most configuration in mise, vars can be defined across several files. So for example, you could +put some vars in your global mise config `~/.config/mise/config.toml`, use them in a task at +`~/src/work/myproject/mise.toml`. You can also override those vars in "later" config files such +as `~/src/work/myproject/mise.local.toml` and they will be used inside tasks of any config file. + +As of this writing vars are only supported in TOML tasks. I want to add support for file tasks, but +I don't want to turn all file tasks into tera templates just for this feature. + +## `[task_config]` options + +Options available in the top-level `mise.toml` `[task_config]` section. These apply to all tasks which +are included by that config file or use the same root directory, e.g.: `~/src/myprojec/mise.toml`'s `[task_config]` +applies to file tasks like `~/src/myproject/mise-tasks/mytask` but not to tasks in `~/src/myproject/subproj/mise.toml`. + +### `task_config.dir` + +Change the default directory tasks are run from. + +```toml +[task_config] +dir = "{{cwd}}" +``` + +### `task_config.includes` + +Add toml files containing toml tasks, or file tasks to include when looking for tasks. + +```toml +[task_config] +includes = [ + "tasks.toml", # a task toml file + "mytasks" # a directory containing file tasks (in addition to the default file tasks directories) +] +``` + +If using included task toml files, note that they have a different format than the `mise.toml` file. They are just a list of tasks. +The file should be the same format as the `[tasks]` section of `mise.toml` but without the `[task]` prefix: + +::: code-group + +```toml [tasks.toml] +task1 = "echo task1" +task2 = "echo task2" +task3 = "echo task3" + +[task4] +run = "echo task4" +``` + +::: + +If you want auto-completion/validation in included toml tasks files, you can use the following JSON schema: diff --git a/e2e/tasks/test_task_deps b/e2e/tasks/test_task_deps index e95e74b63..20f6779ba 100644 --- a/e2e/tasks/test_task_deps +++ b/e2e/tasks/test_task_deps @@ -16,10 +16,10 @@ assert "mise run d" "a c d" -assert "mise run d ::: b" "[a] a -[b] b -[c] c -[d] d" +assert "mise run d ::: b" "a +b +c +d" cat <mise.toml [tasks."hello:1"] diff --git a/e2e/tasks/test_task_run_depends b/e2e/tasks/test_task_run_depends new file mode 100644 index 000000000..5652dc6f9 --- /dev/null +++ b/e2e/tasks/test_task_run_depends @@ -0,0 +1,25 @@ +#!/usr/bin/env bash + +cat <mise.toml +[tasks.build] +run = 'echo build' +[tasks.all] +depends = ['build a', 'build b', 'build c'] +EOF +assert "mise run all | sort" "[build] build a +[build] build b +[build] build c" + +cat <mise.toml +[tasks.build1] +run = 'echo build' +[tasks.build2] +depends = ['build1 a'] +run = 'echo build' +[tasks.all] +depends = ['build1 a', 'build2 b'] +run = "echo all" +EOF +assert "mise run all" "build a +build b +all" diff --git a/e2e/tasks/test_task_run_output b/e2e/tasks/test_task_run_output new file mode 100644 index 000000000..7f613ea6d --- /dev/null +++ b/e2e/tasks/test_task_run_output @@ -0,0 +1,75 @@ +#!/usr/bin/env bash + +cat <mise.toml +[tasks.a] +run = 'echo running a' +[tasks.b] +depends = 'a' +run = 'echo running b' +[tasks.c] +depends = ['b'] +run = 'echo running c' +[tasks.all] +depends = ['a', 'b', 'c'] +depends_post = 'z' +run = 'echo running all' +[tasks.d] +run = 'echo running d' +[tasks.z] +run = 'echo running z' +EOF + +MISE_TASK_OUTPUT=silent assert_empty "mise run all" "" +MISE_TASK_OUTPUT=quiet assert "mise run all" "running a +running b +running c +running all +running z" +MISE_TASK_OUTPUT=interleave assert "mise run all" "running a +running b +running c +running all +running z" +MISE_TASK_OUTPUT=prefix assert "mise run all" "[a] running a +[b] running b +[c] running c +[all] running all +[z] running z" +# defaults to interleave if linear depedency graph +assert "mise run all" "running a +running b +running c +running all +running z" +# now the graph isn't linear so it uses prefix +assert_contains "mise run a ::: d" "[a] running a" +assert_contains "mise run a ::: d" "[d] running d" +assert "mise task deps" "a +all +├── c +│ └── b +│ └── a +├── b +│ └── a +└── a +b +└── a +c +└── b + └── a +d +z +├── all +│ ├── c +│ │ └── b +│ │ └── a +│ ├── b +│ │ └── a +│ └── a +├── a +├── b +│ └── a +├── c +│ └── b +│ └── a +└── d" diff --git a/man/man1/mise.1 b/man/man1/mise.1 index 2012ed606..31d7a0d7d 100644 --- a/man/man1/mise.1 +++ b/man/man1/mise.1 @@ -4,7 +4,7 @@ .SH NAME mise \- The front\-end to your dev env .SH SYNOPSIS -\fBmise\fR [\fB\-C\fR|\fB\-\-cd\fR] [\fB\-E\fR|\fB\-\-env\fR] [\fB\-j\fR|\fB\-\-jobs\fR] [\fB\-q\fR|\fB\-\-quiet\fR] [\fB\-\-raw\fR] [\fB\-v\fR|\fB\-\-verbose\fR]... [\fB\-y\fR|\fB\-\-yes\fR] [\fB\-h\fR|\fB\-\-help\fR] [\fITASK\fR] [\fITASK_ARGS\fR] [\fIsubcommands\fR] +\fBmise\fR [\fB\-C\fR|\fB\-\-cd\fR] [\fB\-E\fR|\fB\-\-env\fR] [\fB\-j\fR|\fB\-\-jobs\fR] [\fB\-q\fR|\fB\-\-quiet\fR] [\fB\-\-raw\fR] [\fB\-\-silent\fR] [\fB\-v\fR|\fB\-\-verbose\fR]... [\fB\-y\fR|\fB\-\-yes\fR] [\fB\-h\fR|\fB\-\-help\fR] [\fITASK\fR] [\fITASK_ARGS\fR] [\fIsubcommands\fR] .SH DESCRIPTION .PP mise is a tool for managing runtime versions. https://github.com/jdx/mise @@ -35,6 +35,9 @@ Suppress non\-error messages \fB\-\-raw\fR Read/write directly to stdin/stdout/stderr instead of by line .TP +\fB\-\-silent\fR +Suppress all task output and mise non\-error messages +.TP \fB\-v\fR, \fB\-\-verbose\fR Show extra output (use \-vv for even more) .TP diff --git a/mise.usage.kdl b/mise.usage.kdl index d80ffd18d..f2f4a91b1 100644 --- a/mise.usage.kdl +++ b/mise.usage.kdl @@ -42,6 +42,7 @@ flag "-t --tool" help="Tool(s) to run in addition to what is in mise.toml files } flag "-q --quiet" help="Suppress non-error messages" global=true flag "--raw" help="Read/write directly to stdin/stdout/stderr instead of by line" global=true +flag "--silent" help="Suppress all task output and mise non-error messages" flag "--timings" help="Shows elapsed time after each task completes" hide=true { long_help "Shows elapsed time after each task completes\n\nDefault to always show with `MISE_TASK_TIMINGS=1`" } @@ -1019,6 +1020,7 @@ The name of the script will be the name of the tasks. long_help "Hides elapsed time after each task completes\n\nDefault to always hide with `MISE_TASK_TIMINGS=0`" } flag "-q --quiet" help="Don't show extra output" + flag "-S --silent" help="Don't show any output except for errors" mount run="mise tasks --usage" } cmd "self-update" help="Updates mise itself." { @@ -1392,6 +1394,7 @@ The name of the script will be the name of the tasks. long_help "Hides elapsed time after each task completes\n\nDefault to always hide with `MISE_TASK_TIMINGS=0`" } flag "-q --quiet" help="Don't show extra output" + flag "-S --silent" help="Don't show any output except for errors" arg "[TASK]" help="Tasks to run\nCan specify multiple tasks by separating with `:::`\ne.g.: mise run task1 arg1 arg2 ::: task2 arg1 arg2" default="default" arg "[ARGS]..." help="Arguments to pass to the tasks. Use \":::\" to separate tasks" var=true mount run="mise tasks --usage" diff --git a/schema/mise-task.json b/schema/mise-task.json index 7b1d3b706..b63482e75 100644 --- a/schema/mise-task.json +++ b/schema/mise-task.json @@ -40,8 +40,40 @@ "depends": { "description": "other tasks to run before this task", "items": { - "description": "task to run before this task", - "type": "string" + "oneOf": [ + { + "description": "task with args to run before this task", + "type": "string" + }, + { + "description": "task with args to run before this task", + "items": { + "description": "task name and args", + "type": "string" + }, + "type": "array" + } + ] + }, + "type": "array" + }, + "depends_post": { + "description": "other tasks to run after this task", + "items": { + "oneOf": [ + { + "description": "task with args to run after this task", + "type": "string" + }, + { + "description": "task with args to run after this task", + "items": { + "description": "task name and args", + "type": "string" + }, + "type": "array" + } + ] }, "type": "array" }, diff --git a/schema/mise.json b/schema/mise.json index 91b739579..b7864ae5a 100644 --- a/schema/mise.json +++ b/schema/mise.json @@ -650,6 +650,10 @@ "description": "Path to a file containing custom tool shorthands.", "type": "string" }, + "silent": { + "description": "Suppress all `mise run|watch` output except errors—including what tasks output.", + "type": "boolean" + }, "status": { "additionalProperties": false, "properties": { @@ -815,8 +819,40 @@ "depends": { "description": "other tasks to run before this task", "items": { - "description": "task to run before this task", - "type": "string" + "oneOf": [ + { + "description": "task with args to run before this task", + "type": "string" + }, + { + "description": "task with args to run before this task", + "items": { + "description": "task name and args", + "type": "string" + }, + "type": "array" + } + ] + }, + "type": "array" + }, + "depends_post": { + "description": "other tasks to run after this task", + "items": { + "oneOf": [ + { + "description": "task with args to run after this task", + "type": "string" + }, + { + "description": "task with args to run after this task", + "items": { + "description": "task name and args", + "type": "string" + }, + "type": "array" + } + ] }, "type": "array" }, diff --git a/settings.toml b/settings.toml index 0457de29c..bf953b3ac 100644 --- a/settings.toml +++ b/settings.toml @@ -841,6 +841,11 @@ node = "https://github.com/my-org/mise-node.git" """ +[silent] +env = "MISE_SILENT" +type = "Bool" +description = "Suppress all `mise run|watch` output except errors—including what tasks output." + [status.missing_tools] env = "MISE_STATUS_MESSAGE_MISSING_TOOLS" type = "String" @@ -891,6 +896,7 @@ description = "Paths that mise will not look for tasks in." [task_output] env = "MISE_TASK_OUTPUT" type = "String" +rust_type = "crate::cli::run::TaskOutput" optional = true description = "Change output style when executing tasks." enum = [ diff --git a/src/cli/mod.rs b/src/cli/mod.rs index b9a9d4ade..6fb959e4a 100644 --- a/src/cli/mod.rs +++ b/src/cli/mod.rs @@ -45,8 +45,8 @@ mod render_help; #[cfg(feature = "clap_mangen")] mod render_mangen; mod reshim; -mod run; -pub(crate) mod self_update; +pub mod run; +pub mod self_update; mod set; mod settings; mod shell; @@ -118,7 +118,13 @@ pub struct Cli { pub shell: Option, /// Tool(s) to run in addition to what is in mise.toml files /// e.g.: node@20 python@3.10 - #[clap(short, long, hide = true, value_name = "TOOL@VERSION")] + #[clap( + short, + long, + hide = true, + value_name = "TOOL@VERSION", + env = "MISE_QUIET" + )] pub tool: Vec, /// Suppress non-error messages #[clap(short = 'q', long, global = true, overrides_with = "verbose")] @@ -126,6 +132,9 @@ pub struct Cli { /// Read/write directly to stdin/stdout/stderr instead of by line #[clap(long, global = true)] pub raw: bool, + /// Suppress all task output and mise non-error messages + #[clap(long)] + pub silent: bool, /// Shows elapsed time after each task completes /// /// Default to always show with `MISE_TASK_TIMINGS=1` @@ -341,6 +350,7 @@ impl Cli { prefix: self.prefix, shell: self.shell, quiet: self.quiet, + silent: self.silent, raw: self.raw, timings: self.timings, tmpdir: Default::default(), diff --git a/src/cli/run.rs b/src/cli/run.rs index 0c1e5c083..43aed1ab6 100644 --- a/src/cli/run.rs +++ b/src/cli/run.rs @@ -133,9 +133,13 @@ pub struct Run { pub no_timings: bool, /// Don't show extra output - #[clap(long, short, verbatim_doc_comment)] + #[clap(long, short, verbatim_doc_comment, env = "MISE_QUIET")] pub quiet: bool, + /// Don't show any output except for errors + #[clap(long, short = 'S', verbatim_doc_comment, env = "MISE_SILENT")] + pub silent: bool, + #[clap(skip)] pub is_linear: bool, @@ -210,12 +214,7 @@ impl Run { let num_tasks = tasks.all().count(); self.is_linear = tasks.is_linear(); - if let Some(task) = tasks.all().next() { - self.output = self.output(task)?; - if let TaskOutput::Quiet = self.output { - self.quiet = true; - } - } + self.output = self.output(None); let tasks = Mutex::new(tasks); let timer = std::time::Instant::now(); @@ -286,13 +285,13 @@ impl Run { fn run_task(&self, env: &BTreeMap, task: &Task) -> Result<()> { let prefix = task.estyled_prefix(); if SETTINGS.task_skip.contains(&task.name) { - if !self.quiet && !task.quiet { + if !self.quiet(Some(task)) { eprintln!("{prefix} skipping task"); } return Ok(()); } if !self.force && self.sources_are_fresh(task)? { - if !self.quiet && !task.quiet { + if !self.quiet(Some(task)) { eprintln!("{prefix} sources up-to-date, skipping"); } return Ok(()); @@ -368,7 +367,7 @@ impl Run { .bright() .to_string(), ); - if !self.quiet && !task.quiet { + if !self.quiet(Some(task)) { eprintln!("{prefix} {cmd}"); } @@ -467,7 +466,7 @@ impl Run { let cmd = format!("{} {}", display_path(file), args.join(" ")); let cmd = trunc(&style::ebold(format!("$ {cmd}")).bright().to_string()); - if !self.quiet && !task.quiet { + if !self.quiet(Some(task)) { eprintln!("{prefix} {cmd}"); } @@ -498,9 +497,9 @@ impl Run { let mut cmd = CmdLineRunner::new(program.clone()) .args(args) .envs(env) - .raw(self.raw(task)); + .raw(self.raw(Some(task))); cmd.with_pass_signals(); - match self.output { + match self.output(Some(task)) { TaskOutput::Prefix => cmd = cmd.prefix(format!("{prefix} ")), TaskOutput::Quiet | TaskOutput::Interleave => { cmd = cmd @@ -508,6 +507,9 @@ impl Run { .stdout(Stdio::inherit()) .stderr(Stdio::inherit()) } + TaskOutput::Silent => { + cmd = cmd.stdout(Stdio::null()).stderr(Stdio::null()); + } } let dir = self.cwd(task)?; if !dir.exists() { @@ -526,24 +528,38 @@ impl Run { Ok(()) } - fn output(&self, task: &Task) -> Result { - if self.quiet { - Ok(TaskOutput::Quiet) + fn output(&self, task: Option<&Task>) -> TaskOutput { + if self.silent(task) { + TaskOutput::Silent + } else if self.quiet(task) { + TaskOutput::Quiet } else if self.prefix { - Ok(TaskOutput::Prefix) + TaskOutput::Prefix } else if self.interleave { - Ok(TaskOutput::Interleave) - } else if let Some(output) = &SETTINGS.task_output { - Ok(output.parse()?) + TaskOutput::Interleave + } else if let Some(output) = SETTINGS.task_output { + output } else if self.raw(task) || self.jobs() == 1 || self.is_linear { - Ok(TaskOutput::Interleave) + TaskOutput::Interleave } else { - Ok(TaskOutput::Prefix) + TaskOutput::Prefix } } - fn raw(&self, task: &Task) -> bool { - self.raw || task.raw || SETTINGS.raw + fn silent(&self, task: Option<&Task>) -> bool { + self.silent || SETTINGS.silent || self.output.is_silent() || task.is_some_and(|t| t.silent) + } + + fn quiet(&self, task: Option<&Task>) -> bool { + self.quiet + || SETTINGS.quiet + || self.output.is_quiet() + || task.is_some_and(|t| t.quiet) + || self.silent(task) + } + + fn raw(&self, task: Option<&Task>) -> bool { + self.raw || SETTINGS.raw || task.is_some_and(|t| t.raw) } fn jobs(&self) -> usize { @@ -633,7 +649,8 @@ impl Run { Ok(Config::get() .project_root .clone() - .unwrap_or_else(|| env::current_dir().unwrap())) + .or_else(|| dirs::CWD.clone()) + .unwrap_or_default()) } } @@ -646,7 +663,7 @@ impl Run { } fn timings(&self) -> bool { - !self.quiet + !self.quiet(None) && !self.no_timings && SETTINGS .task_timings @@ -760,13 +777,25 @@ static AFTER_LONG_HELP: &str = color_print::cstr!( "# ); -#[derive(Debug, Default, PartialEq, strum::EnumString)] +#[derive( + Debug, + Default, + Clone, + Copy, + PartialEq, + strum::EnumString, + strum::EnumIs, + serde::Serialize, + serde::Deserialize, +)] +#[serde(rename_all = "snake_case")] #[strum(serialize_all = "snake_case")] pub enum TaskOutput { #[default] Prefix, Interleave, Quiet, + Silent, } fn trunc(msg: &str) -> String { diff --git a/src/cli/tasks/info.rs b/src/cli/tasks/info.rs index 05a1372ef..c497cf133 100644 --- a/src/cli/tasks/info.rs +++ b/src/cli/tasks/info.rs @@ -1,4 +1,5 @@ use eyre::{bail, Result}; +use itertools::Itertools; use serde_json::json; use crate::config::Config; @@ -57,7 +58,10 @@ impl TasksInfo { info::inline_section("Properties", properties.join(", "))?; } if !task.depends.is_empty() { - info::inline_section("Depends on", task.depends.join(", "))?; + info::inline_section("Depends on", task.depends.iter().join(", "))?; + } + if !task.depends_post.is_empty() { + info::inline_section("Depends post", task.depends_post.iter().join(", "))?; } if let Some(dir) = &task.dir { info::inline_section("Directory", display_path(dir))?; @@ -91,7 +95,8 @@ impl TasksInfo { "aliases": task.aliases.join(", "), "description": task.description.to_string(), "source": task.config_source, - "depends": task.depends.join(", "), + "depends": task.depends.iter().join(", "), + "depends_post": task.depends_post.iter().join(", "), "env": task.env, "dir": task.dir, "hide": task.hide, diff --git a/src/config/config_file/toml.rs b/src/config/config_file/toml.rs index 780c69910..fecea7e72 100644 --- a/src/config/config_file/toml.rs +++ b/src/config/config_file/toml.rs @@ -38,7 +38,7 @@ impl<'a> TomlParser<'a> { } pub fn parse_array(&self, key: &str) -> eyre::Result>> where - T: Default + From, + T: From, { self.table .get(key) diff --git a/src/task/deps.rs b/src/task/deps.rs index 0f8e648d7..b28199c3f 100644 --- a/src/task/deps.rs +++ b/src/task/deps.rs @@ -1,18 +1,22 @@ -use crate::config::Config; use crate::task::Task; use crossbeam_channel as channel; use itertools::Itertools; use petgraph::graph::DiGraph; use petgraph::Direction; use std::collections::{HashMap, HashSet}; +use std::iter::once; -#[derive(Debug)] +#[derive(Debug, Clone)] pub struct Deps { pub graph: DiGraph, - sent: HashSet, // tasks that have already started so should not run again + sent: HashSet<(String, Vec)>, // tasks+args that have already started so should not run again tx: channel::Sender>, } +fn task_key(task: &Task) -> (String, Vec) { + (task.name.clone(), task.args.clone()) +} + /// manages a dependency graph of tasks so `mise run` knows what to run next impl Deps { pub fn new(tasks: Vec) -> eyre::Result { @@ -20,37 +24,37 @@ impl Deps { let mut indexes = HashMap::new(); let mut stack = vec![]; + let mut add_idx = |task: &Task, graph: &mut DiGraph| { + *indexes + .entry(task_key(task)) + .or_insert_with(|| graph.add_node(task.clone())) + }; + // first we add all tasks to the graph, create a stack of work for this function, and // store the index of each task in the graph for t in &tasks { stack.push(t.clone()); - indexes - .entry(t.name.clone()) - .or_insert_with(|| graph.add_node(t.clone())); + add_idx(t, &mut graph); } - let config = Config::get(); - let all_tasks_to_run: Vec<&Task> = tasks - .iter() + let all_tasks_to_run: Vec = tasks + .into_iter() .map(|t| { - eyre::Ok( - [t].into_iter() - .chain(t.all_depends(&config)?) - .collect::>(), - ) + let depends = t.all_depends()?; + eyre::Ok(once(t).chain(depends).collect::>()) }) .flatten_ok() .collect::>>()?; while let Some(a) = stack.pop() { - let a_idx = *indexes - .entry(a.name.clone()) - .or_insert_with(|| graph.add_node(a.clone())); - for b in a.resolve_depends(&Config::get(), &all_tasks_to_run)? { - let b_idx = *indexes - .entry(b.name.clone()) - .or_insert_with(|| graph.add_node(b.clone())); - if !graph.contains_edge(a_idx, b_idx) { - graph.add_edge(a_idx, b_idx, ()); - } + let a_idx = add_idx(&a, &mut graph); + let (pre, post) = a.resolve_depends(&all_tasks_to_run)?; + for b in pre { + let b_idx = add_idx(&b, &mut graph); + graph.update_edge(a_idx, b_idx, ()); + stack.push(b.clone()); + } + for b in post { + let b_idx = add_idx(&b, &mut graph); + graph.update_edge(b_idx, a_idx, ()); stack.push(b.clone()); } } @@ -59,21 +63,15 @@ impl Deps { Ok(Self { graph, tx, sent }) } - fn leaves(&self) -> Vec { - self.graph - .externals(Direction::Outgoing) - .map(|idx| self.graph[idx].clone()) - .collect() - } - /// main method to emit tasks that no longer have dependencies being waited on fn emit_leaves(&mut self) { - let leaves = self.leaves().into_iter().collect_vec(); + let leaves = leaves(&self.graph).into_iter().collect_vec(); for task in leaves { - if self.sent.contains(&task.name) { + let key = (task.name.clone(), task.args.clone()); + if self.sent.contains(&key) { continue; } - self.sent.insert(task.name.clone()); + self.sent.insert(key); if let Err(e) = self.tx.send(Some(task)) { trace!("Error sending task: {e:?}"); } @@ -101,34 +99,46 @@ impl Deps { // #[requires(self.graph.node_count() > 0)] // #[ensures(self.graph.node_count() == old(self.graph.node_count()) - 1)] pub fn remove(&mut self, task: &Task) { - if let Some(idx) = self - .graph - .node_indices() - .find(|&idx| &self.graph[idx] == task) - { + if let Some(idx) = self.node_idx(task) { self.graph.remove_node(idx); self.emit_leaves(); } } + fn node_idx(&self, task: &Task) -> Option { + self.graph + .node_indices() + .find(|&idx| &self.graph[idx] == task) + } + pub fn all(&self) -> impl Iterator { self.graph.node_indices().map(|idx| &self.graph[idx]) } pub fn is_linear(&self) -> bool { - !self.graph.node_indices().any(|idx| { - self.graph - .neighbors_directed(idx, Direction::Outgoing) - .count() - > 1 - }) + let mut graph = self.graph.clone(); + // pop dependencies off, if we get multiple dependencies at once it's not linear + loop { + let leaves = leaves(&graph); + if leaves.is_empty() { + return true; + } else if leaves.len() > 1 { + return false; + } else { + let idx = self + .graph + .node_indices() + .find(|&idx| graph[idx] == leaves[0]) + .unwrap(); + graph.remove_node(idx); + } + } } +} - // fn pop(&'a mut self) -> Option<&'a Tasks> { - // if let Some(leaf) = self.leaves().first() { - // self.remove(&leaf.clone()) - // } else { - // None - // } - // } +fn leaves(graph: &DiGraph) -> Vec { + graph + .externals(Direction::Outgoing) + .map(|idx| graph[idx].clone()) + .collect() } diff --git a/src/task/mod.rs b/src/task/mod.rs index 3b0aba726..4848e9ada 100644 --- a/src/task/mod.rs +++ b/src/task/mod.rs @@ -25,12 +25,14 @@ use std::{ffi, fmt, path}; use xx::regex; mod deps; +mod task_dep; mod task_script_parser; use crate::config::config_file::ConfigFile; use crate::file::display_path; use crate::ui::style; pub use deps::Deps; +use task_dep::TaskDep; #[derive(Debug, Clone, Eq, PartialEq, Deserialize)] pub struct Task { @@ -46,10 +48,12 @@ pub struct Task { pub cf: Option>, #[serde(skip)] pub config_root: Option, - #[serde(default)] - pub depends: Vec, - #[serde(default)] - pub wait_for: Vec, + #[serde(default, deserialize_with = "deserialize_arr")] + pub depends: Vec, + #[serde(default, deserialize_with = "deserialize_arr")] + pub depends_post: Vec, + #[serde(default, deserialize_with = "deserialize_arr")] + pub wait_for: Vec, #[serde(default)] pub env: BTreeMap, #[serde(default)] @@ -66,6 +70,8 @@ pub struct Task { pub shell: Option, #[serde(default)] pub quiet: bool, + #[serde(default)] + pub silent: bool, // normal type #[serde(default, deserialize_with = "deserialize_arr")] @@ -166,6 +172,7 @@ impl Task { task.sources = p.parse_array("sources")?.unwrap_or_default(); task.outputs = p.parse_array("outputs")?.unwrap_or_default(); task.depends = p.parse_array("depends")?.unwrap_or_default(); + task.depends_post = p.parse_array("depends_post")?.unwrap_or_default(); task.wait_for = p.parse_array("wait_for")?.unwrap_or_default(); task.dir = p.parse_str("dir")?; task.env = p.parse_env("env")?.unwrap_or_default(); @@ -186,19 +193,6 @@ impl Task { project_root.join("mise-tasks") } - // pub fn args(&self) -> impl Iterator { - // if let Some(script) = &self.script { - // // TODO: cli_args - // vec!["-c".to_string(), script.to_string()].into_iter() - // } else { - // self.args - // .iter() - // .chain(self.cli_args.iter()) - // .cloned() - // .collect_vec() - // .into_iter() - // } - // } pub fn with_args(mut self, args: Vec) -> Self { self.args = args; self @@ -216,41 +210,52 @@ impl Task { } } - pub fn all_depends<'a>(&self, config: &'a Config) -> Result> { + pub fn all_depends(&self) -> Result> { + let config = Config::get(); let tasks = config.tasks_with_aliases()?; - let mut depends: Vec<&Task> = self + let mut depends: Vec = self .depends .iter() - .map(|pat| match_tasks(&tasks, pat)) + .chain(self.depends_post.iter()) + .map(|td| match_tasks(&tasks, td)) .flatten_ok() .filter_ok(|t| t.name != self.name) .collect::>>()?; for dep in depends.clone() { - depends.extend(dep.all_depends(config)?); + depends.extend(dep.all_depends()?); } Ok(depends) } - pub fn resolve_depends<'a>( - &self, - config: &'a Config, - tasks_to_run: &[&Task], - ) -> Result> { - let tasks_to_run: HashSet<&Task> = tasks_to_run.iter().copied().collect(); + pub fn resolve_depends(&self, tasks_to_run: &[Task]) -> Result<(Vec, Vec)> { + let config = Config::get(); + let tasks_to_run: HashSet<&Task> = tasks_to_run.iter().collect(); let tasks = config.tasks_with_aliases()?; - self.wait_for + let depends = self + .depends .iter() - .map(|pat| match_tasks(&tasks, pat)) + .map(|td| match_tasks(&tasks, td)) + .flatten_ok() + .collect_vec(); + let wait_for = self + .wait_for + .iter() + .map(|td| match_tasks(&tasks, td)) + .flatten_ok() + .filter_ok(|t| tasks_to_run.contains(t)) + .collect_vec(); + let depends_post = tasks_to_run + .iter() + .flat_map(|t| t.depends_post.iter().map(|td| match_tasks(&tasks, td))) .flatten_ok() - .filter_ok(|t| tasks_to_run.contains(*t)) - .chain( - self.depends - .iter() - .map(|pat| match_tasks(&tasks, pat)) - .flatten_ok(), - ) .filter_ok(|t| t.name != self.name) - .collect() + .collect::>>()?; + let depends = depends + .into_iter() + .chain(wait_for) + .filter_ok(|t| t.name != self.name) + .collect::>()?; + Ok((depends, depends_post)) } pub fn parse_usage_spec(&self, cwd: Option) -> Result<(usage::Spec, Vec)> { @@ -276,7 +281,8 @@ impl Task { && spec.cmd.before_help_long.is_none() && !self.depends.is_empty() { - spec.cmd.before_help_long = Some(format!("- Depends: {}", self.depends.join(", "))); + spec.cmd.before_help_long = + Some(format!("- Depends: {}", self.depends.iter().join(", "))); } spec.cmd.usage = spec.cmd.usage(); Ok((spec, scripts)) @@ -401,10 +407,18 @@ fn name_from_path(prefix: impl AsRef, path: impl AsRef) -> Result(tasks: &BTreeMap, pat: &str) -> Result> { - let matches = tasks.get_matching(pat)?.into_iter().cloned().collect_vec(); +fn match_tasks(tasks: &BTreeMap, td: &TaskDep) -> Result> { + let matches = tasks + .get_matching(&td.task)? + .into_iter() + .map(|t| { + let mut t = (*t).clone(); + t.args = td.args.clone(); + t + }) + .collect_vec(); if matches.is_empty() { - return Err(eyre!("task not found: {pat}")); + return Err(eyre!("task not found: {td}")); }; Ok(matches) @@ -420,6 +434,7 @@ impl Default for Task { cf: None, config_root: None, depends: vec![], + depends_post: vec![], wait_for: vec![], env: BTreeMap::new(), dir: None, @@ -428,6 +443,7 @@ impl Default for Task { sources: vec![], outputs: vec![], shell: None, + silent: false, run: vec![], run_windows: vec![], args: vec![], @@ -462,13 +478,17 @@ impl PartialOrd for Task { impl Ord for Task { fn cmp(&self, other: &Self) -> Ordering { - self.name.cmp(&other.name) + match self.name.cmp(&other.name) { + Ordering::Equal => self.args.cmp(&other.args), + o => o, + } } } impl Hash for Task { fn hash(&self, state: &mut H) { self.name.hash(state); + self.args.iter().for_each(|arg| arg.hash(state)); } } @@ -479,7 +499,7 @@ impl TreeItem for (&Graph, NodeIndex) { if let Some(w) = self.0.node_weight(self.1) { miseprint!("{}", w.name)?; } - std::io::Result::Ok(()) + Ok(()) } fn children(&self) -> Cow<[Self::Child]> { diff --git a/src/task/task_dep.rs b/src/task/task_dep.rs new file mode 100644 index 000000000..4175fd40e --- /dev/null +++ b/src/task/task_dep.rs @@ -0,0 +1,117 @@ +use crate::config::config_file::toml::deserialize_arr; +use itertools::Itertools; +use serde::ser::SerializeSeq; +use serde::{Deserialize, Deserializer, Serialize}; +use std::fmt; +use std::fmt::{Display, Formatter}; +use std::str::FromStr; + +#[derive(Debug, Clone, Eq, PartialEq)] +pub struct TaskDep { + pub task: String, + pub args: Vec, +} + +impl Display for TaskDep { + fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result { + write!(f, "{}", self.task)?; + if !self.args.is_empty() { + write!(f, " {}", self.args.join(" "))?; + } + Ok(()) + } +} + +impl From for TaskDep { + fn from(s: String) -> Self { + s.parse().unwrap() + } +} + +impl FromStr for TaskDep { + type Err = String; + + fn from_str(s: &str) -> Result { + let parts = s.split_whitespace().collect_vec(); + if parts.is_empty() { + return Err("Task name is required".to_string()); + } + Ok(Self { + task: parts[0].to_string(), + args: parts[1..].iter().map(|s| s.to_string()).collect(), + }) + } +} + +impl<'de> Deserialize<'de> for TaskDep { + fn deserialize>(deserializer: D) -> Result { + let input: Vec = deserialize_arr(deserializer)?; + if input.is_empty() { + Err(serde::de::Error::custom("Task name is required")) + } else if input.len() == 1 { + Ok(input[0].to_string().into()) + } else { + Ok(Self { + task: input[0].clone(), + args: input[1..].to_vec(), + }) + } + } +} + +impl Serialize for TaskDep { + fn serialize(&self, serializer: S) -> Result { + if self.args.is_empty() { + serializer.serialize_str(&self.task) + } else { + // TODO: it would be possible to track if the user specified a string and if so, continue that format + let mut seq = serializer.serialize_seq(Some(1 + self.args.len()))?; + seq.serialize_element(&self.task)?; + for arg in &self.args { + seq.serialize_element(arg)?; + } + seq.end() + } + } +} + +mod tests { + #[allow(unused_imports)] // no idea why I need this + use super::*; + + #[test] + fn test_task_dep_from_str() { + let td: TaskDep = "task".parse().unwrap(); + assert_eq!(td.task, "task"); + assert!(td.args.is_empty()); + + let td: TaskDep = "task arg1 arg2".parse().unwrap(); + assert_eq!(td.task, "task"); + assert_eq!(td.args, vec!["arg1", "arg2"]); + } + + #[test] + fn test_task_dep_display() { + let td = TaskDep { + task: "task".to_string(), + args: vec!["arg1".to_string(), "arg2".to_string()], + }; + assert_eq!(td.to_string(), "task arg1 arg2"); + } + + #[test] + fn test_task_dep_deserialize() { + let td: TaskDep = serde_json::from_str(r#""task""#).unwrap(); + assert_eq!(td.task, "task"); + assert!(td.args.is_empty()); + assert_eq!(&serde_json::to_string(&td).unwrap(), r#""task""#); + + let td: TaskDep = serde_json::from_str(r#"["task", "arg1", "arg2"]"#).unwrap(); + assert_eq!(td.task, "task"); + assert_eq!(td.args, vec!["arg1", "arg2"]); + assert_eq!( + &serde_json::to_string(&td).unwrap(), + r#"["task","arg1","arg2"]"# + ); + } +} diff --git a/xtasks/fig/src/mise.ts b/xtasks/fig/src/mise.ts index 852c5c57f..d16ee9a8c 100644 --- a/xtasks/fig/src/mise.ts +++ b/xtasks/fig/src/mise.ts @@ -1907,6 +1907,14 @@ const completionSpec: Fig.Spec = { ], "description": "Don't show extra output", "isRepeatable": false + }, + { + "name": [ + "-S", + "--silent" + ], + "description": "Don't show any output except for errors", + "isRepeatable": false } ], "generateSpec": usageGenerateSpec(["mise tasks --usage"]), @@ -2592,6 +2600,14 @@ const completionSpec: Fig.Spec = { ], "description": "Don't show extra output", "isRepeatable": false + }, + { + "name": [ + "-S", + "--silent" + ], + "description": "Don't show any output except for errors", + "isRepeatable": false } ], "args": [ @@ -3689,6 +3705,13 @@ const completionSpec: Fig.Spec = { "description": "Read/write directly to stdin/stdout/stderr instead of by line", "isRepeatable": false }, + { + "name": [ + "--silent" + ], + "description": "Suppress all task output and mise non-error messages", + "isRepeatable": false + }, { "name": [ "-v",