Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions crates/pixi/tests/integration_rust/common/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -601,6 +601,7 @@ impl PixiControl {
} else {
PreferExecutable::TaskFirst
},
args.templated,
)
.map_err(RunError::TaskGraphError)?;

Expand Down
9 changes: 9 additions & 0 deletions crates/pixi_cli/src/run.rs
Original file line number Diff line number Diff line change
Expand Up @@ -84,6 +84,14 @@ pub struct Args {
#[arg(long)]
pub skip_deps: bool,

/// Enable template rendering for the command arguments.
///
/// By default, arguments passed to `pixi run` on the command line are not
/// processed by the template engine. Use this flag to enable rendering
/// of template variables like `{{ pixi.platform }}`.
#[arg(long)]
pub templated: bool,

/// Run the task in dry-run mode (only print the command that would run)
#[clap(short = 'n', long)]
pub dry_run: bool,
Expand Down Expand Up @@ -194,6 +202,7 @@ pub async fn execute(args: Args) -> miette::Result<()> {
} else {
PreferExecutable::TaskFirst
},
args.templated,
)?;
tracing::debug!("Task graph: {}", task_graph);

Expand Down
12 changes: 11 additions & 1 deletion crates/pixi_manifest/src/task.rs
Original file line number Diff line number Diff line change
Expand Up @@ -209,7 +209,13 @@ impl Task {
Ok(rendered) => Ok(Some(Cow::Owned(rendered))),
Err(e) => Err(e),
},
Task::Custom(custom) => custom.cmd.as_single(context),
Task::Custom(custom) => {
if custom.templated {
custom.cmd.as_single(context)
} else {
custom.cmd.as_single_no_render()
}
}
Task::Execute(exe) => exe.cmd.as_single(context),
Task::Alias(_) => Ok(None),
}
Expand Down Expand Up @@ -432,6 +438,10 @@ pub struct Custom {
/// The working directory for the command relative to the root of the
/// project.
pub cwd: Option<PathBuf>,

/// Whether to render the command through the template engine.
/// CLI commands default to false to avoid unexpected template errors.
pub templated: bool,
}

impl From<Custom> for Task {
Expand Down
116 changes: 114 additions & 2 deletions crates/pixi_task/src/task_graph.rs
Original file line number Diff line number Diff line change
Expand Up @@ -211,6 +211,7 @@ impl<'p> TaskGraph<'p> {
args: Vec<String>,
skip_deps: bool,
prefer_executable: PreferExecutable,
templated: bool,
) -> Result<Self, TaskGraphError> {
// Split 'args' into arguments if it's a single string, supporting commands
// like: `"test 1 == 0 || echo failed"` or `"echo foo && echo bar"` or
Expand Down Expand Up @@ -331,6 +332,7 @@ impl<'p> TaskGraph<'p> {
Custom {
cmd,
cwd: env::current_dir().ok(),
templated,
}
.into(),
),
Expand Down Expand Up @@ -642,6 +644,7 @@ mod test {
environment_name: Option<EnvironmentName>,
skip_deps: bool,
prefer_executable: PreferExecutable,
templated: bool,
) -> Vec<String> {
let project = Workspace::from_str(Path::new("pixi.toml"), project_str).unwrap();

Expand All @@ -654,6 +657,7 @@ mod test {
run_args.iter().map(|arg| arg.to_string()).collect(),
skip_deps,
prefer_executable,
templated,
)
.unwrap();

Expand Down Expand Up @@ -693,6 +697,7 @@ mod test {
None,
false,
PreferExecutable::TaskFirst,
false,
),
vec!["echo root", "echo task1", "echo task2", "echo top '--test'"]
);
Expand All @@ -718,6 +723,7 @@ mod test {
None,
false,
PreferExecutable::TaskFirst,
false,
),
vec!["echo root", "echo task1", "echo task2", "echo top"]
);
Expand Down Expand Up @@ -745,6 +751,7 @@ mod test {
None,
false,
PreferExecutable::TaskFirst,
false,
),
vec!["echo linux", "echo task1", "echo task2", "echo top",]
);
Expand All @@ -765,6 +772,7 @@ mod test {
None,
false,
PreferExecutable::TaskFirst,
false,
),
vec![r#"echo bla"#]
);
Expand All @@ -791,6 +799,7 @@ mod test {
None,
false,
PreferExecutable::TaskFirst,
false,
),
vec![r#"echo build"#]
);
Expand Down Expand Up @@ -820,6 +829,7 @@ mod test {
None,
false,
PreferExecutable::TaskFirst,
false,
),
vec![r#"hello world"#]
);
Expand Down Expand Up @@ -853,6 +863,7 @@ mod test {
Some(EnvironmentName::Named("cuda".to_string())),
false,
PreferExecutable::TaskFirst,
false,
),
vec![r#"python train.py --cuda"#, r#"python test.py --cuda"#]
);
Expand Down Expand Up @@ -884,6 +895,7 @@ mod test {
None,
false,
PreferExecutable::TaskFirst,
false,
),
vec![r#"echo foo"#, r#"echo bar"#]
);
Expand Down Expand Up @@ -916,6 +928,7 @@ mod test {
None,
false,
PreferExecutable::TaskFirst,
false,
);
}

Expand All @@ -938,7 +951,8 @@ mod test {
None,
None,
true,
PreferExecutable::TaskFirst
PreferExecutable::TaskFirst,
false,
),
vec![r#"echo bar"#]
);
Expand All @@ -949,7 +963,8 @@ mod test {
None,
None,
false,
PreferExecutable::TaskFirst
PreferExecutable::TaskFirst,
false,
),
vec!["echo foo", "echo bar"]
);
Expand All @@ -976,6 +991,102 @@ mod test {
assert_eq!(quote_result, r#"'echo' 'it'"'"'s'"#);
}

/// Regression test for https://github.com/prefix-dev/pixi/issues/5478
///
/// Verifies that `pixi run echo '{{ hello }}'` does not fail when
/// templating is disabled (the default for CLI commands).
#[test]
fn test_custom_command_with_braces_no_template() {
let project = r#"
[project]
name = "pixi"
channels = []
platforms = ["linux-64", "osx-64", "win-64", "osx-arm64", "linux-riscv64"]
"#;

let project =
Workspace::from_str(Path::new("pixi.toml"), project).expect("valid workspace");

let search_envs = SearchEnvironments::from_opt_env(&project, None, None);

// Without --templated, {{ hello }} should pass through as-is.
// Multiple args simulate how the shell + clap deliver them:
// pixi run echo '{{ hello }}'
// becomes args = ["echo", "{{ hello }}"]
let graph = TaskGraph::from_cmd_args(
&project,
&search_envs,
vec!["echo".to_string(), "{{ hello }}".to_string()],
false,
PreferExecutable::TaskFirst,
false,
)
.expect("should not fail with braces when templating is disabled");

let order = graph.topological_order();
assert_eq!(order.len(), 1);
let task = &graph[order[0]];
let context = pixi_manifest::task::TaskRenderContext {
platform: task.run_environment.best_platform(),
environment_name: task.run_environment.name(),
manifest_path: Some(&project.workspace.provenance.path),
args: task.args.as_ref(),
};
let cmd = task
.task
.as_single_command(&context)
.expect("should not fail")
.expect("should have a command");
assert!(cmd.contains("{{ hello }}"));
}

/// Verifies that `pixi run --templated echo '{{ pixi.platform }}'`
/// renders template variables.
#[test]
fn test_custom_command_with_templated_flag() {
let project = r#"
[project]
name = "pixi"
channels = []
platforms = ["linux-64", "osx-64", "win-64", "osx-arm64", "linux-riscv64"]
"#;

let project =
Workspace::from_str(Path::new("pixi.toml"), project).expect("valid workspace");

let search_envs = SearchEnvironments::from_opt_env(&project, None, None);

// With --templated, {{ pixi.platform }} should be rendered.
// Multiple args simulate how the shell + clap deliver them:
// pixi run --templated echo '{{ pixi.platform }}'
// becomes args = ["echo", "{{ pixi.platform }}"]
let graph = TaskGraph::from_cmd_args(
&project,
&search_envs,
vec!["echo".to_string(), "{{ pixi.platform }}".to_string()],
false,
PreferExecutable::TaskFirst,
true,
)
.expect("should succeed with valid template variable");

let order = graph.topological_order();
let task = &graph[order[0]];
let context = pixi_manifest::task::TaskRenderContext {
platform: task.run_environment.best_platform(),
environment_name: task.run_environment.name(),
manifest_path: Some(&project.workspace.provenance.path),
args: task.args.as_ref(),
};
let cmd = task
.task
.as_single_command(&context)
.expect("should render successfully")
.expect("should have a command");
// The platform should be resolved, not the raw template
assert!(!cmd.contains("{{"));
}

#[test]
fn test_prefer_executable_always() {
let project = r#"
Expand All @@ -999,6 +1110,7 @@ mod test {
vec!["foo".to_string()],
false,
PreferExecutable::Always,
false,
)
.expect("graph should be created");

Expand Down
2 changes: 2 additions & 0 deletions docs/reference/cli/pixi/run.md
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,8 @@ pixi run [OPTIONS] [TASK]...
: Use a clean environment to run the task
- <a id="arg---skip-deps" href="#arg---skip-deps">`--skip-deps`</a>
: Don't run the dependencies of the task ('depends-on' field in the task definition)
- <a id="arg---templated" href="#arg---templated">`--templated`</a>
: Enable template rendering for the command arguments
- <a id="arg---dry-run" href="#arg---dry-run">`--dry-run (-n)`</a>
: Run the task in dry-run mode (only print the command that would run)
- <a id="arg---help" href="#arg---help">`--help`</a>
Expand Down
4 changes: 4 additions & 0 deletions docs/reference/cli/pixi/run_extender
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,10 @@ pixi run --environment cuda python
# The PATH should only contain the pixi environment here.
pixi run --clean-env "echo \$PATH"

# By default, CLI commands are not processed by the template engine.
# Use --templated to enable template rendering for ad-hoc commands.
pixi run --templated echo '{{ pixi.platform }}'

```

## Notes
Expand Down
12 changes: 11 additions & 1 deletion docs/workspace/advanced_tasks.md
Original file line number Diff line number Diff line change
Expand Up @@ -275,7 +275,17 @@ pixi run partial-override-with-arg cli-arg

### MiniJinja Templating for Task Arguments

Task commands support MiniJinja templating syntax for accessing and formatting argument values. This provides powerful flexibility when constructing commands.
Task commands defined in the manifest support MiniJinja templating syntax for accessing and formatting argument values. This provides powerful flexibility when constructing commands.

!!! note "Templating and ad-hoc CLI commands"
MiniJinja templating is only applied to tasks defined in your manifest file (`pixi.toml` / `pyproject.toml`).
Ad-hoc commands passed directly to `pixi run` on the command line are **not** templated by default,
so commands like `pixi run echo '{{ hello }}'` are passed through as-is.
Use the `--templated` flag to opt in to template rendering for CLI commands:

```shell
pixi run --templated echo '{{ pixi.platform }}'
```

Basic syntax for using an argument in your command:

Expand Down
Loading