diff --git a/crates/pixi_manifest/src/task.rs b/crates/pixi_manifest/src/task.rs index c3b02f210c..08ceb3282d 100644 --- a/crates/pixi_manifest/src/task.rs +++ b/crates/pixi_manifest/src/task.rs @@ -495,27 +495,28 @@ impl From for Item { Item::Value(Value::InlineTable(table)) } Task::Alias(alias) => { - let mut table = Table::new().into_inline_table(); - table.insert( - "depends-on", - 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)) + let mut array = Array::new(); + for dep in alias.depends_on.iter() { + let mut table = Table::new().into_inline_table(); + + table.insert("task", dep.task_name.to_string().into()); + + if let Some(args) = &dep.args { + table.insert( + "args", + Value::Array(Array::from_iter( + args.iter().map(|arg| Value::from(arg.clone())), + )), + ); + } + + if let Some(env) = &dep.environment { + table.insert("environment", env.to_string().into()); + } + + array.push(Value::InlineTable(table)); + } + Item::Value(Value::Array(array)) } _ => Item::None, } diff --git a/crates/pixi_manifest/src/toml/task.rs b/crates/pixi_manifest/src/toml/task.rs index 6823187651..b4ae261bca 100644 --- a/crates/pixi_manifest/src/toml/task.rs +++ b/crates/pixi_manifest/src/toml/task.rs @@ -43,6 +43,37 @@ impl<'de> toml_span::Deserialize<'de> for TomlTask { let mut th = match value.take() { ValueInner::String(str) => return Ok(Task::Plain(str.into_owned()).into()), ValueInner::Table(table) => TableHelper::from((table, value.span)), + ValueInner::Array(array) => { + let mut deps = Vec::new(); + for mut item in array { + match item.take() { + ValueInner::Table(table) => { + let mut th = TableHelper::from((table, item.span)); + let name = th.required::("task")?; + let args = th.optional::>("args"); + let environment = th + .optional::("environment") + .map(|env| EnvironmentName::from_str(&env)) + .transpose() + .map_err(|e| { + DeserError::from(expected( + "valid environment name", + ValueInner::String(e.attempted_parse.into()), + item.span, + )) + })?; + + deps.push(Dependency::new(&name, args, environment)); + } + _ => return Err(expected("table", item.take(), item.span).into()), + } + } + return Ok(Task::Alias(Alias { + depends_on: deps, + description: None, + }) + .into()); + } inner => return Err(expected("string or table", inner, value.span).into()), }; @@ -58,7 +89,9 @@ impl<'de> toml_span::Deserialize<'de> for TomlTask { .map(|mut item| { let span = item.span; match item.take() { - ValueInner::String(str) => Ok(Dependency::from(str.as_ref())), + ValueInner::String(str) => Ok::( + Dependency::new(str.as_ref(), None, None), + ), ValueInner::Table(table) => { let mut th = TableHelper::from((table, span)); let name = th.required::("task")?; @@ -75,16 +108,13 @@ impl<'de> toml_span::Deserialize<'de> for TomlTask { )) })?; - // If the creating a new dependency fails, it means the environment name is invalid and exists hence we can safely unwrap the environment Ok(Dependency::new(&name, args, environment)) } inner => Err(expected("string or table", inner, span).into()), } }) .collect::, DeserError>>()?, - ValueInner::String(str) => { - vec![Dependency::from(str.as_ref())] - } + ValueInner::String(str) => Vec::from([Dependency::from(str.as_ref())]), inner => { return Err::, DeserError>( expected("string or array", inner, value.span).into(), @@ -129,9 +159,7 @@ impl<'de> toml_span::Deserialize<'de> for TomlTask { } }) .collect::, _>>()?, - ValueInner::String(str) => { - vec![Dependency::from(str.as_ref())] - } + ValueInner::String(str) => Vec::from([Dependency::from(str.as_ref())]), inner => return Err(expected("string or array", inner, value.span).into()), }; diff --git a/docs/source_files/pixi_tomls/pixi_task_alias.toml b/docs/source_files/pixi_tomls/pixi_task_alias.toml new file mode 100644 index 0000000000..903ddcf655 --- /dev/null +++ b/docs/source_files/pixi_tomls/pixi_task_alias.toml @@ -0,0 +1,13 @@ +[workspace] +channels = ["https://prefix.dev/conda-forge"] +name = "pixi" +platforms = ["linux-64", "win-64", "osx-64", "osx-arm64", "linux-aarch64"] + +# --8<-- [start:all] +# --8<-- [start:not-all] +[tasks] +fmt = "ruff" +lint = "pylint" +# --8<-- [end:not-all] +style = [{ task = "fmt" }, { task = "lint" }] +# --8<-- [end:all] diff --git a/docs/workspace/advanced_tasks.md b/docs/workspace/advanced_tasks.md index 0736f27b94..c142f731e0 100644 --- a/docs/workspace/advanced_tasks.md +++ b/docs/workspace/advanced_tasks.md @@ -76,19 +76,28 @@ pixi task add fmt ruff pixi task add lint pylint ``` +```toml title="pixi.toml" +--8<-- "docs/source_files/pixi_tomls/pixi_task_alias.toml:not-all" +``` + + +### Shorthand Syntax + +Pixi supports a shorthand syntax for defining tasks that only depend on other tasks. Instead of using the more verbose `depends-on` field, you can define a task directly as an array of dependencies. + +Executing: + ``` pixi task alias style fmt lint ``` -Results in the following `pixi.toml`. +results in the following `pixi.toml`: ```toml title="pixi.toml" -fmt = "ruff" -lint = "pylint" -style = { depends-on = ["fmt", "lint"] } +--8<-- "docs/source_files/pixi_tomls/pixi_task_alias.toml:all" ``` -Now run both tools with one command. +Now you can run both tools with one command. ```shell pixi run style diff --git a/schema/model.py b/schema/model.py index 465c79e9de..d0dbe0a2d6 100644 --- a/schema/model.py +++ b/schema/model.py @@ -460,7 +460,7 @@ class Target(StrictBaseModel): pypi_dependencies: dict[PyPIPackageName, PyPIRequirement] | None = Field( None, description="The PyPI dependencies for this target" ) - tasks: dict[TaskName, TaskInlineTable | NonEmptyStr] | None = Field( + tasks: dict[TaskName, TaskInlineTable | list[DependsOn] | NonEmptyStr] | None = Field( None, description="The tasks of the target" ) activation: Activation | None = Field( diff --git a/schema/schema.json b/schema/schema.json index 2e052a25ba..e9c95f6abf 100644 --- a/schema/schema.json +++ b/schema/schema.json @@ -1556,6 +1556,12 @@ { "$ref": "#/$defs/TaskInlineTable" }, + { + "type": "array", + "items": { + "$ref": "#/$defs/DependsOn" + } + }, { "type": "string", "minLength": 1 diff --git a/tests/integration_python/test_main_cli.py b/tests/integration_python/test_main_cli.py index ff4464d2b5..45a2bba8ae 100644 --- a/tests/integration_python/test_main_cli.py +++ b/tests/integration_python/test_main_cli.py @@ -1235,3 +1235,48 @@ def test_pixi_task_list_platforms(pixi: Path, tmp_pixi_workspace: Path) -> None: verify_cli_command( [pixi, "task", "list", "--manifest-path", manifest], stderr_contains=["foo", "bar"] ) + + +def test_pixi_add_alias(pixi: Path, tmp_pixi_workspace: Path) -> None: + manifest = tmp_pixi_workspace.joinpath("pixi.toml") + toml = """ + [workspace] + name = "test" + channels = [] + platforms = ["linux-64", "win-64", "osx-64", "osx-arm64"] + """ + manifest.write_text(toml) + + verify_cli_command( + [pixi, "task", "alias", "dummy-a", "dummy-b", "dummy-c", "--manifest-path", manifest] + ) + # Test platform-specific task alias + verify_cli_command( + [ + pixi, + "task", + "alias", + "--platform", + "linux-64", + "linux-alias", + "dummy-b", + "dummy-c", + "--manifest-path", + manifest, + ] + ) + + with open(manifest, "rb") as f: + manifest_content = tomllib.load(f) + + assert "target" in manifest_content + assert "linux-64" in manifest_content["target"] + assert "tasks" in manifest_content["target"]["linux-64"] + assert "linux-alias" in manifest_content["target"]["linux-64"]["tasks"] + assert manifest_content["target"]["linux-64"]["tasks"]["linux-alias"] == [ + {"task": "dummy-b"}, + {"task": "dummy-c"}, + ] + + assert "dummy-a" in manifest_content["tasks"] + assert manifest_content["tasks"]["dummy-a"] == [{"task": "dummy-b"}, {"task": "dummy-c"}] diff --git a/tests/integration_python/test_run_cli.py b/tests/integration_python/test_run_cli.py index 71e54a8023..489444ec30 100644 --- a/tests/integration_python/test_run_cli.py +++ b/tests/integration_python/test_run_cli.py @@ -1085,3 +1085,41 @@ def test_multiple_dependencies_with_environments( "0.2.0", ], ) + + +def test_short_circuit_composition(pixi: Path, tmp_pixi_workspace: Path) -> None: + """Test that short-circuiting composition works.""" + manifest_path = tmp_pixi_workspace.joinpath("pixi.toml") + + manifest_content = tomli.loads(EMPTY_BOILERPLATE_PROJECT) + + manifest_content["tasks"] = { + "task1": "echo task1", + "task2": "echo task2", + "task3": [{"task": "task1"}], + "task4": [{"task": "task3"}, {"task": "task2"}], + "task5": {"depends-on": [{"task": "task3"}, {"task": "task2"}]}, + } + + manifest_path.write_text(tomli_w.dumps(manifest_content)) + + verify_cli_command( + [pixi, "run", "--manifest-path", manifest_path, "task4"], + stdout_contains=["task1", "task2"], + ) + + verify_cli_command( + [pixi, "run", "--manifest-path", manifest_path, "task3"], + stdout_contains="task1", + ) + + output1 = verify_cli_command( + [pixi, "run", "--manifest-path", manifest_path, "task5"], + ) + + output2 = verify_cli_command( + [pixi, "run", "--manifest-path", manifest_path, "task4"], + ) + + assert output1.stdout == output2.stdout + assert output1.stderr == output2.stderr