Skip to content

Commit 720cddc

Browse files
committed
add template args for env vars in tasks as well
1 parent 68c748f commit 720cddc

File tree

4 files changed

+100
-15
lines changed

4 files changed

+100
-15
lines changed

crates/pixi_cli/src/task.rs

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,7 @@ use miette::IntoDiagnostic;
1414
use pixi_api::WorkspaceContext;
1515
use pixi_manifest::{
1616
EnvironmentName, FeatureName,
17-
task::{Alias, CmdArgs, Dependency, Execute, Task, TaskArg, TaskName, quote},
17+
task::{Alias, CmdArgs, Dependency, Execute, Task, TaskArg, TaskName, TemplateString, quote},
1818
};
1919
use rattler_conda_types::Platform;
2020
use serde::Serialize;
@@ -206,7 +206,7 @@ impl From<AddArgs> for Task {
206206
} else {
207207
let mut env = IndexMap::new();
208208
for (key, value) in value.env {
209-
env.insert(key, value);
209+
env.insert(key, TemplateString::from(value));
210210
}
211211
Some(env)
212212
};
@@ -500,7 +500,7 @@ pub struct TaskInfo {
500500
depends_on: Vec<Dependency>,
501501
args: Option<Vec<TaskArg>>,
502502
cwd: Option<PathBuf>,
503-
env: Option<IndexMap<String, String>>,
503+
env: Option<IndexMap<String, TemplateString>>,
504504
default_environment: Option<EnvironmentName>,
505505
clean_env: bool,
506506
inputs: Option<Vec<String>>,

crates/pixi_manifest/src/task.rs

Lines changed: 6 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -230,7 +230,7 @@ impl Task {
230230
}
231231
}
232232
/// Returns the environment variables for the task to run in.
233-
pub fn env(&self) -> Option<&IndexMap<String, String>> {
233+
pub fn env(&self) -> Option<&IndexMap<String, TemplateString>> {
234234
match self {
235235
Task::Plain(_) => None,
236236
Task::Custom(_) => None,
@@ -363,7 +363,7 @@ pub struct Execute {
363363
pub cwd: Option<PathBuf>,
364364

365365
/// A list of environment variables to set before running the command
366-
pub env: Option<IndexMap<String, String>>,
366+
pub env: Option<IndexMap<String, TemplateString>>,
367367

368368
/// A default environment to run the task in.
369369
pub default_environment: Option<EnvironmentName>,
@@ -970,7 +970,10 @@ impl From<Task> for Item {
970970
table.insert("cwd", cwd.to_string_lossy().to_string().into());
971971
}
972972
if let Some(env) = &process.env {
973-
table.insert("env", Value::InlineTable(env.into_iter().collect()));
973+
table.insert(
974+
"env",
975+
Value::InlineTable(env.iter().map(|(k, v)| (k, v.source())).collect()),
976+
);
974977
}
975978
if let Some(description) = &process.description {
976979
table.insert("description", description.into());

crates/pixi_task/src/executable_task.rs

Lines changed: 90 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -19,7 +19,10 @@ use pixi_core::{
1919
workspace::get_activated_environment_variables,
2020
workspace::{Environment, HasWorkspaceRef},
2121
};
22-
use pixi_manifest::{Task, TaskName, task::ArgValues, task::TemplateStringError};
22+
use pixi_manifest::{
23+
Task, TaskName,
24+
task::{ArgValues, TaskRenderContext, TemplateStringError},
25+
};
2326
use pixi_progress::await_in_progress;
2427
use rattler_lock::LockFile;
2528
use thiserror::Error;
@@ -157,7 +160,8 @@ impl<'p> ExecutableTask<'p> {
157160
.map_err(FailedToParseShellScript::ArgumentReplacement)?;
158161
if let Some(task) = task {
159162
// Get the export specific environment variables
160-
let export = get_export_specific_task_env(self.task.as_ref());
163+
let export = get_export_specific_task_env(self.task.as_ref(), &context)
164+
.map_err(FailedToParseShellScript::ArgumentReplacement)?;
161165

162166
// Append the command line arguments verbatim
163167
let extra = self.args.extra_args();
@@ -498,16 +502,22 @@ fn get_output_writer_and_handle() -> (ShellPipeWriter, JoinHandle<String>) {
498502
/// task script. At runtime they are interpreted by `deno_task_shell`, not by an
499503
/// external OS shell, so `$VAR`-style expansion follows deno-task-shell’s
500504
/// semantics.
501-
fn get_export_specific_task_env(task: &Task) -> String {
502-
// Append the environment variables if they don't exist
505+
fn get_export_specific_task_env(
506+
task: &Task,
507+
context: &TaskRenderContext,
508+
) -> Result<String, TemplateStringError> {
509+
// Append the environment variables if they don’t exist
503510
let mut export = String::new();
504511
if let Some(env) = task.env() {
505512
for (key, value) in env {
506-
tracing::debug!("Setting environment variable: {}=\"{}\"", key, value);
507-
export.push_str(&format!("export \"{key}={value}\";\n"));
513+
let rendered = value.render(context)?;
514+
// Escape double quotes so the export statement remains valid shell.
515+
let escaped = rendered.replace('"', "\\\"");
516+
tracing::debug!("Setting environment variable: {}=\"{}\"", key, escaped);
517+
export.push_str(&format!("export \"{key}={escaped}\";\n"));
508518
}
509519
}
510-
export
520+
Ok(export)
511521
}
512522

513523
/// Determine the environment variables to use when executing a command. The
@@ -557,6 +567,7 @@ pub async fn get_task_env(
557567
#[cfg(test)]
558568
mod tests {
559569
use super::*;
570+
use pixi_manifest::task::{ArgValues, TypedArg};
560571
use std::path::Path;
561572

562573
const PROJECT_BOILERPLATE: &str = r#"
@@ -585,11 +596,82 @@ mod tests {
585596
.task(&TaskName::from("test"), None)
586597
.unwrap();
587598

588-
let export = get_export_specific_task_env(task);
599+
let context = TaskRenderContext::default();
600+
let export = get_export_specific_task_env(task, &context).unwrap();
589601

590602
assert_eq!(export, "export \"FOO=bar\";\nexport \"BAR=$FOO\";\n");
591603
}
592604

605+
#[test]
606+
fn test_export_specific_task_env_with_template() {
607+
let file_contents = r#"
608+
[tasks]
609+
test = {cmd = "test", env = {BACKEND = "{{ backend }}"}, args = [{arg = "backend", default = "Numba"}]}
610+
"#;
611+
let workspace = Workspace::from_str(
612+
Path::new("pixi.toml"),
613+
&format!("{PROJECT_BOILERPLATE}\n{file_contents}"),
614+
)
615+
.unwrap();
616+
617+
let task = workspace
618+
.default_environment()
619+
.task(&TaskName::from("test"), None)
620+
.unwrap();
621+
622+
// Test with explicit arg value
623+
let args = ArgValues::TypedArgs {
624+
args: vec![TypedArg {
625+
name: "backend".into(),
626+
value: "CuPy".into(),
627+
}],
628+
extra: vec![],
629+
};
630+
let context = TaskRenderContext {
631+
args: Some(&args),
632+
..Default::default()
633+
};
634+
let export = get_export_specific_task_env(task, &context).unwrap();
635+
assert_eq!(export, "export \"BACKEND=CuPy\";\n");
636+
637+
// Test with default context (no args renders the default)
638+
let default_args = ArgValues::TypedArgs {
639+
args: vec![TypedArg {
640+
name: "backend".into(),
641+
value: "Numba".into(),
642+
}],
643+
extra: vec![],
644+
};
645+
let context = TaskRenderContext {
646+
args: Some(&default_args),
647+
..Default::default()
648+
};
649+
let export = get_export_specific_task_env(task, &context).unwrap();
650+
assert_eq!(export, "export \"BACKEND=Numba\";\n");
651+
}
652+
653+
#[test]
654+
fn test_export_env_escapes_double_quotes() {
655+
let file_contents = r#"
656+
[tasks]
657+
test = {cmd = "test", env = {JSON = '{"key": "value"}'}}
658+
"#;
659+
let workspace = Workspace::from_str(
660+
Path::new("pixi.toml"),
661+
&format!("{PROJECT_BOILERPLATE}\n{file_contents}"),
662+
)
663+
.unwrap();
664+
665+
let task = workspace
666+
.default_environment()
667+
.task(&TaskName::from("test"), None)
668+
.unwrap();
669+
670+
let context = TaskRenderContext::default();
671+
let export = get_export_specific_task_env(task, &context).unwrap();
672+
assert_eq!(export, "export \"JSON={\\\"key\\\": \\\"value\\\"}\";\n");
673+
}
674+
593675
#[test]
594676
fn test_as_script() {
595677
let file_contents = r#"

tests/data/workspace-discovery/implicit-tables-pyproject/pyproject.toml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,4 +7,4 @@ version = "0.1.0"
77

88
[tool.pixi]
99
workspace.channels = ["conda-forge"]
10-
workspace.platforms = ["osx-arm64"]
10+
workspace.platforms = ["osx-arm64"]

0 commit comments

Comments
 (0)