diff --git a/Cargo.lock b/Cargo.lock index 6a2108e170..ff5a86d4dc 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -4330,6 +4330,7 @@ name = "pixi" version = "0.44.0" dependencies = [ "ahash", + "anyhow", "assert_matches", "async-fd-lock", "async-once-cell", diff --git a/Cargo.toml b/Cargo.toml index 3e06d8c73f..c62e769bbd 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -276,6 +276,7 @@ uv-pep440 = { workspace = true } uv-pep508 = { workspace = true } uv-pypi-types = { workspace = true } +anyhow = "1.0.97" ctrlc = { workspace = true } fs-err = { workspace = true, features = ["tokio"] } pixi_allocator = { workspace = true, optional = true } diff --git a/crates/pixi_manifest/src/task.rs b/crates/pixi_manifest/src/task.rs index 8750e47cac..ed11359be3 100644 --- a/crates/pixi_manifest/src/task.rs +++ b/crates/pixi_manifest/src/task.rs @@ -37,10 +37,35 @@ impl From for TaskName { TaskName(name) } } -impl From for String { - fn from(task_name: TaskName) -> Self { - task_name.0 // Assuming TaskName is a tuple struct with the first - // element as String + +/// A task dependency with optional args +#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize)] +pub struct Dependency { + pub task_name: TaskName, + pub args: Option>, +} + +impl Dependency { + pub fn new(s: &str, args: Option>) -> Self { + Dependency { + task_name: TaskName(s.to_string()), + args, + } + } +} + +impl From<&str> for Dependency { + fn from(s: &str) -> Self { + Dependency::new(s, None) + } +} + +impl std::fmt::Display for Dependency { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + match &self.args { + Some(args) if !args.is_empty() => write!(f, "{} with args", self.task_name), + _ => write!(f, "{}", self.task_name), + } } } @@ -56,14 +81,14 @@ impl FromStr for TaskName { #[derive(Debug, Clone)] pub enum Task { Plain(String), - Execute(Execute), + Execute(Box), Alias(Alias), Custom(Custom), } impl Task { /// Returns the names of the task that this task depends on - pub fn depends_on(&self) -> &[TaskName] { + pub fn depends_on(&self) -> &[Dependency] { match self { Task::Plain(_) | Task::Custom(_) => &[], Task::Execute(cmd) => &cmd.depends_on, @@ -185,6 +210,45 @@ impl Task { _ => None, } } + + /// Returns the arguments of the task. + pub fn get_args(&self) -> Option<&IndexMap>> { + match self { + Task::Execute(exe) => exe.args.as_ref(), + _ => None, + } + } + + /// Creates a new task with updated arguments from the provided values. + /// Returns None if the task doesn't support arguments. + pub fn with_updated_args(&self, arg_values: &[String]) -> Option { + match self { + Task::Execute(exe) => { + if arg_values.len() > exe.args.as_ref().map_or(0, |args| args.len()) { + tracing::warn!("Task has more arguments than provided values"); + return None; + } + + if let Some(args_map) = &exe.args { + let mut new_args = args_map.clone(); + for ((arg_name, _), value) in args_map.iter().zip(arg_values.iter()) { + if let Some(arg_value) = new_args.get_mut(arg_name) { + *arg_value = Some(value.clone()); + } + } + + // Create a new Execute with the updated args + let mut new_exe = (**exe).clone(); + new_exe.args = Some(new_args); + + Some(Task::Execute(Box::new(new_exe))) + } else { + None + } + } + _ => None, + } + } } /// A command script executes a single command from the environment @@ -204,7 +268,7 @@ pub struct Execute { /// A list of commands that should be run before this one // BREAK: Make the remove the alias and force kebab-case - pub depends_on: Vec, + pub depends_on: Vec, /// The working directory for the command relative to the root of the /// project. @@ -218,11 +282,34 @@ pub struct Execute { /// Isolate the task from the running machine pub clean_env: bool, + + /// The arguments to pass to the task + pub args: Option>>, } impl From for Task { fn from(value: Execute) -> Self { - Task::Execute(value) + Task::Execute(Box::new(value)) + } +} + +#[derive(Debug, Clone, Eq, Hash, PartialEq)] +pub struct TaskArg { + /// The name of the argument + pub name: String, + + /// The default value of the argument + pub default: Option, +} + +impl std::str::FromStr for TaskArg { + type Err = miette::Error; + + fn from_str(s: &str) -> Result { + Ok(TaskArg { + name: s.to_string(), + default: None, + }) } } @@ -284,7 +371,7 @@ impl CmdArgs { #[derive(Debug, Clone)] pub struct Alias { /// A list of commands that should be run before this one - pub depends_on: Vec, + pub depends_on: Vec, /// A description of the task. pub description: Option, @@ -306,7 +393,7 @@ impl Display for Task { let depends_on = self.depends_on(); if !depends_on.is_empty() { if depends_on.len() == 1 { - write!(f, ", depends-on = '{}'", depends_on.iter().format(","))?; + write!(f, ", depends-on = '{}'", depends_on[0])?; } else { write!(f, ", depends-on = [{}]", depends_on.iter().format(","))?; } @@ -366,13 +453,22 @@ impl From for Item { if !process.depends_on.is_empty() { table.insert( "depends-on", - Value::Array(Array::from_iter( - process - .depends_on - .into_iter() - .map(String::from) - .map(Value::from), - )), + Value::Array(Array::from_iter(process.depends_on.into_iter().map( + |dep| match &dep.args { + Some(args) if !args.is_empty() => { + let mut table = Table::new().into_inline_table(); + table.insert("task", dep.task_name.to_string().into()); + table.insert( + "args", + Value::Array(Array::from_iter( + args.iter().map(|arg| Value::from(arg.clone())), + )), + ); + Value::InlineTable(table) + } + _ => Value::from(dep.task_name.to_string()), + }, + ))), ); } if let Some(cwd) = process.cwd { @@ -390,13 +486,22 @@ impl From for Item { let mut table = Table::new().into_inline_table(); table.insert( "depends-on", - Value::Array(Array::from_iter( - alias - .depends_on - .into_iter() - .map(String::from) - .map(Value::from), - )), + Value::Array(Array::from_iter(alias.depends_on.into_iter().map(|dep| { + match &dep.args { + Some(args) if !args.is_empty() => { + let mut table = Table::new().into_inline_table(); + table.insert("task", dep.task_name.to_string().into()); + table.insert( + "args", + Value::Array(Array::from_iter( + args.iter().map(|arg| Value::from(arg.clone())), + )), + ); + Value::InlineTable(table) + } + _ => Value::from(dep.task_name.to_string()), + } + }))), ); Item::Value(Value::InlineTable(table)) } diff --git a/crates/pixi_manifest/src/toml/task.rs b/crates/pixi_manifest/src/toml/task.rs index 0ffe509be3..4900de0208 100644 --- a/crates/pixi_manifest/src/toml/task.rs +++ b/crates/pixi_manifest/src/toml/task.rs @@ -1,4 +1,4 @@ -use pixi_toml::{OneOrMany, TomlFromStr, TomlIndexMap, TomlWith}; +use pixi_toml::{TomlFromStr, TomlIndexMap}; use toml_span::{ de_helpers::{expected, TableHelper}, value::ValueInner, @@ -6,11 +6,33 @@ use toml_span::{ }; use crate::{ - task::{Alias, CmdArgs, Execute}, + task::{Alias, CmdArgs, Dependency, Execute, TaskArg}, warning::Deprecation, Task, TaskName, WithWarnings, }; +impl<'de> toml_span::Deserialize<'de> for TaskArg { + fn deserialize(value: &mut Value<'de>) -> Result { + let mut th = match value.take() { + ValueInner::String(str) => { + return Ok(TaskArg { + name: str.into_owned(), + default: None, + }) + } + ValueInner::Table(table) => TableHelper::from((table, value.span)), + inner => return Err(expected("string or table", inner, value.span).into()), + }; + + let name = th.required::("arg")?; + let default = th.optional::("default"); + + th.finalize(None)?; + + Ok(TaskArg { name, default }) + } +} + /// A task defined in the manifest. pub type TomlTask = WithWarnings; @@ -26,24 +48,72 @@ impl<'de> toml_span::Deserialize<'de> for TomlTask { let mut warnings = Vec::new(); let mut depends_on = |th: &mut TableHelper| { - let depends_on = th.optional::>>>("depends-on"); - if let Some(depends_on) = depends_on { - return Some(depends_on.into_inner()); + let mut depends_on = th.take("depends-on"); + if let Some((_, mut value)) = depends_on.take() { + let deps = match value.take() { + ValueInner::Array(array) => array + .into_iter() + .map(|mut item| { + let span = item.span; + match item.take() { + ValueInner::String(str) => Ok::( + Dependency::new(str.as_ref(), None), + ), + ValueInner::Table(table) => { + let mut th = TableHelper::from((table, span)); + let name = th.required::("task")?; + let args = th.optional::>("args"); + Ok(Dependency::new(&name, args)) + } + inner => Err(expected("string or table", inner, span).into()), + } + }) + .collect::, _>>()?, + ValueInner::String(str) => { + vec![Dependency::new(str.as_ref(), None)] + } + inner => { + return Err::, DeserError>( + expected("string or array", inner, value.span).into(), + ); + } + }; + + return Ok(deps); } if let Some((key, mut value)) = th.table.remove_entry("depends_on") { warnings .push(Deprecation::renamed_field("depends_on", "depends-on", key.span).into()); - return match TomlWith::<_, OneOrMany>>::deserialize(&mut value) { - Ok(depends_on) => Some(depends_on.into_inner()), - Err(err) => { - th.errors.extend(err.errors); - None + let deps = match value.take() { + ValueInner::Array(array) => array + .into_iter() + .map(|mut item| { + let span = item.span; + match item.take() { + ValueInner::String(str) => Ok::( + Dependency::new(str.as_ref(), None), + ), + ValueInner::Table(table) => { + let mut th = TableHelper::from((table, span)); + let name = th.required::("task")?; + let args = th.optional::>("args"); + Ok(Dependency::new(&name, args)) + } + inner => Err(expected("string or table", inner, span).into()), + } + }) + .collect::, _>>()?, + ValueInner::String(str) => { + vec![Dependency::new(str.as_ref(), None)] } + inner => return Err(expected("string or array", inner, value.span).into()), }; + + return Ok(deps); } - None + Ok(vec![]) }; let task = if let Some(cmd) = cmd { @@ -58,10 +128,25 @@ impl<'de> toml_span::Deserialize<'de> for TomlTask { .map(TomlIndexMap::into_inner); let description = th.optional("description"); let clean_env = th.optional("clean-env").unwrap_or(false); + let args = th.optional::>("args"); + let mut have_default = false; + for arg in args.as_ref().unwrap_or(&vec![]) { + if arg.default.is_some() { + have_default = true; + } + if have_default && arg.default.is_none() { + return Err(expected( + "default value required after previous arguments with defaults", + ValueInner::Table(Default::default()), + value.span, + ) + .into()); + } + } th.finalize(None)?; - Task::Execute(Execute { + Task::Execute(Box::new(Execute { cmd, inputs, outputs, @@ -70,7 +155,8 @@ impl<'de> toml_span::Deserialize<'de> for TomlTask { env, description, clean_env, - }) + args: args.map(|args| args.into_iter().map(|arg| (arg, None)).collect()), + })) } else { let depends_on = depends_on(&mut th).unwrap_or_default(); let description = th.optional("description"); diff --git a/docs/reference/cli/pixi/task/add.md b/docs/reference/cli/pixi/task/add.md index 3ecb85a97a..56ea2cde35 100644 --- a/docs/reference/cli/pixi/task/add.md +++ b/docs/reference/cli/pixi/task/add.md @@ -37,5 +37,8 @@ pixi task add [OPTIONS] ... : A description of the task to be added - `--clean-env` : Isolate the task from the shell environment, and only use the pixi environment to run the task +- `--args ` +: The arguments to pass to the task +
May be provided more than once. --8<-- "docs/reference/cli/pixi/task/add_extender:example" diff --git a/docs/source_files/pixi_tomls/task_arguments.toml b/docs/source_files/pixi_tomls/task_arguments.toml new file mode 100644 index 0000000000..989819cf9a --- /dev/null +++ b/docs/source_files/pixi_tomls/task_arguments.toml @@ -0,0 +1,24 @@ +[workspace] +channels = ["conda-forge"] +name = "task-arguments" + +# --8<-- [start:project_tasks] +# Task with required arguments +[tasks.greet] +args = ["name"] +cmd = "echo Hello, {{ name }}!" + +# Task with optional arguments (default values) +[tasks.build] +args = [ + { "arg" = "project", "default" = "my-app" }, + { "arg" = "mode", "default" = "development" }, +] +cmd = "echo Building {{ project }} in {{ mode }} mode" + +# Task with mixed required and optional arguments +[tasks.deploy] +args = ["service", { "arg" = "environment", "default" = "staging" }] +cmd = "echo Deploying {{ service }} to {{ environment }}" + +# --8<-- [end:project_tasks] diff --git a/docs/source_files/pixi_tomls/task_arguments_dependent.toml b/docs/source_files/pixi_tomls/task_arguments_dependent.toml new file mode 100644 index 0000000000..1dfafa4462 --- /dev/null +++ b/docs/source_files/pixi_tomls/task_arguments_dependent.toml @@ -0,0 +1,27 @@ +[workspace] +channels = ["conda-forge"] +name = "task-arguments-dependent" + +# --8<-- [start:project_tasks] +# Base task with arguments +[tasks.install] +args = [ + { "arg" = "path", "default" = "/default/path" }, # Path to manifest + { "arg" = "flag", "default" = "--normal" }, # Installation flag +] +cmd = "echo Installing with manifest {{ path }} and flag {{ flag }}" + +# Dependent task specifying arguments for the base task +[tasks.install-release] +depends-on = [{ "task" = "install", "args" = ["/path/to/manifest", "--debug"] }] + +# Task with multiple dependencies, passing different arguments +[tasks.deploy] +cmd = "echo Deploying" +depends-on = [ + # Override with custom path and verbosity + { "task" = "install", "args" = ["/custom/path", "--verbose"] }, + # Other dependent tasks can be added here +] + +# --8<-- [end:project_tasks] diff --git a/docs/source_files/pixi_tomls/task_arguments_partial.toml b/docs/source_files/pixi_tomls/task_arguments_partial.toml new file mode 100644 index 0000000000..d343de7786 --- /dev/null +++ b/docs/source_files/pixi_tomls/task_arguments_partial.toml @@ -0,0 +1,17 @@ +[workspace] +channels = ["conda-forge"] +name = "task-arguments-partial" + +# --8<-- [start:project_tasks] +[tasks.base-task] +args = [ + { "arg" = "arg1", "default" = "default1" }, # First argument with default + { "arg" = "arg2", "default" = "default2" }, # Second argument with default +] +cmd = "echo Base task with {{ arg1 }} and {{ arg2 }}" + +[tasks.partial-override] +# Only override the first argument +depends-on = [{ "task" = "base-task", "args" = ["override1"] }] + +# --8<-- [end:project_tasks] diff --git a/docs/workspace/advanced_tasks.md b/docs/workspace/advanced_tasks.md index 6ab2ad1238..ae86534cdc 100644 --- a/docs/workspace/advanced_tasks.md +++ b/docs/workspace/advanced_tasks.md @@ -1,4 +1,3 @@ - When building a package, you often have to do more than just run the code. Steps like formatting, linting, compiling, testing, benchmarking, etc. are often part of a workspace. With Pixi tasks, this should become much easier to do. @@ -81,7 +80,7 @@ pixi task add fmt ruff pixi task add lint pylint ``` -```shell +``` pixi task alias style fmt lint ``` @@ -129,6 +128,88 @@ This will add the following line to [manifest file](../reference/pixi_manifest.m bar = { cmd = "python bar.py", cwd = "scripts" } ``` +## Task Arguments + +Tasks can accept arguments that can be referenced in the command. This provides more flexibility and reusability for your tasks. + +### Why Use Task Arguments? + +Task arguments make your tasks more versatile and maintainable: + +- **Reusability**: Create generic tasks that can work with different inputs rather than duplicating tasks for each specific case +- **Flexibility**: Change behavior at runtime without modifying your pixi.toml file +- **Clarity**: Make your task intentions clear by explicitly defining what values can be customized +- **Validation**: Define required arguments to ensure tasks are called correctly +- **Default values**: Set sensible defaults while allowing overrides when needed + +For example, instead of creating separate build tasks for development and production modes, you can create a single parameterized task that handles both cases. + +Arguments can be: + +- **Required**: must be provided when running the task +- **Optional**: can have default values that are used when not explicitly provided + +### Defining Task Arguments + +Define arguments in your task using the `args` field: + +```toml title="pixi.toml" +--8<-- "docs/source_files/pixi_tomls/task_arguments.toml:project_tasks" +``` + +### Using Task Arguments + +When running a task, provide arguments in the order they are defined: + +```shell +# Required argument +pixi run greet John +✨ Pixi task (greet in default): echo Hello, John! + +# Default values are used when omitted +pixi run build +✨ Pixi task (build in default): echo Building my-app in development mode + +# Override default values +pixi run build my-project production +✨ Pixi task (build in default): echo Building my-project in production mode + +# Mixed argument types +pixi run deploy auth-service +✨ Pixi task (deploy in default): echo Deploying auth-service to staging +pixi run deploy auth-service production +✨ Pixi task (deploy in default): echo Deploying auth-service to production +``` +### Passing Arguments to Dependent Tasks + +You can pass arguments to tasks that are dependencies of other tasks: + +```toml title="pixi.toml" +--8<-- "docs/source_files/pixi_tomls/task_arguments_dependent.toml:project_tasks" +``` + +When executing a dependent task, the arguments are passed to the dependency: + +```shell +pixi run install-release +✨ Pixi task (install in default): echo Installing with manifest /path/to/manifest and flag --debug + +pixi run deploy +✨ Pixi task (install in default): echo Installing with manifest /custom/path and flag --verbose +✨ Pixi task (deploy in default): echo Deploying +``` + +When a dependent task doesn't specify all arguments, the default values are used for the missing ones: + +```toml title="pixi.toml" +--8<-- "docs/source_files/pixi_tomls/task_arguments_partial.toml:project_tasks" +``` + +```shell +pixi run partial-override +✨ Pixi task (base-task in default): echo Base task with override1 and default2 +``` + ## Caching When you specify `inputs` and/or `outputs` to a task, Pixi will reuse the result of the task. @@ -246,7 +327,7 @@ Next to running actual executable like `./myprogram`, `cmake` or `python` the sh - Set env variable using: `export ENV_VAR=value` - Use env variable using: `$ENV_VAR` - unset env variable using `unset ENV_VAR` -- **Shell variables:** Shell variables are similar to environment variables, but won’t be exported to spawned commands. +- **Shell variables:** Shell variables are similar to environment variables, but won't be exported to spawned commands. - Set them: `VAR=value` - use them: `VAR=value && echo $VAR` - **Pipelines:** Use the stdout output of a command into the stdin a following command diff --git a/examples/simple-calculator/calculator.py b/examples/simple-calculator/calculator.py new file mode 100644 index 0000000000..86d67f1a38 --- /dev/null +++ b/examples/simple-calculator/calculator.py @@ -0,0 +1,30 @@ +#!/usr/bin/env python3 +""" +A simple calculator module for demonstrating pixi task arguments. +This file contains functions that can be called by the calculate task. +""" + + +def sum(a, b): + """Add two numbers and return the result.""" + return int(a) + int(b) + + +def multiply(a, b): + """Multiply two numbers and return the result.""" + return int(a) * int(b) + + +def subtract(a, b): + """Subtract b from a and return the result.""" + return int(a) - int(b) + + +def divide(a, b): + """Divide a by b and return the result.""" + return int(a) / int(b) + + +if __name__ == "__main__": + print("Calculator module loaded.") + print("Available operations: sum, multiply, subtract, divide") diff --git a/examples/simple-calculator/pixi.lock b/examples/simple-calculator/pixi.lock new file mode 100644 index 0000000000..a12e4f5522 --- /dev/null +++ b/examples/simple-calculator/pixi.lock @@ -0,0 +1,761 @@ +version: 6 +environments: + default: + channels: + - url: https://conda.anaconda.org/conda-forge/ + packages: + linux-64: + - conda: https://conda.anaconda.org/conda-forge/linux-64/_libgcc_mutex-0.1-conda_forge.tar.bz2 + - conda: https://conda.anaconda.org/conda-forge/linux-64/_openmp_mutex-4.5-2_gnu.tar.bz2 + - conda: https://conda.anaconda.org/conda-forge/linux-64/bzip2-1.0.8-h4bc722e_7.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/ca-certificates-2025.1.31-hbcca054_0.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/ld_impl_linux-64-2.43-h712a8e2_4.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/libexpat-2.6.4-h5888daf_0.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/libffi-3.4.6-h2dba641_0.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/libgcc-14.2.0-h767d61c_2.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/libgcc-ng-14.2.0-h69a702a_2.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/libgomp-14.2.0-h767d61c_2.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/liblzma-5.6.4-hb9d3cd8_0.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/libmpdec-4.0.0-h4bc722e_0.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/libsqlite-3.49.1-hee588c1_2.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/libuuid-2.38.1-h0b41bf4_0.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/libzlib-1.3.1-hb9d3cd8_2.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/ncurses-6.5-h2d0b736_3.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/openssl-3.4.1-h7b32b05_0.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/python-3.13.2-hf636f53_101_cp313.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/python_abi-3.13-5_cp313.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/readline-8.2-h8c095d6_2.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/tk-8.6.13-noxft_h4845f30_101.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/tzdata-2025b-h78e105d_0.conda + osx-64: + - conda: https://conda.anaconda.org/conda-forge/osx-64/bzip2-1.0.8-hfdf4475_7.conda + - conda: https://conda.anaconda.org/conda-forge/osx-64/ca-certificates-2025.1.31-h8857fd0_0.conda + - conda: https://conda.anaconda.org/conda-forge/osx-64/libexpat-2.6.4-h240833e_0.conda + - conda: https://conda.anaconda.org/conda-forge/osx-64/libffi-3.4.6-h281671d_0.conda + - conda: https://conda.anaconda.org/conda-forge/osx-64/liblzma-5.6.4-hd471939_0.conda + - conda: https://conda.anaconda.org/conda-forge/osx-64/libmpdec-4.0.0-hfdf4475_0.conda + - conda: https://conda.anaconda.org/conda-forge/osx-64/libsqlite-3.49.1-hdb6dae5_2.conda + - conda: https://conda.anaconda.org/conda-forge/osx-64/libzlib-1.3.1-hd23fc13_2.conda + - conda: https://conda.anaconda.org/conda-forge/osx-64/ncurses-6.5-h0622a9a_3.conda + - conda: https://conda.anaconda.org/conda-forge/osx-64/openssl-3.4.1-hc426f3f_0.conda + - conda: https://conda.anaconda.org/conda-forge/osx-64/python-3.13.2-h534c281_101_cp313.conda + - conda: https://conda.anaconda.org/conda-forge/osx-64/python_abi-3.13-5_cp313.conda + - conda: https://conda.anaconda.org/conda-forge/osx-64/readline-8.2-h7cca4af_2.conda + - conda: https://conda.anaconda.org/conda-forge/osx-64/tk-8.6.13-h1abcd95_1.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/tzdata-2025b-h78e105d_0.conda + osx-arm64: + - conda: https://conda.anaconda.org/conda-forge/osx-arm64/bzip2-1.0.8-h99b78c6_7.conda + - conda: https://conda.anaconda.org/conda-forge/osx-arm64/ca-certificates-2025.1.31-hf0a4a13_0.conda + - conda: https://conda.anaconda.org/conda-forge/osx-arm64/libexpat-2.6.4-h286801f_0.conda + - conda: https://conda.anaconda.org/conda-forge/osx-arm64/libffi-3.4.2-h3422bc3_5.tar.bz2 + - conda: https://conda.anaconda.org/conda-forge/osx-arm64/liblzma-5.6.4-h39f12f2_0.conda + - conda: https://conda.anaconda.org/conda-forge/osx-arm64/libmpdec-4.0.0-h99b78c6_0.conda + - conda: https://conda.anaconda.org/conda-forge/osx-arm64/libsqlite-3.49.1-h3f77e49_2.conda + - conda: https://conda.anaconda.org/conda-forge/osx-arm64/libzlib-1.3.1-h8359307_2.conda + - conda: https://conda.anaconda.org/conda-forge/osx-arm64/ncurses-6.5-h5e97a16_3.conda + - conda: https://conda.anaconda.org/conda-forge/osx-arm64/openssl-3.4.1-h81ee809_0.conda + - conda: https://conda.anaconda.org/conda-forge/osx-arm64/python-3.13.2-h81fe080_101_cp313.conda + - conda: https://conda.anaconda.org/conda-forge/osx-arm64/python_abi-3.13-5_cp313.conda + - conda: https://conda.anaconda.org/conda-forge/osx-arm64/readline-8.2-h1d1bf99_2.conda + - conda: https://conda.anaconda.org/conda-forge/osx-arm64/tk-8.6.13-h5083fa2_1.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/tzdata-2025b-h78e105d_0.conda + win-64: + - conda: https://conda.anaconda.org/conda-forge/win-64/bzip2-1.0.8-h2466b09_7.conda + - conda: https://conda.anaconda.org/conda-forge/win-64/ca-certificates-2025.1.31-h56e8100_0.conda + - conda: https://conda.anaconda.org/conda-forge/win-64/libexpat-2.6.4-he0c23c2_0.conda + - conda: https://conda.anaconda.org/conda-forge/win-64/libffi-3.4.6-h537db12_0.conda + - conda: https://conda.anaconda.org/conda-forge/win-64/liblzma-5.6.4-h2466b09_0.conda + - conda: https://conda.anaconda.org/conda-forge/win-64/libmpdec-4.0.0-h2466b09_0.conda + - conda: https://conda.anaconda.org/conda-forge/win-64/libsqlite-3.49.1-h67fdade_2.conda + - conda: https://conda.anaconda.org/conda-forge/win-64/libzlib-1.3.1-h2466b09_2.conda + - conda: https://conda.anaconda.org/conda-forge/win-64/openssl-3.4.1-ha4e3fda_0.conda + - conda: https://conda.anaconda.org/conda-forge/win-64/python-3.13.2-h261c0b1_101_cp313.conda + - conda: https://conda.anaconda.org/conda-forge/win-64/python_abi-3.13-5_cp313.conda + - conda: https://conda.anaconda.org/conda-forge/win-64/tk-8.6.13-h5226925_1.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/tzdata-2025b-h78e105d_0.conda + - conda: https://conda.anaconda.org/conda-forge/win-64/ucrt-10.0.22621.0-h57928b3_1.conda + - conda: https://conda.anaconda.org/conda-forge/win-64/vc-14.3-h2b53caa_26.conda + - conda: https://conda.anaconda.org/conda-forge/win-64/vc14_runtime-14.42.34438-hfd919c2_26.conda +packages: +- conda: https://conda.anaconda.org/conda-forge/linux-64/_libgcc_mutex-0.1-conda_forge.tar.bz2 + sha256: fe51de6107f9edc7aa4f786a70f4a883943bc9d39b3bb7307c04c41410990726 + md5: d7c89558ba9fa0495403155b64376d81 + license: None + size: 2562 + timestamp: 1578324546067 +- conda: https://conda.anaconda.org/conda-forge/linux-64/_openmp_mutex-4.5-2_gnu.tar.bz2 + build_number: 16 + sha256: fbe2c5e56a653bebb982eda4876a9178aedfc2b545f25d0ce9c4c0b508253d22 + md5: 73aaf86a425cc6e73fcf236a5a46396d + depends: + - _libgcc_mutex 0.1 conda_forge + - libgomp >=7.5.0 + constrains: + - openmp_impl 9999 + license: BSD-3-Clause + license_family: BSD + size: 23621 + timestamp: 1650670423406 +- conda: https://conda.anaconda.org/conda-forge/linux-64/bzip2-1.0.8-h4bc722e_7.conda + sha256: 5ced96500d945fb286c9c838e54fa759aa04a7129c59800f0846b4335cee770d + md5: 62ee74e96c5ebb0af99386de58cf9553 + depends: + - __glibc >=2.17,<3.0.a0 + - libgcc-ng >=12 + license: bzip2-1.0.6 + license_family: BSD + size: 252783 + timestamp: 1720974456583 +- conda: https://conda.anaconda.org/conda-forge/osx-64/bzip2-1.0.8-hfdf4475_7.conda + sha256: cad153608b81fb24fc8c509357daa9ae4e49dfc535b2cb49b91e23dbd68fc3c5 + md5: 7ed4301d437b59045be7e051a0308211 + depends: + - __osx >=10.13 + license: bzip2-1.0.6 + license_family: BSD + size: 134188 + timestamp: 1720974491916 +- conda: https://conda.anaconda.org/conda-forge/osx-arm64/bzip2-1.0.8-h99b78c6_7.conda + sha256: adfa71f158cbd872a36394c56c3568e6034aa55c623634b37a4836bd036e6b91 + md5: fc6948412dbbbe9a4c9ddbbcfe0a79ab + depends: + - __osx >=11.0 + license: bzip2-1.0.6 + license_family: BSD + size: 122909 + timestamp: 1720974522888 +- conda: https://conda.anaconda.org/conda-forge/win-64/bzip2-1.0.8-h2466b09_7.conda + sha256: 35a5dad92e88fdd7fc405e864ec239486f4f31eec229e31686e61a140a8e573b + md5: 276e7ffe9ffe39688abc665ef0f45596 + depends: + - ucrt >=10.0.20348.0 + - vc >=14.2,<15 + - vc14_runtime >=14.29.30139 + license: bzip2-1.0.6 + license_family: BSD + size: 54927 + timestamp: 1720974860185 +- conda: https://conda.anaconda.org/conda-forge/linux-64/ca-certificates-2025.1.31-hbcca054_0.conda + sha256: bf832198976d559ab44d6cdb315642655547e26d826e34da67cbee6624cda189 + md5: 19f3a56f68d2fd06c516076bff482c52 + license: ISC + size: 158144 + timestamp: 1738298224464 +- conda: https://conda.anaconda.org/conda-forge/osx-64/ca-certificates-2025.1.31-h8857fd0_0.conda + sha256: 42e911ee2d8808eacedbec46d99b03200a6138b8e8a120bd8acabe1cac41c63b + md5: 3418b6c8cac3e71c0bc089fc5ea53042 + license: ISC + size: 158408 + timestamp: 1738298385933 +- conda: https://conda.anaconda.org/conda-forge/osx-arm64/ca-certificates-2025.1.31-hf0a4a13_0.conda + sha256: 7e12816618173fe70f5c638b72adf4bfd4ddabf27794369bb17871c5bb75b9f9 + md5: 3569d6a9141adc64d2fe4797f3289e06 + license: ISC + size: 158425 + timestamp: 1738298167688 +- conda: https://conda.anaconda.org/conda-forge/win-64/ca-certificates-2025.1.31-h56e8100_0.conda + sha256: 1bedccdf25a3bd782d6b0e57ddd97cdcda5501716009f2de4479a779221df155 + md5: 5304a31607974dfc2110dfbb662ed092 + license: ISC + size: 158690 + timestamp: 1738298232550 +- conda: https://conda.anaconda.org/conda-forge/linux-64/ld_impl_linux-64-2.43-h712a8e2_4.conda + sha256: db73f38155d901a610b2320525b9dd3b31e4949215c870685fd92ea61b5ce472 + md5: 01f8d123c96816249efd255a31ad7712 + depends: + - __glibc >=2.17,<3.0.a0 + constrains: + - binutils_impl_linux-64 2.43 + license: GPL-3.0-only + license_family: GPL + size: 671240 + timestamp: 1740155456116 +- conda: https://conda.anaconda.org/conda-forge/linux-64/libexpat-2.6.4-h5888daf_0.conda + sha256: 56541b98447b58e52d824bd59d6382d609e11de1f8adf20b23143e353d2b8d26 + md5: db833e03127376d461e1e13e76f09b6c + depends: + - __glibc >=2.17,<3.0.a0 + - libgcc >=13 + constrains: + - expat 2.6.4.* + license: MIT + license_family: MIT + size: 73304 + timestamp: 1730967041968 +- conda: https://conda.anaconda.org/conda-forge/osx-64/libexpat-2.6.4-h240833e_0.conda + sha256: d10f43d0c5df6c8cf55259bce0fe14d2377eed625956cddce06f58827d288c59 + md5: 20307f4049a735a78a29073be1be2626 + depends: + - __osx >=10.13 + constrains: + - expat 2.6.4.* + license: MIT + license_family: MIT + size: 70758 + timestamp: 1730967204736 +- conda: https://conda.anaconda.org/conda-forge/osx-arm64/libexpat-2.6.4-h286801f_0.conda + sha256: e42ab5ace927ee7c84e3f0f7d813671e1cf3529f5f06ee5899606630498c2745 + md5: 38d2656dd914feb0cab8c629370768bf + depends: + - __osx >=11.0 + constrains: + - expat 2.6.4.* + license: MIT + license_family: MIT + size: 64693 + timestamp: 1730967175868 +- conda: https://conda.anaconda.org/conda-forge/win-64/libexpat-2.6.4-he0c23c2_0.conda + sha256: 0c0447bf20d1013d5603499de93a16b6faa92d7ead870d96305c0f065b6a5a12 + md5: eb383771c680aa792feb529eaf9df82f + depends: + - ucrt >=10.0.20348.0 + - vc >=14.2,<15 + - vc14_runtime >=14.29.30139 + constrains: + - expat 2.6.4.* + license: MIT + license_family: MIT + size: 139068 + timestamp: 1730967442102 +- conda: https://conda.anaconda.org/conda-forge/linux-64/libffi-3.4.6-h2dba641_0.conda + sha256: 67a6c95e33ebc763c1adc3455b9a9ecde901850eb2fceb8e646cc05ef3a663da + md5: e3eb7806380bc8bcecba6d749ad5f026 + depends: + - __glibc >=2.17,<3.0.a0 + - libgcc >=13 + license: MIT + license_family: MIT + size: 53415 + timestamp: 1739260413716 +- conda: https://conda.anaconda.org/conda-forge/osx-64/libffi-3.4.6-h281671d_0.conda + sha256: 7805fdc536a3da7fb63dc48e040105cd4260c69a1d2bf5804dadd31bde8bab51 + md5: b8667b0d0400b8dcb6844d8e06b2027d + depends: + - __osx >=10.13 + license: MIT + license_family: MIT + size: 47258 + timestamp: 1739260651925 +- conda: https://conda.anaconda.org/conda-forge/osx-arm64/libffi-3.4.2-h3422bc3_5.tar.bz2 + sha256: 41b3d13efb775e340e4dba549ab5c029611ea6918703096b2eaa9c015c0750ca + md5: 086914b672be056eb70fd4285b6783b6 + license: MIT + license_family: MIT + size: 39020 + timestamp: 1636488587153 +- conda: https://conda.anaconda.org/conda-forge/win-64/libffi-3.4.6-h537db12_0.conda + sha256: 77922d8dd2faf88ac6accaeebf06409d1820486fde710cff6b554d12273e46be + md5: 31d5107f75b2f204937728417e2e39e5 + depends: + - ucrt >=10.0.20348.0 + - vc >=14.2,<15 + - vc14_runtime >=14.29.30139 + license: MIT + license_family: MIT + size: 40830 + timestamp: 1739260917585 +- conda: https://conda.anaconda.org/conda-forge/linux-64/libgcc-14.2.0-h767d61c_2.conda + sha256: 3a572d031cb86deb541d15c1875aaa097baefc0c580b54dc61f5edab99215792 + md5: ef504d1acbd74b7cc6849ef8af47dd03 + depends: + - __glibc >=2.17,<3.0.a0 + - _openmp_mutex >=4.5 + constrains: + - libgomp 14.2.0 h767d61c_2 + - libgcc-ng ==14.2.0=*_2 + license: GPL-3.0-only WITH GCC-exception-3.1 + license_family: GPL + size: 847885 + timestamp: 1740240653082 +- conda: https://conda.anaconda.org/conda-forge/linux-64/libgcc-ng-14.2.0-h69a702a_2.conda + sha256: fb7558c328b38b2f9d2e412c48da7890e7721ba018d733ebdfea57280df01904 + md5: a2222a6ada71fb478682efe483ce0f92 + depends: + - libgcc 14.2.0 h767d61c_2 + license: GPL-3.0-only WITH GCC-exception-3.1 + license_family: GPL + size: 53758 + timestamp: 1740240660904 +- conda: https://conda.anaconda.org/conda-forge/linux-64/libgomp-14.2.0-h767d61c_2.conda + sha256: 1a3130e0b9267e781b89399580f3163632d59fe5b0142900d63052ab1a53490e + md5: 06d02030237f4d5b3d9a7e7d348fe3c6 + depends: + - __glibc >=2.17,<3.0.a0 + license: GPL-3.0-only WITH GCC-exception-3.1 + license_family: GPL + size: 459862 + timestamp: 1740240588123 +- conda: https://conda.anaconda.org/conda-forge/linux-64/liblzma-5.6.4-hb9d3cd8_0.conda + sha256: cad52e10319ca4585bc37f0bc7cce99ec7c15dc9168e42ccb96b741b0a27db3f + md5: 42d5b6a0f30d3c10cd88cb8584fda1cb + depends: + - __glibc >=2.17,<3.0.a0 + - libgcc >=13 + license: 0BSD + size: 111357 + timestamp: 1738525339684 +- conda: https://conda.anaconda.org/conda-forge/osx-64/liblzma-5.6.4-hd471939_0.conda + sha256: a895b5b16468a6ed436f022d72ee52a657f9b58214b91fabfab6230e3592a6dd + md5: db9d7b0152613f097cdb61ccf9f70ef5 + depends: + - __osx >=10.13 + license: 0BSD + size: 103749 + timestamp: 1738525448522 +- conda: https://conda.anaconda.org/conda-forge/osx-arm64/liblzma-5.6.4-h39f12f2_0.conda + sha256: 560c59d3834cc652a84fb45531bd335ad06e271b34ebc216e380a89798fe8e2c + md5: e3fd1f8320a100f2b210e690a57cd615 + depends: + - __osx >=11.0 + license: 0BSD + size: 98945 + timestamp: 1738525462560 +- conda: https://conda.anaconda.org/conda-forge/win-64/liblzma-5.6.4-h2466b09_0.conda + sha256: 3f552b0bdefdd1459ffc827ea3bf70a6a6920c7879d22b6bfd0d73015b55227b + md5: c48f6ad0ef0a555b27b233dfcab46a90 + depends: + - ucrt >=10.0.20348.0 + - vc >=14.2,<15 + - vc14_runtime >=14.29.30139 + license: 0BSD + size: 104465 + timestamp: 1738525557254 +- conda: https://conda.anaconda.org/conda-forge/linux-64/libmpdec-4.0.0-h4bc722e_0.conda + sha256: d02d1d3304ecaf5c728e515eb7416517a0b118200cd5eacbe829c432d1664070 + md5: aeb98fdeb2e8f25d43ef71fbacbeec80 + depends: + - __glibc >=2.17,<3.0.a0 + - libgcc-ng >=12 + license: BSD-2-Clause + license_family: BSD + size: 89991 + timestamp: 1723817448345 +- conda: https://conda.anaconda.org/conda-forge/osx-64/libmpdec-4.0.0-hfdf4475_0.conda + sha256: 791be3d30d8e37ec49bcc23eb8f1e1415d911a7c023fa93685f2ea485179e258 + md5: ed625b2e59dff82859c23dd24774156b + depends: + - __osx >=10.13 + license: BSD-2-Clause + license_family: BSD + size: 76561 + timestamp: 1723817691512 +- conda: https://conda.anaconda.org/conda-forge/osx-arm64/libmpdec-4.0.0-h99b78c6_0.conda + sha256: f7917de9117d3a5fe12a39e185c7ce424f8d5010a6f97b4333e8a1dcb2889d16 + md5: 7476305c35dd9acef48da8f754eedb40 + depends: + - __osx >=11.0 + license: BSD-2-Clause + license_family: BSD + size: 69263 + timestamp: 1723817629767 +- conda: https://conda.anaconda.org/conda-forge/win-64/libmpdec-4.0.0-h2466b09_0.conda + sha256: fc529fc82c7caf51202cc5cec5bb1c2e8d90edbac6d0a4602c966366efe3c7bf + md5: 74860100b2029e2523cf480804c76b9b + depends: + - ucrt >=10.0.20348.0 + - vc >=14.2,<15 + - vc14_runtime >=14.29.30139 + license: BSD-2-Clause + license_family: BSD + size: 88657 + timestamp: 1723861474602 +- conda: https://conda.anaconda.org/conda-forge/linux-64/libsqlite-3.49.1-hee588c1_2.conda + sha256: a086289bf75c33adc1daed3f1422024504ffb5c3c8b3285c49f025c29708ed16 + md5: 962d6ac93c30b1dfc54c9cccafd1003e + depends: + - __glibc >=2.17,<3.0.a0 + - libgcc >=13 + - libzlib >=1.3.1,<2.0a0 + license: Unlicense + size: 918664 + timestamp: 1742083674731 +- conda: https://conda.anaconda.org/conda-forge/osx-64/libsqlite-3.49.1-hdb6dae5_2.conda + sha256: 82695c9b16a702de615c8303387384c6ec5cf8b98e16458e5b1935b950e4ec38 + md5: 1819e770584a7e83a81541d8253cbabe + depends: + - __osx >=10.13 + - libzlib >=1.3.1,<2.0a0 + license: Unlicense + size: 977701 + timestamp: 1742083869897 +- conda: https://conda.anaconda.org/conda-forge/osx-arm64/libsqlite-3.49.1-h3f77e49_2.conda + sha256: 907a95f73623c343fc14785cbfefcb7a6b4f2bcf9294fcb295c121611c3a590d + md5: 3b1e330d775170ac46dff9a94c253bd0 + depends: + - __osx >=11.0 + - libzlib >=1.3.1,<2.0a0 + license: Unlicense + size: 900188 + timestamp: 1742083865246 +- conda: https://conda.anaconda.org/conda-forge/win-64/libsqlite-3.49.1-h67fdade_2.conda + sha256: c092d42d00fd85cf609cc58574ba2b03c141af5762283f36f5dd445ef7c0f4fe + md5: b58b66d4ad1aaf1c2543cbbd6afb1a59 + depends: + - ucrt >=10.0.20348.0 + - vc >=14.2,<15 + - vc14_runtime >=14.29.30139 + license: Unlicense + size: 1081292 + timestamp: 1742083956001 +- conda: https://conda.anaconda.org/conda-forge/linux-64/libuuid-2.38.1-h0b41bf4_0.conda + sha256: 787eb542f055a2b3de553614b25f09eefb0a0931b0c87dbcce6efdfd92f04f18 + md5: 40b61aab5c7ba9ff276c41cfffe6b80b + depends: + - libgcc-ng >=12 + license: BSD-3-Clause + license_family: BSD + size: 33601 + timestamp: 1680112270483 +- conda: https://conda.anaconda.org/conda-forge/linux-64/libzlib-1.3.1-hb9d3cd8_2.conda + sha256: d4bfe88d7cb447768e31650f06257995601f89076080e76df55e3112d4e47dc4 + md5: edb0dca6bc32e4f4789199455a1dbeb8 + depends: + - __glibc >=2.17,<3.0.a0 + - libgcc >=13 + constrains: + - zlib 1.3.1 *_2 + license: Zlib + license_family: Other + size: 60963 + timestamp: 1727963148474 +- conda: https://conda.anaconda.org/conda-forge/osx-64/libzlib-1.3.1-hd23fc13_2.conda + sha256: 8412f96504fc5993a63edf1e211d042a1fd5b1d51dedec755d2058948fcced09 + md5: 003a54a4e32b02f7355b50a837e699da + depends: + - __osx >=10.13 + constrains: + - zlib 1.3.1 *_2 + license: Zlib + license_family: Other + size: 57133 + timestamp: 1727963183990 +- conda: https://conda.anaconda.org/conda-forge/osx-arm64/libzlib-1.3.1-h8359307_2.conda + sha256: ce34669eadaba351cd54910743e6a2261b67009624dbc7daeeafdef93616711b + md5: 369964e85dc26bfe78f41399b366c435 + depends: + - __osx >=11.0 + constrains: + - zlib 1.3.1 *_2 + license: Zlib + license_family: Other + size: 46438 + timestamp: 1727963202283 +- conda: https://conda.anaconda.org/conda-forge/win-64/libzlib-1.3.1-h2466b09_2.conda + sha256: ba945c6493449bed0e6e29883c4943817f7c79cbff52b83360f7b341277c6402 + md5: 41fbfac52c601159df6c01f875de31b9 + depends: + - ucrt >=10.0.20348.0 + - vc >=14.2,<15 + - vc14_runtime >=14.29.30139 + constrains: + - zlib 1.3.1 *_2 + license: Zlib + license_family: Other + size: 55476 + timestamp: 1727963768015 +- conda: https://conda.anaconda.org/conda-forge/linux-64/ncurses-6.5-h2d0b736_3.conda + sha256: 3fde293232fa3fca98635e1167de6b7c7fda83caf24b9d6c91ec9eefb4f4d586 + md5: 47e340acb35de30501a76c7c799c41d7 + depends: + - __glibc >=2.17,<3.0.a0 + - libgcc >=13 + license: X11 AND BSD-3-Clause + size: 891641 + timestamp: 1738195959188 +- conda: https://conda.anaconda.org/conda-forge/osx-64/ncurses-6.5-h0622a9a_3.conda + sha256: ea4a5d27ded18443749aefa49dc79f6356da8506d508b5296f60b8d51e0c4bd9 + md5: ced34dd9929f491ca6dab6a2927aff25 + depends: + - __osx >=10.13 + license: X11 AND BSD-3-Clause + size: 822259 + timestamp: 1738196181298 +- conda: https://conda.anaconda.org/conda-forge/osx-arm64/ncurses-6.5-h5e97a16_3.conda + sha256: 2827ada40e8d9ca69a153a45f7fd14f32b2ead7045d3bbb5d10964898fe65733 + md5: 068d497125e4bf8a66bf707254fff5ae + depends: + - __osx >=11.0 + license: X11 AND BSD-3-Clause + size: 797030 + timestamp: 1738196177597 +- conda: https://conda.anaconda.org/conda-forge/linux-64/openssl-3.4.1-h7b32b05_0.conda + sha256: cbf62df3c79a5c2d113247ddea5658e9ff3697b6e741c210656e239ecaf1768f + md5: 41adf927e746dc75ecf0ef841c454e48 + depends: + - __glibc >=2.17,<3.0.a0 + - ca-certificates + - libgcc >=13 + license: Apache-2.0 + license_family: Apache + size: 2939306 + timestamp: 1739301879343 +- conda: https://conda.anaconda.org/conda-forge/osx-64/openssl-3.4.1-hc426f3f_0.conda + sha256: 505a46671dab5d66df8e684f99a9ae735a607816b12810b572d63caa512224df + md5: a7d63f8e7ab23f71327ea6d27e2d5eae + depends: + - __osx >=10.13 + - ca-certificates + license: Apache-2.0 + license_family: Apache + size: 2591479 + timestamp: 1739302628009 +- conda: https://conda.anaconda.org/conda-forge/osx-arm64/openssl-3.4.1-h81ee809_0.conda + sha256: 4f8e2389e1b711b44182a075516d02c80fa7a3a7e25a71ff1b5ace9eae57a17a + md5: 75f9f0c7b1740017e2db83a53ab9a28e + depends: + - __osx >=11.0 + - ca-certificates + license: Apache-2.0 + license_family: Apache + size: 2934522 + timestamp: 1739301896733 +- conda: https://conda.anaconda.org/conda-forge/win-64/openssl-3.4.1-ha4e3fda_0.conda + sha256: 56dcc2b4430bfc1724e32661c34b71ae33a23a14149866fc5645361cfd3b3a6a + md5: 0730f8094f7088592594f9bf3ae62b3f + depends: + - ca-certificates + - ucrt >=10.0.20348.0 + - vc >=14.2,<15 + - vc14_runtime >=14.29.30139 + license: Apache-2.0 + license_family: Apache + size: 8515197 + timestamp: 1739304103653 +- conda: https://conda.anaconda.org/conda-forge/linux-64/python-3.13.2-hf636f53_101_cp313.conda + build_number: 101 + sha256: cc1984ee54261cee6a2db75c65fc7d2967bc8c6e912d332614df15244d7730ef + md5: a7902a3611fe773da3921cbbf7bc2c5c + depends: + - __glibc >=2.17,<3.0.a0 + - bzip2 >=1.0.8,<2.0a0 + - ld_impl_linux-64 >=2.36.1 + - libexpat >=2.6.4,<3.0a0 + - libffi >=3.4,<4.0a0 + - libgcc >=13 + - liblzma >=5.6.4,<6.0a0 + - libmpdec >=4.0.0,<5.0a0 + - libsqlite >=3.48.0,<4.0a0 + - libuuid >=2.38.1,<3.0a0 + - libzlib >=1.3.1,<2.0a0 + - ncurses >=6.5,<7.0a0 + - openssl >=3.4.1,<4.0a0 + - python_abi 3.13.* *_cp313 + - readline >=8.2,<9.0a0 + - tk >=8.6.13,<8.7.0a0 + - tzdata + license: Python-2.0 + size: 33233150 + timestamp: 1739803603242 + python_site_packages_path: lib/python3.13/site-packages +- conda: https://conda.anaconda.org/conda-forge/osx-64/python-3.13.2-h534c281_101_cp313.conda + build_number: 101 + sha256: 19abb6ba8a1af6985934a48f05fccd29ecc54926febdb8b3803f30134c518b34 + md5: 2e883c630979a183e23a510d470194e2 + depends: + - __osx >=10.13 + - bzip2 >=1.0.8,<2.0a0 + - libexpat >=2.6.4,<3.0a0 + - libffi >=3.4,<4.0a0 + - liblzma >=5.6.4,<6.0a0 + - libmpdec >=4.0.0,<5.0a0 + - libsqlite >=3.48.0,<4.0a0 + - libzlib >=1.3.1,<2.0a0 + - ncurses >=6.5,<7.0a0 + - openssl >=3.4.1,<4.0a0 + - python_abi 3.13.* *_cp313 + - readline >=8.2,<9.0a0 + - tk >=8.6.13,<8.7.0a0 + - tzdata + license: Python-2.0 + size: 13961675 + timestamp: 1739802065430 + python_site_packages_path: lib/python3.13/site-packages +- conda: https://conda.anaconda.org/conda-forge/osx-arm64/python-3.13.2-h81fe080_101_cp313.conda + build_number: 101 + sha256: 6239a14c39a9902d6b617d57efe3eefbab23cf30cdc67122fdab81d04da193cd + md5: 71a76067a1cac1a2f03b43a08646a63e + depends: + - __osx >=11.0 + - bzip2 >=1.0.8,<2.0a0 + - libexpat >=2.6.4,<3.0a0 + - libffi >=3.4,<4.0a0 + - liblzma >=5.6.4,<6.0a0 + - libmpdec >=4.0.0,<5.0a0 + - libsqlite >=3.48.0,<4.0a0 + - libzlib >=1.3.1,<2.0a0 + - ncurses >=6.5,<7.0a0 + - openssl >=3.4.1,<4.0a0 + - python_abi 3.13.* *_cp313 + - readline >=8.2,<9.0a0 + - tk >=8.6.13,<8.7.0a0 + - tzdata + license: Python-2.0 + size: 11682568 + timestamp: 1739801342527 + python_site_packages_path: lib/python3.13/site-packages +- conda: https://conda.anaconda.org/conda-forge/win-64/python-3.13.2-h261c0b1_101_cp313.conda + build_number: 101 + sha256: b6e7a6f314343926b5a236592272e5014edcda150e14d18d0fb9440d8a185c3f + md5: 5116c74f5e3e77b915b7b72eea0ec946 + depends: + - bzip2 >=1.0.8,<2.0a0 + - libexpat >=2.6.4,<3.0a0 + - libffi >=3.4,<4.0a0 + - liblzma >=5.6.4,<6.0a0 + - libmpdec >=4.0.0,<5.0a0 + - libsqlite >=3.48.0,<4.0a0 + - libzlib >=1.3.1,<2.0a0 + - openssl >=3.4.1,<4.0a0 + - python_abi 3.13.* *_cp313 + - tk >=8.6.13,<8.7.0a0 + - tzdata + - ucrt >=10.0.20348.0 + - vc >=14.2,<15 + - vc14_runtime >=14.29.30139 + license: Python-2.0 + size: 16848398 + timestamp: 1739800686310 + python_site_packages_path: Lib/site-packages +- conda: https://conda.anaconda.org/conda-forge/linux-64/python_abi-3.13-5_cp313.conda + build_number: 5 + sha256: 438225b241c5f9bddae6f0178a97f5870a89ecf927dfca54753e689907331442 + md5: 381bbd2a92c863f640a55b6ff3c35161 + constrains: + - python 3.13.* *_cp313 + license: BSD-3-Clause + license_family: BSD + size: 6217 + timestamp: 1723823393322 +- conda: https://conda.anaconda.org/conda-forge/osx-64/python_abi-3.13-5_cp313.conda + build_number: 5 + sha256: 075ad768648e88b78d2a94099563b43d3082e7c35979f457164f26d1079b7b5c + md5: 927a2186f1f997ac018d67c4eece90a6 + constrains: + - python 3.13.* *_cp313 + license: BSD-3-Clause + license_family: BSD + size: 6291 + timestamp: 1723823083064 +- conda: https://conda.anaconda.org/conda-forge/osx-arm64/python_abi-3.13-5_cp313.conda + build_number: 5 + sha256: 4437198eae80310f40b23ae2f8a9e0a7e5c2b9ae411a8621eb03d87273666199 + md5: b8e82d0a5c1664638f87f63cc5d241fb + constrains: + - python 3.13.* *_cp313 + license: BSD-3-Clause + license_family: BSD + size: 6322 + timestamp: 1723823058879 +- conda: https://conda.anaconda.org/conda-forge/win-64/python_abi-3.13-5_cp313.conda + build_number: 5 + sha256: 0c12cc1b84962444002c699ed21e815fb9f686f950d734332a1b74d07db97756 + md5: 44b4fe6f22b57103afb2299935c8b68e + constrains: + - python 3.13.* *_cp313 + license: BSD-3-Clause + license_family: BSD + size: 6716 + timestamp: 1723823166911 +- conda: https://conda.anaconda.org/conda-forge/linux-64/readline-8.2-h8c095d6_2.conda + sha256: 2d6d0c026902561ed77cd646b5021aef2d4db22e57a5b0178dfc669231e06d2c + md5: 283b96675859b20a825f8fa30f311446 + depends: + - libgcc >=13 + - ncurses >=6.5,<7.0a0 + license: GPL-3.0-only + license_family: GPL + size: 282480 + timestamp: 1740379431762 +- conda: https://conda.anaconda.org/conda-forge/osx-64/readline-8.2-h7cca4af_2.conda + sha256: 53017e80453c4c1d97aaf78369040418dea14cf8f46a2fa999f31bd70b36c877 + md5: 342570f8e02f2f022147a7f841475784 + depends: + - ncurses >=6.5,<7.0a0 + license: GPL-3.0-only + license_family: GPL + size: 256712 + timestamp: 1740379577668 +- conda: https://conda.anaconda.org/conda-forge/osx-arm64/readline-8.2-h1d1bf99_2.conda + sha256: 7db04684d3904f6151eff8673270922d31da1eea7fa73254d01c437f49702e34 + md5: 63ef3f6e6d6d5c589e64f11263dc5676 + depends: + - ncurses >=6.5,<7.0a0 + license: GPL-3.0-only + license_family: GPL + size: 252359 + timestamp: 1740379663071 +- conda: https://conda.anaconda.org/conda-forge/linux-64/tk-8.6.13-noxft_h4845f30_101.conda + sha256: e0569c9caa68bf476bead1bed3d79650bb080b532c64a4af7d8ca286c08dea4e + md5: d453b98d9c83e71da0741bb0ff4d76bc + depends: + - libgcc-ng >=12 + - libzlib >=1.2.13,<2.0.0a0 + license: TCL + license_family: BSD + size: 3318875 + timestamp: 1699202167581 +- conda: https://conda.anaconda.org/conda-forge/osx-64/tk-8.6.13-h1abcd95_1.conda + sha256: 30412b2e9de4ff82d8c2a7e5d06a15f4f4fef1809a72138b6ccb53a33b26faf5 + md5: bf830ba5afc507c6232d4ef0fb1a882d + depends: + - libzlib >=1.2.13,<2.0.0a0 + license: TCL + license_family: BSD + size: 3270220 + timestamp: 1699202389792 +- conda: https://conda.anaconda.org/conda-forge/osx-arm64/tk-8.6.13-h5083fa2_1.conda + sha256: 72457ad031b4c048e5891f3f6cb27a53cb479db68a52d965f796910e71a403a8 + md5: b50a57ba89c32b62428b71a875291c9b + depends: + - libzlib >=1.2.13,<2.0.0a0 + license: TCL + license_family: BSD + size: 3145523 + timestamp: 1699202432999 +- conda: https://conda.anaconda.org/conda-forge/win-64/tk-8.6.13-h5226925_1.conda + sha256: 2c4e914f521ccb2718946645108c9bd3fc3216ba69aea20c2c3cedbd8db32bb1 + md5: fc048363eb8f03cd1737600a5d08aafe + depends: + - ucrt >=10.0.20348.0 + - vc >=14.2,<15 + - vc14_runtime >=14.29.30139 + license: TCL + license_family: BSD + size: 3503410 + timestamp: 1699202577803 +- conda: https://conda.anaconda.org/conda-forge/noarch/tzdata-2025b-h78e105d_0.conda + sha256: 5aaa366385d716557e365f0a4e9c3fca43ba196872abbbe3d56bb610d131e192 + md5: 4222072737ccff51314b5ece9c7d6f5a + license: LicenseRef-Public-Domain + size: 122968 + timestamp: 1742727099393 +- conda: https://conda.anaconda.org/conda-forge/win-64/ucrt-10.0.22621.0-h57928b3_1.conda + sha256: db8dead3dd30fb1a032737554ce91e2819b43496a0db09927edf01c32b577450 + md5: 6797b005cd0f439c4c5c9ac565783700 + constrains: + - vs2015_runtime >=14.29.30037 + license: LicenseRef-MicrosoftWindowsSDK10 + size: 559710 + timestamp: 1728377334097 +- conda: https://conda.anaconda.org/conda-forge/win-64/vc-14.3-h2b53caa_26.conda + sha256: 7a685b5c37e9713fa314a0d26b8b1d7a2e6de5ab758698199b5d5b6dba2e3ce1 + md5: d3f0381e38093bde620a8d85f266ae55 + depends: + - vc14_runtime >=14.42.34433 + track_features: + - vc14 + license: BSD-3-Clause + license_family: BSD + size: 17893 + timestamp: 1743195261486 +- conda: https://conda.anaconda.org/conda-forge/win-64/vc14_runtime-14.42.34438-hfd919c2_26.conda + sha256: 30dcb71bb166e351aadbdc18f1718757c32cdaa0e1e5d9368469ee44f6bf4709 + md5: 91651a36d31aa20c7ba36299fb7068f4 + depends: + - ucrt >=10.0.20348.0 + constrains: + - vs2015_runtime 14.42.34438.* *_26 + license: LicenseRef-MicrosoftVisualCpp2015-2022Runtime + license_family: Proprietary + size: 750733 + timestamp: 1743195092905 diff --git a/examples/simple-calculator/pixi.toml b/examples/simple-calculator/pixi.toml new file mode 100644 index 0000000000..3d0c203ab5 --- /dev/null +++ b/examples/simple-calculator/pixi.toml @@ -0,0 +1,35 @@ +[project] +authors = ["Parsa "] +channels = ["conda-forge"] +description = "Example demonstrating task arguments in pixi" +name = "simple-calculator" +platforms = ["linux-64", "osx-64", "osx-arm64", "win-64"] +version = "0.1.0" + +[dependencies] +python = ">=3.10" + +[tasks.greeting] +args = [ + { arg = "name", default = "User" }, + { arg = "project", default = "Pixi" }, +] +cmd = "echo Hello, {{ name }}! Welcome to {{ project }}" + +[tasks.calculate] +args = ["operation", "number1", "number2"] +cmd = "python -c \"from calculator import {{ operation }}; print({{ operation }}({{ number1 }}, {{ number2 }}))\"" + +[tasks.run-pipeline] +cmd = "echo Pipeline completed" +depends-on = [ + { task = "greeting", args = [ + "Developer", + "Task Arguments Example", + ] }, + { task = "calculate", args = [ + "sum", + "5", + "10", + ] }, +] diff --git a/pixi_docs/Cargo.lock b/pixi_docs/Cargo.lock index cab0f1aba4..2fb469bec0 100644 --- a/pixi_docs/Cargo.lock +++ b/pixi_docs/Cargo.lock @@ -4186,6 +4186,7 @@ name = "pixi" version = "0.44.0" dependencies = [ "ahash", + "anyhow", "assert_matches", "async-fd-lock", "async-once-cell", diff --git a/schema/model.py b/schema/model.py index 2bd7c9377d..26f6dab047 100644 --- a/schema/model.py +++ b/schema/model.py @@ -310,6 +310,20 @@ class PyPIVersion(_PyPIRequirement): TaskName = Annotated[str, Field(pattern=r"^[^\s\$]+$", description="A valid task name.")] +class TaskArgs(StrictBaseModel): + """The arguments of a task.""" + + arg: NonEmptyStr + default: NonEmptyStr | None = Field(None, description="The default value of the argument") + + +class DependsOn(StrictBaseModel): + """The dependencies of a task.""" + + task: TaskName + args: list[NonEmptyStr] | None = Field(None, description="The arguments to pass to the task") + + class TaskInlineTable(StrictBaseModel): """A precise definition of a task.""" @@ -324,7 +338,7 @@ class TaskInlineTable(StrictBaseModel): alias="depends_on", description="The tasks that this task depends on. Environment variables will **not** be expanded. Deprecated in favor of `depends-on` from v0.21.0 onward.", ) - depends_on: list[TaskName] | TaskName | None = Field( + depends_on: list[DependsOn | TaskName] | DependsOn | TaskName | None = Field( None, description="The tasks that this task depends on. Environment variables will **not** be expanded.", ) @@ -350,6 +364,11 @@ class TaskInlineTable(StrictBaseModel): None, description="Whether to run in a clean environment, removing all environment variables except those defined in `env` and by pixi itself.", ) + args: list[TaskArgs | NonEmptyStr] | None = Field( + None, + description="The arguments to pass to the task", + examples=["arg1", "arg2"], + ) ####################### diff --git a/schema/schema.json b/schema/schema.json index e332e6b344..4430a91a50 100644 --- a/schema/schema.json +++ b/schema/schema.json @@ -451,6 +451,32 @@ "strict" ] }, + "DependsOn": { + "title": "DependsOn", + "description": "The dependencies of a task.", + "type": "object", + "required": [ + "task" + ], + "additionalProperties": false, + "properties": { + "args": { + "title": "Args", + "description": "The arguments to pass to the task", + "type": "array", + "items": { + "type": "string", + "minLength": 1 + } + }, + "task": { + "title": "Task", + "description": "A valid task name.", + "type": "string", + "pattern": "^[^\\s\\$]+$" + } + } + }, "Environment": { "title": "Environment", "description": "A composition of the dependencies of features which can be activated to run tasks or provide a shell", @@ -1534,12 +1560,54 @@ } } }, + "TaskArgs": { + "title": "TaskArgs", + "description": "The arguments of a task.", + "type": "object", + "required": [ + "arg" + ], + "additionalProperties": false, + "properties": { + "arg": { + "title": "Arg", + "type": "string", + "minLength": 1 + }, + "default": { + "title": "Default", + "description": "The default value of the argument", + "type": "string", + "minLength": 1 + } + } + }, "TaskInlineTable": { "title": "TaskInlineTable", "description": "A precise definition of a task.", "type": "object", "additionalProperties": false, "properties": { + "args": { + "title": "Args", + "description": "The arguments to pass to the task", + "type": "array", + "items": { + "anyOf": [ + { + "$ref": "#/$defs/TaskArgs" + }, + { + "type": "string", + "minLength": 1 + } + ] + }, + "examples": [ + "arg1", + "arg2" + ] + }, "clean-env": { "title": "Clean-Env", "description": "Whether to run in a clean environment, removing all environment variables except those defined in `env` and by pixi itself.", @@ -1575,11 +1643,21 @@ { "type": "array", "items": { - "description": "A valid task name.", - "type": "string", - "pattern": "^[^\\s\\$]+$" + "anyOf": [ + { + "$ref": "#/$defs/DependsOn" + }, + { + "description": "A valid task name.", + "type": "string", + "pattern": "^[^\\s\\$]+$" + } + ] } }, + { + "$ref": "#/$defs/DependsOn" + }, { "description": "A valid task name.", "type": "string", diff --git a/src/cli/run.rs b/src/cli/run.rs index 6845e8f7b5..bbdf3368cd 100644 --- a/src/cli/run.rs +++ b/src/cli/run.rs @@ -190,6 +190,11 @@ pub async fn execute(args: Args) -> miette::Result<()> { // Add a newline between task outputs eprintln!(); } + + let display_command = executable_task + .replace_args(&executable_task.display_command().to_string()) + .unwrap_or_else(|_| executable_task.display_command().to_string()); + eprintln!( "{}{}{}{}{}{}{}", console::Emoji("✨ ", ""), @@ -207,7 +212,7 @@ pub async fn execute(args: Args) -> miette::Result<()> { "".to_string() }, console::style("): ").bold(), - executable_task.display_command(), + display_command, if let Some(description) = executable_task.task().description() { console::style(format!(": ({})", description)).yellow() } else { diff --git a/src/cli/task.rs b/src/cli/task.rs index 7b95b34864..1c97bdb7c7 100644 --- a/src/cli/task.rs +++ b/src/cli/task.rs @@ -11,7 +11,7 @@ use indexmap::IndexMap; use itertools::Itertools; use miette::IntoDiagnostic; use pixi_manifest::{ - task::{quote, Alias, CmdArgs, Execute, Task, TaskName}, + task::{quote, Alias, CmdArgs, Dependency, Execute, Task, TaskArg, TaskName}, EnvironmentName, FeatureName, }; use rattler_conda_types::Platform; @@ -73,7 +73,7 @@ pub struct AddArgs { /// Depends on these other commands. #[clap(long)] #[clap(num_args = 1..)] - pub depends_on: Option>, + pub depends_on: Option>, /// The platform for which the task should be added. #[arg(long, short)] @@ -100,6 +100,10 @@ pub struct AddArgs { /// environment to run the task. #[arg(long)] pub clean_env: bool, + + /// The arguments to pass to the task + #[arg(long, num_args = 1..)] + pub args: Option>, } /// Parse a single key-value pair @@ -120,7 +124,7 @@ pub struct AliasArgs { /// Depends on these tasks to execute #[clap(required = true, num_args = 1..)] - pub depends_on: Vec, + pub depends_on: Vec, /// The platform for which the alias should be added #[arg(long, short)] @@ -201,8 +205,9 @@ impl From for Task { } Some(env) }; + let args = value.args; - Self::Execute(Execute { + Self::Execute(Box::new(Execute { cmd: CmdArgs::Single(cmd_args), depends_on, inputs: None, @@ -211,7 +216,8 @@ impl From for Task { env, description, clean_env, - }) + args: args.map(|args| args.into_iter().map(|arg| (arg, None)).collect()), + })) } } } @@ -544,7 +550,7 @@ impl From<(&FeatureName, &HashMap<&TaskName, &Task>)> for SerializableFeature { pub struct TaskInfo { cmd: Option, description: Option, - depends_on: Vec, + depends_on: Vec, cwd: Option, env: Option>, clean_env: bool, diff --git a/src/task/executable_task.rs b/src/task/executable_task.rs index 9be8b33b8b..b4106f47aa 100644 --- a/src/task/executable_task.rs +++ b/src/task/executable_task.rs @@ -15,6 +15,7 @@ use pixi_consts::consts; use pixi_manifest::{Task, TaskName}; use pixi_progress::await_in_progress; use rattler_lock::LockFile; +use regex; use thiserror::Error; use tokio::task::JoinHandle; @@ -28,6 +29,23 @@ use crate::{ Workspace, }; +#[derive(Debug, Error, Diagnostic)] +pub enum ShellParsingError { + #[error("Failed to parse shell script. Task: '{task}'")] + ParseError { + #[source] + source: anyhow::Error, + task: String, + }, + + #[error("Failed to replace argument placeholders. Task: '{task}'")] + ArgumentReplacement { + #[source] + source: anyhow::Error, + task: String, + }, +} + /// Runs task in project. #[derive(Default, Debug)] pub struct RunOutput { @@ -37,10 +55,11 @@ pub struct RunOutput { } #[derive(Debug, Error, Diagnostic)] -#[error("The task failed to parse. task: '{script}' error: '{error}'")] +#[error("The task failed to parse")] pub struct FailedToParseShellScript { pub script: String, - pub error: String, + #[source] + pub error: ShellParsingError, } #[derive(Debug, Error, Diagnostic)] @@ -91,12 +110,40 @@ impl<'p> ExecutableTask<'p> { /// Constructs a new executable task from a task graph node. pub fn from_task_graph(task_graph: &TaskGraph<'p>, task_id: TaskId) -> Self { let node = &task_graph[task_id]; + + let task = if let Some(argument_values) = node.arguments_values.clone() { + // Create a task with updated arguments + match &node.task { + Cow::Borrowed(task) => { + Cow::Owned(task.with_updated_args(&argument_values).unwrap_or_else(|| { + tracing::warn!( + "Failed to update arguments for task {}", + node.name.as_ref().unwrap_or(&"default".into()) + ); + (*task).clone() + })) + } + Cow::Owned(task) => { + Cow::Owned(task.with_updated_args(&argument_values).unwrap_or_else(|| { + tracing::warn!( + "Failed to update arguments for task {}", + node.name.as_ref().unwrap_or(&"default".into()) + ); + task.clone() + })) + } + } + } else { + // Clone the existing task + node.task.clone() + }; + Self { workspace: task_graph.project(), name: node.name.clone(), - task: node.task.clone(), + task, run_environment: node.run_environment.clone(), - additional_args: node.additional_args.clone(), + additional_args: node.additional_args.clone().unwrap_or_default(), } } @@ -114,6 +161,52 @@ impl<'p> ExecutableTask<'p> { pub(crate) fn project(&self) -> &'p Workspace { self.workspace } + /// Replaces the arguments in the task with the values from the task args. + pub fn replace_args(&self, task: &str) -> Result { + let mut task = task.to_string(); + if let Some(args) = self.task().get_args() { + for (arg, value) in args { + // Match {{ arg_name }} with flexible whitespace + let pattern = format!(r"\{{\{{\s*{}\s*\}}\}}", regex::escape(&arg.name)); + let replacement = match value { + Some(val) => val, + None => arg.default.as_deref().ok_or_else(|| { + ShellParsingError::ArgumentReplacement { + source: anyhow::Error::msg(format!( + "no value provided for argument '{}'", + arg.name, + )), + task: task.to_string(), + } + })?, + }; + task = regex::Regex::new(&pattern) + .map_err(|e| ShellParsingError::ArgumentReplacement { + source: e.into(), + task: task.to_string(), + })? + .replace_all(&task, replacement) + .into_owned(); + } + } + + // Check if there are any remaining {{ name }} patterns + let remaining_pattern = regex::Regex::new(r"\{\{\s*[\w\-\.]+\s*\}\}").map_err(|e| { + ShellParsingError::ArgumentReplacement { + source: e.into(), + task: task.to_string(), + } + })?; + + if remaining_pattern.is_match(&task) { + return Err(ShellParsingError::ArgumentReplacement { + source: anyhow::Error::msg("unresolved argument placeholders found"), + task: task.to_string(), + }); + } + + Ok(task) + } /// Returns the task as script fn as_script(&self) -> Option { @@ -148,11 +241,22 @@ impl<'p> ExecutableTask<'p> { if let Some(full_script) = self.as_script() { tracing::debug!("Parsing shell script: {}", full_script); + // Replace the arguments with the values + let full_script = + self.replace_args(&full_script) + .map_err(|e| FailedToParseShellScript { + script: full_script, + error: e, + })?; + // Parse the shell command deno_task_shell::parser::parse(full_script.trim()) .map_err(|e| FailedToParseShellScript { - script: full_script, - error: e.to_string(), + script: full_script.clone(), + error: ShellParsingError::ParseError { + source: e, + task: full_script, + }, }) .map(Some) } else { @@ -486,4 +590,41 @@ mod tests { .to_string() ); } + + #[test] + fn test_replace_args() { + let file_contents = r#" + [tasks] + simple = {cmd = "echo Simple task"} + "#; + + let workspace = Workspace::from_str( + Path::new("pixi.toml"), + &format!("{PROJECT_BOILERPLATE}\n{file_contents}"), + ) + .unwrap(); + + let task = workspace + .default_environment() + .task(&TaskName::from("simple"), None) + .unwrap(); + + let executable_task = ExecutableTask { + workspace: &workspace, + name: Some("simple".into()), + task: Cow::Borrowed(task), + run_environment: workspace.default_environment(), + additional_args: vec![], + }; + + // Test a command with no placeholders works fine + let result = executable_task + .replace_args("echo No placeholders here") + .unwrap(); + assert_eq!(result, "echo No placeholders here"); + + // Test that using an undefined argument errors + let result = executable_task.replace_args("echo {{ undefined }}"); + assert!(result.is_err()); + } } diff --git a/src/task/task_graph.rs b/src/task/task_graph.rs index 8f3bdf8063..76d4001a9f 100644 --- a/src/task/task_graph.rs +++ b/src/task/task_graph.rs @@ -9,7 +9,7 @@ use std::{ use itertools::Itertools; use miette::Diagnostic; use pixi_manifest::{ - task::{CmdArgs, Custom}, + task::{CmdArgs, Custom, Dependency}, Task, TaskName, }; use thiserror::Error; @@ -30,6 +30,16 @@ use crate::{ #[derive(Debug, Clone, Copy, Eq, PartialOrd, PartialEq, Ord, Hash)] pub struct TaskId(usize); +/// A dependency is a task name and a list of arguments. +#[derive(Debug, Clone, Eq, PartialEq, PartialOrd, Ord, Hash)] +pub struct GraphDependency(TaskId, Option>); + +impl GraphDependency { + pub fn task_id(&self) -> TaskId { + self.0 + } +} + /// A node in the [`TaskGraph`]. #[derive(Debug)] pub struct TaskNode<'p> { @@ -44,11 +54,15 @@ pub struct TaskNode<'p> { /// Additional arguments to pass to the command. These arguments are passed /// verbatim, e.g. they will not be interpreted by deno. - pub additional_args: Vec, + pub additional_args: Option>, + + /// The arguments to pass to the dependencies. + pub arguments_values: Option>, /// The id's of the task that this task depends on. - pub dependencies: Vec, + pub dependencies: Vec, } + impl fmt::Display for TaskNode<'_> { fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { write!( @@ -60,7 +74,7 @@ impl fmt::Display for TaskNode<'_> { self.format_additional_args(), self.dependencies .iter() - .map(|id| id.0.to_string()) + .map(|id| format!("{:?}", id.task_id())) .collect::>() .join(", ") ) @@ -78,19 +92,27 @@ impl TaskNode<'_> { pub(crate) fn full_command(&self) -> Option { let mut cmd = self.task.as_single_command()?.to_string(); - if !self.additional_args.is_empty() { - // Pass each additional argument varbatim by wrapping it in single quotes - cmd.push_str(&format!(" {}", self.format_additional_args())); + if let Some(additional_args) = &self.additional_args { + if !additional_args.is_empty() { + // Pass each additional argument varbatim by wrapping it in single quotes + cmd.push_str(&format!(" {}", self.format_additional_args())); + } } Some(cmd) } /// Format the additional arguments passed to this command - fn format_additional_args(&self) -> impl Display + '_ { - self.additional_args - .iter() - .format_with(" ", |arg, f| f(&format_args!("'{}'", arg))) + fn format_additional_args(&self) -> Box { + if let Some(additional_args) = &self.additional_args { + Box::new( + additional_args + .iter() + .format_with(" ", |arg, f| f(&format_args!("'{}'", arg))), + ) + } else { + Box::new("".to_string()) + } } } @@ -161,26 +183,45 @@ impl<'p> TaskGraph<'p> { Some(explicit_env) if task_env.is_default() => explicit_env, _ => task_env, }; + + let task_name = args.remove(0); + + let (additional_args, arguments_values) = if let Some(argument_map) = + task.get_args() + { + // Check if we don't have more arguments than the task expects + if args.len() > argument_map.len() { + return Err(TaskGraphError::TooManyArguments(task_name.to_string())); + } + + (None, Some(args)) + } else { + (Some(args), None) + }; + if skip_deps { return Ok(Self { project, nodes: vec![TaskNode { - name: Some(args.remove(0).into()), + name: Some(task_name.into()), task: Cow::Borrowed(task), run_environment: run_env, - additional_args: args, + additional_args, + arguments_values, dependencies: vec![], }], }); } + return Self::from_root( project, search_envs, TaskNode { - name: Some(args.remove(0).into()), + name: Some(task_name.into()), task: Cow::Borrowed(task), run_environment: run_env, - additional_args: args, + additional_args, + arguments_values, dependencies: vec![], }, ); @@ -219,7 +260,8 @@ impl<'p> TaskGraph<'p> { .into(), ), run_environment, - additional_args, + additional_args: Some(additional_args), + arguments_values: None, dependencies: vec![], }, ) @@ -231,40 +273,49 @@ impl<'p> TaskGraph<'p> { search_environments: &SearchEnvironments<'p, D>, root: TaskNode<'p>, ) -> Result { - let mut task_name_to_node: HashMap = - HashMap::from_iter(root.name.clone().into_iter().map(|name| (name, TaskId(0)))); + let mut task_name_with_args_to_node: HashMap = + HashMap::from_iter(root.name.clone().into_iter().map(|name| { + ( + Dependency::new(&name.to_string(), root.arguments_values.clone()), + TaskId(0), + ) + })); let mut nodes = vec![root]; // Iterate over all the nodes in the graph and add them to the graph. let mut next_node_to_visit = 0; while next_node_to_visit < nodes.len() { - let dependency_names = + let dependencies = Vec::from_iter(nodes[next_node_to_visit].task.depends_on().iter().cloned()); + // Collect all dependency data before modifying nodes + let mut deps_to_process = Vec::new(); + // Iterate over all the dependencies of the node and add them to the graph. - let mut node_dependencies = Vec::with_capacity(dependency_names.len()); - for dependency in dependency_names { + let mut node_dependencies = Vec::with_capacity(dependencies.len()); + for dependency in dependencies { // Check if we visited this node before already. - if let Some(&task_id) = task_name_to_node.get(&dependency) { - node_dependencies.push(task_id); + if let Some(&task_id) = task_name_with_args_to_node.get(&dependency) { + node_dependencies.push(GraphDependency(task_id, dependency.args.clone())); continue; } // Find the task in the project let node = &nodes[next_node_to_visit]; + + // Clone what we need before modifying nodes + let node_name = node + .name + .clone() + .expect("only named tasks can have dependencies"); + let task_ref = match &node.task { + Cow::Borrowed(task) => task, + Cow::Owned(_) => unreachable!("only named tasks can have dependencies"), + }; + let (task_env, task_dependency) = match search_environments.find_task( - dependency.clone(), - FindTaskSource::DependsOn( - node.name - .clone() - .expect("only named tasks can have dependencies"), - match &node.task { - Cow::Borrowed(task) => task, - Cow::Owned(_) => { - unreachable!("only named tasks can have dependencies") - } - }, - ), + dependency.task_name.clone(), + FindTaskSource::DependsOn(node_name, task_ref), ) { Err(FindTaskError::MissingTask(err)) => { return Err(TaskGraphError::MissingTask(err)) @@ -275,21 +326,28 @@ impl<'p> TaskGraph<'p> { Ok(result) => result, }; + // Store the dependency data for processing later + deps_to_process.push((dependency, task_env, task_dependency)); + } + + // Process all dependencies after collecting them + for (dependency, task_env, task_dependency) in deps_to_process { // Add the node to the graph let task_id = TaskId(nodes.len()); nodes.push(TaskNode { - name: Some(dependency.clone()), + name: Some(dependency.task_name.clone()), task: Cow::Borrowed(task_dependency), run_environment: task_env, - additional_args: Vec::new(), + additional_args: Some(Vec::new()), + arguments_values: dependency.args.clone(), dependencies: Vec::new(), }); // Store the task id in the map to be able to look up the name later - task_name_to_node.insert(dependency.clone(), task_id); + task_name_with_args_to_node.insert(dependency.clone(), task_id); // Add the dependency to the node - node_dependencies.push(task_id); + node_dependencies.push(GraphDependency(task_id, dependency.args.clone())); } nodes[next_node_to_visit].dependencies = node_dependencies; @@ -325,7 +383,7 @@ impl<'p> TaskGraph<'p> { } for dependency in nodes[id.0].dependencies.iter() { - visit(*dependency, nodes, visited, order); + visit(dependency.task_id(), nodes, visited, order); } order.push(id); @@ -344,6 +402,9 @@ pub enum TaskGraphError { #[error("could not split task, assuming non valid task")] InvalidTask, + + #[error("task '{0}' received more arguments than expected")] + TooManyArguments(String), } #[cfg(test)] diff --git a/tests/integration_python/test_run_cli.py b/tests/integration_python/test_run_cli.py index b92a60b42e..641efaacc3 100644 --- a/tests/integration_python/test_run_cli.py +++ b/tests/integration_python/test_run_cli.py @@ -1,10 +1,17 @@ import json +import tomli_w from pathlib import Path -from .common import EMPTY_BOILERPLATE_PROJECT, verify_cli_command, ExitCode, default_env_path +from .common import ( + EMPTY_BOILERPLATE_PROJECT, + verify_cli_command, + ExitCode, + default_env_path, +) import tempfile import os +import tomli def test_run_in_shell_environment(pixi: Path, tmp_pixi_workspace: Path) -> None: @@ -393,3 +400,504 @@ def test_run_dry_run(pixi: Path, tmp_pixi_workspace: Path) -> None: stdout_excludes="WET", stderr_excludes="WET", ) + + +def test_run_args(pixi: Path, tmp_pixi_workspace: Path) -> None: + manifest = tmp_pixi_workspace.joinpath("pixi.toml") + toml = f""" + {EMPTY_BOILERPLATE_PROJECT} + [tasks] + """ + manifest.write_text(toml) + + +def test_invalid_task_args(pixi: Path, tmp_pixi_workspace: Path) -> None: + manifest_path = tmp_pixi_workspace.joinpath("pixi.toml") + + manifest_content = tomli.loads(EMPTY_BOILERPLATE_PROJECT) + + manifest_content["tasks"] = { + "task_invalid_defaults": { + "cmd": "echo Invalid defaults: {{ arg1 }} {{ arg2 }} {{ arg3 }}", + "args": [ + {"arg": "arg1", "default": "default1"}, + "arg2", + {"arg": "arg3", "default": "default3"}, + ], + } + } + + manifest_path.write_text(tomli_w.dumps(manifest_content)) + + verify_cli_command( + [ + pixi, + "run", + "--manifest-path", + manifest_path, + "task_invalid_defaults", + "arg1", + "arg2", + "arg3", + ], + ExitCode.FAILURE, + stderr_contains="expected default value required after previous arguments with defaults", + ) + + +def test_task_args_with_defaults(pixi: Path, tmp_pixi_workspace: Path) -> None: + """Test tasks with all default arguments.""" + manifest_path = tmp_pixi_workspace.joinpath("pixi.toml") + + manifest_content = tomli.loads(EMPTY_BOILERPLATE_PROJECT) + + manifest_content["tasks"] = { + "task_with_defaults": { + "cmd": "echo Running task with {{ arg1 }} and {{ arg2 }} and {{ arg3 }}", + "args": [ + {"arg": "arg1", "default": "default1"}, + {"arg": "arg2", "default": "default2"}, + {"arg": "arg3", "default": "default3"}, + ], + } + } + + manifest_path.write_text(tomli_w.dumps(manifest_content)) + + verify_cli_command( + [pixi, "run", "--manifest-path", manifest_path, "task_with_defaults"], + stdout_contains="Running task with default1 and default2 and default3", + ) + + verify_cli_command( + [ + pixi, + "run", + "--manifest-path", + manifest_path, + "task_with_defaults", + "custom1", + "custom2", + ], + stdout_contains="Running task with custom1 and custom2 and default3", + ) + + verify_cli_command( + [ + pixi, + "run", + "--manifest-path", + manifest_path, + "task_with_defaults", + "custom1", + "custom2", + "custom3", + ], + stdout_contains="Running task with custom1 and custom2 and custom3", + ) + + +def test_task_args_with_some_defaults(pixi: Path, tmp_pixi_workspace: Path) -> None: + """Test tasks with a mix of required and default arguments.""" + manifest_path = tmp_pixi_workspace.joinpath("pixi.toml") + + manifest_content = tomli.loads(EMPTY_BOILERPLATE_PROJECT) + + manifest_content["tasks"] = { + "task_with_some_defaults": { + "cmd": "echo Testing {{ required_arg }} with {{ optional_arg }}", + "args": [ + "required_arg", + {"arg": "optional_arg", "default": "optional-default"}, + ], + } + } + + manifest_path.write_text(tomli_w.dumps(manifest_content)) + + verify_cli_command( + [ + pixi, + "run", + "--manifest-path", + manifest_path, + "task_with_some_defaults", + "required-value", + ], + stdout_contains="Testing required-value with optional-default", + ) + + verify_cli_command( + [ + pixi, + "run", + "--manifest-path", + manifest_path, + "task_with_some_defaults", + "required-value", + "custom-optional", + ], + stdout_contains="Testing required-value with custom-optional", + ) + + +def test_task_args_all_required(pixi: Path, tmp_pixi_workspace: Path) -> None: + """Test tasks where all arguments are required.""" + manifest_path = tmp_pixi_workspace.joinpath("pixi.toml") + + manifest_content = tomli.loads(EMPTY_BOILERPLATE_PROJECT) + + manifest_content["tasks"] = { + "task_all_required": { + "cmd": "echo All args required: {{ arg1 }} {{ arg2 }} {{ arg3 }}", + "args": ["arg1", "arg2", "arg3"], + } + } + + manifest_path.write_text(tomli_w.dumps(manifest_content)) + + verify_cli_command( + [ + pixi, + "run", + "--manifest-path", + manifest_path, + "task_all_required", + "val1", + "val2", + "val3", + ], + stdout_contains="All args required: val1 val2 val3", + ) + + verify_cli_command( + [pixi, "run", "--manifest-path", manifest_path, "task_all_required", "val1"], + ExitCode.FAILURE, + stderr_contains="no value provided for argument 'arg2'", + ) + + +def test_task_args_too_many(pixi: Path, tmp_pixi_workspace: Path) -> None: + """Test error handling when too many arguments are provided.""" + manifest_path = tmp_pixi_workspace.joinpath("pixi.toml") + + manifest_content = tomli.loads(EMPTY_BOILERPLATE_PROJECT) + + manifest_content["tasks"] = { + "task_with_defaults": { + "cmd": "echo Running task with {{ arg1 }} and {{ arg2 }} and {{ arg3 }}", + "args": [ + {"arg": "arg1", "default": "default1"}, + {"arg": "arg2", "default": "default2"}, + {"arg": "arg3", "default": "default3"}, + ], + } + } + + manifest_path.write_text(tomli_w.dumps(manifest_content)) + + verify_cli_command( + [ + pixi, + "run", + "--manifest-path", + manifest_path, + "task_with_defaults", + "a", + "b", + "c", + "d", + ], + ExitCode.FAILURE, + stderr_contains="task 'task_with_defaults' received more arguments than expected", + ) + + +def test_task_with_dependency_args(pixi: Path, tmp_pixi_workspace: Path) -> None: + """Test passing arguments to a dependency task.""" + manifest_path = tmp_pixi_workspace.joinpath("pixi.toml") + + manifest_content = tomli.loads(EMPTY_BOILERPLATE_PROJECT) + + manifest_content["tasks"] = { + "base-task": { + "cmd": "echo Base task with {{ arg1 }} and {{ arg2 }}", + "args": [ + {"arg": "arg1", "default": "default1"}, + {"arg": "arg2", "default": "default2"}, + ], + }, + "parent-task": {"depends-on": [{"task": "base-task", "args": ["custom1", "custom2"]}]}, + "parent-task-partial": {"depends-on": [{"task": "base-task", "args": ["override1"]}]}, + } + + manifest_path.write_text(tomli_w.dumps(manifest_content)) + + verify_cli_command( + [pixi, "run", "--manifest-path", manifest_path, "parent-task"], + stdout_contains="Base task with custom1 and custom2", + ) + + verify_cli_command( + [pixi, "run", "--manifest-path", manifest_path, "parent-task-partial"], + stdout_contains="Base task with override1 and default2", + ) + + +def test_complex_task_dependencies_with_args(pixi: Path, tmp_pixi_workspace: Path) -> None: + """Test complex task dependencies with arguments.""" + manifest_path = tmp_pixi_workspace.joinpath("pixi.toml") + + manifest_content = tomli.loads(EMPTY_BOILERPLATE_PROJECT) + + manifest_content["tasks"] = { + "install": { + "cmd": "echo Installing with manifest {{ path }} and flag {{ flag }}", + "args": [ + {"arg": "path", "default": "/default/path"}, + {"arg": "flag", "default": "--normal"}, + ], + }, + "build": {"cmd": "echo Building with {{ mode }}", "args": ["mode"]}, + "install-release": { + "depends-on": [{"task": "install", "args": ["/path/to/manifest", "--debug"]}] + }, + "deploy": { + "cmd": "echo Deploying", + "depends-on": [ + {"task": "install", "args": ["/custom/path", "--verbose"]}, + {"task": "build", "args": ["production"]}, + ], + }, + } + + manifest_path.write_text(tomli_w.dumps(manifest_content)) + + verify_cli_command( + [pixi, "run", "--manifest-path", manifest_path, "install-release"], + stdout_contains="Installing with manifest /path/to/manifest and flag --debug", + ) + + verify_cli_command( + [pixi, "run", "--manifest-path", manifest_path, "deploy"], + stdout_contains=[ + "Installing with manifest /custom/path and flag --verbose", + "Building with production", + "Deploying", + ], + ) + + +def test_depends_on_with_complex_args(pixi: Path, tmp_pixi_workspace: Path) -> None: + """Test task dependencies with complex argument handling.""" + manifest_path = tmp_pixi_workspace.joinpath("pixi.toml") + + manifest_content = tomli.loads(EMPTY_BOILERPLATE_PROJECT) + + manifest_content["tasks"] = { + "helper-task": { + "cmd": "echo Helper executed with mode={{ mode }} and level={{ level }}", + "args": [ + {"arg": "mode", "default": "normal"}, + {"arg": "level", "default": "info"}, + ], + }, + "utility-task": { + "cmd": "echo Utility with arg={{ required_arg }}", + "args": ["required_arg"], + }, + "main-task": { + "cmd": "echo Main task executed", + "depends-on": [ + {"task": "helper-task", "args": ["debug", "verbose"]}, + {"task": "utility-task", "args": ["important-data"]}, + ], + }, + "partial-args-task": { + "cmd": "echo Partial args task", + "depends-on": [ + { + "task": "helper-task", + "args": ["production"], + } + ], + }, + "mixed-dependency-types": { + "cmd": "echo Mixed dependencies", + "depends-on": [ + "utility-task", + {"task": "helper-task"}, + ], + }, + } + + manifest_path.write_text(tomli_w.dumps(manifest_content)) + + verify_cli_command( + [pixi, "run", "--manifest-path", manifest_path, "main-task"], + stdout_contains=[ + "Helper executed with mode=debug and level=verbose", + "Utility with arg=important-data", + "Main task executed", + ], + ) + + verify_cli_command( + [pixi, "run", "--manifest-path", manifest_path, "partial-args-task"], + stdout_contains=[ + "Helper executed with mode=production and level=info", + "Partial args task", + ], + ) + + verify_cli_command( + [ + pixi, + "run", + "--manifest-path", + manifest_path, + "mixed-dependency-types", + "some-arg", + ], + ExitCode.FAILURE, + stderr_contains="no value provided for argument 'required_arg'", + ) + + +def test_argument_forwarding(pixi: Path, tmp_pixi_workspace: Path) -> None: + """Test argument forwarding behavior with and without defined args.""" + manifest_path = tmp_pixi_workspace.joinpath("pixi.toml") + + # Simple task with no args defined should just forward arguments + manifest_content = tomli.loads(EMPTY_BOILERPLATE_PROJECT) + manifest_content["tasks"] = { + "test_single": { + "cmd": "echo Forwarded args: ", + } + } + manifest_path.write_text(tomli_w.dumps(manifest_content)) + + # This should work - arguments are simply passed to the shell + verify_cli_command( + [pixi, "run", "--manifest-path", manifest_path, "test_single", "arg1", "arg2"], + stdout_contains="Forwarded args: arg1 arg2", + ) + + # Task with defined args should validate them + manifest_content["tasks"] = { + "test_single": { + "cmd": "echo Python file: {{ python-file }}", + "args": ["python-file"], # This argument is mandatory + } + } + manifest_path.write_text(tomli_w.dumps(manifest_content)) + + # This should work - exactly one argument provided as required + verify_cli_command( + [pixi, "run", "--manifest-path", manifest_path, "test_single", "test_file.py"], + stdout_contains="Python file: test_file.py", + ) + + # This should fail - too many arguments provided + verify_cli_command( + [ + pixi, + "run", + "--manifest-path", + manifest_path, + "test_single", + "file1.py", + "file2.py", + ], + ExitCode.FAILURE, + stderr_contains="task 'test_single' received more arguments than expected", + ) + + # This should fail - no arguments provided for a required arg + verify_cli_command( + [pixi, "run", "--manifest-path", manifest_path, "test_single"], + ExitCode.FAILURE, + stderr_contains="no value provided for argument 'python-file'", + ) + + +def test_undefined_arguments_in_command(pixi: Path, tmp_pixi_workspace: Path) -> None: + """Test behavior when using undefined arguments in commands.""" + manifest_path = tmp_pixi_workspace.joinpath("pixi.toml") + manifest_content = tomli.loads(EMPTY_BOILERPLATE_PROJECT) + + # Command with undefined argument + manifest_content["tasks"] = { + "undefined_arg": { + "cmd": "echo Python file: {{ python-file }}", + # No args defined, but using {{ python-file }} in command + } + } + manifest_path.write_text(tomli_w.dumps(manifest_content)) + + verify_cli_command( + [pixi, "run", "--manifest-path", manifest_path, "undefined_arg"], + ExitCode.FAILURE, + stderr_contains="Failed to replace argument placeholders", + ) + + manifest_content["tasks"] = { + "mixed_args": { + "cmd": "echo Python file: {{ python-file }} with {{ non-existing-argument }}", + "args": ["python-file"], + } + } + manifest_path.write_text(tomli_w.dumps(manifest_content)) + + verify_cli_command( + [pixi, "run", "--manifest-path", manifest_path, "mixed_args", "test.py"], + ExitCode.FAILURE, + stderr_contains="Failed to replace argument placeholders", + ) + + +def test_task_args_multiple_inputs(pixi: Path, tmp_pixi_workspace: Path) -> None: + """Test task arguments with multiple inputs.""" + manifest_path = tmp_pixi_workspace.joinpath("pixi.toml") + + manifest_content = tomli.loads(EMPTY_BOILERPLATE_PROJECT) + manifest_content["tasks"] = { + "task4": { + "cmd": "echo Task 4 executed with {{ input1 }} and {{ input2 }}", + "args": [ + {"arg": "input1", "default": "default1"}, + {"arg": "input2", "default": "default2"}, + ], + }, + "task2": { + "cmd": "echo Task 2 executed", + "depends-on": [ + {"task": "task4", "args": ["task2-arg1", "task2-arg2"]}, + ], + }, + "task3": { + "cmd": "echo Task 3 executed", + "depends-on": [ + {"task": "task4", "args": ["task3-arg1", "task3-arg2"]}, + ], + }, + "task1": { + "cmd": "echo Task 1 executed", + "depends-on": [ + {"task": "task2"}, + {"task": "task3"}, + ], + }, + } + manifest_path.write_text(tomli_w.dumps(manifest_content)) + + verify_cli_command( + [pixi, "run", "--manifest-path", manifest_path, "task1"], + stdout_contains=[ + "Task 4 executed with task2-arg1 and task2-arg2", + "Task 4 executed with task3-arg1 and task3-arg2", + "Task 2 executed", + "Task 3 executed", + "Task 1 executed", + ], + ) diff --git a/tests/integration_rust/common/builders.rs b/tests/integration_rust/common/builders.rs index b70d69ddae..603c9a8e49 100644 --- a/tests/integration_rust/common/builders.rs +++ b/tests/integration_rust/common/builders.rs @@ -37,10 +37,9 @@ use pixi::{ cli::{ add, cli_config::DependencyConfig, init, install, remove, search, task, update, workspace, }, - task::TaskName, DependencyType, }; -use pixi_manifest::{EnvironmentName, FeatureName, SpecType}; +use pixi_manifest::{task::Dependency, EnvironmentName, FeatureName, SpecType}; use rattler_conda_types::{NamedChannelOrUrl, Platform, RepoDataRecord}; use url::Url; @@ -324,7 +323,7 @@ impl TaskAddBuilder { } /// Depends on these commands - pub fn with_depends_on(mut self, depends: Vec) -> Self { + pub fn with_depends_on(mut self, depends: Vec) -> Self { self.args.depends_on = Some(depends); self } @@ -360,7 +359,7 @@ pub struct TaskAliasBuilder { impl TaskAliasBuilder { /// Depends on these commands - pub fn with_depends_on(mut self, depends: Vec) -> Self { + pub fn with_depends_on(mut self, depends: Vec) -> Self { self.args.depends_on = depends; self } diff --git a/tests/integration_rust/common/mod.rs b/tests/integration_rust/common/mod.rs index 96d37be987..82aa25a81f 100644 --- a/tests/integration_rust/common/mod.rs +++ b/tests/integration_rust/common/mod.rs @@ -625,6 +625,7 @@ impl TasksControl<'_> { env: Default::default(), description: None, clean_env: false, + args: None, }, } } diff --git a/tests/integration_rust/task_tests.rs b/tests/integration_rust/task_tests.rs index 909c1692d7..463ac5f2c9 100644 --- a/tests/integration_rust/task_tests.rs +++ b/tests/integration_rust/task_tests.rs @@ -86,7 +86,9 @@ pub async fn add_command_types() { let project = pixi.workspace().unwrap(); let tasks = project.default_environment().tasks(None).unwrap(); let task = tasks.get(&::from("testing")).unwrap(); - assert!(matches!(task, Task::Alias(a) if a.depends_on.first().unwrap().as_str() == "test")); + assert!( + matches!(task, Task::Alias(a) if a.depends_on.first().unwrap().task_name.as_str() == "test") + ); } #[tokio::test]