Skip to content
Open
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
6 changes: 3 additions & 3 deletions crates/pixi_cli/src/task.rs
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@ use miette::IntoDiagnostic;
use pixi_api::WorkspaceContext;
use pixi_manifest::{
EnvironmentName, FeatureName,
task::{Alias, CmdArgs, Dependency, Execute, Task, TaskArg, TaskName, quote},
task::{Alias, CmdArgs, Dependency, Execute, Task, TaskArg, TaskName, TemplateString, quote},
};
use rattler_conda_types::Platform;
use serde::Serialize;
Expand Down Expand Up @@ -206,7 +206,7 @@ impl From<AddArgs> for Task {
} else {
let mut env = IndexMap::new();
for (key, value) in value.env {
env.insert(key, value);
env.insert(key, TemplateString::from(value));
}
Some(env)
};
Expand Down Expand Up @@ -500,7 +500,7 @@ pub struct TaskInfo {
depends_on: Vec<Dependency>,
args: Option<Vec<TaskArg>>,
cwd: Option<PathBuf>,
env: Option<IndexMap<String, String>>,
env: Option<IndexMap<String, TemplateString>>,
default_environment: Option<EnvironmentName>,
clean_env: bool,
inputs: Option<Vec<String>>,
Expand Down
9 changes: 6 additions & 3 deletions crates/pixi_manifest/src/task.rs
Original file line number Diff line number Diff line change
Expand Up @@ -230,7 +230,7 @@ impl Task {
}
}
/// Returns the environment variables for the task to run in.
pub fn env(&self) -> Option<&IndexMap<String, String>> {
pub fn env(&self) -> Option<&IndexMap<String, TemplateString>> {
match self {
Task::Plain(_) => None,
Task::Custom(_) => None,
Expand Down Expand Up @@ -363,7 +363,7 @@ pub struct Execute {
pub cwd: Option<PathBuf>,

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

/// A default environment to run the task in.
pub default_environment: Option<EnvironmentName>,
Expand Down Expand Up @@ -970,7 +970,10 @@ impl From<Task> for Item {
table.insert("cwd", cwd.to_string_lossy().to_string().into());
}
if let Some(env) = &process.env {
table.insert("env", Value::InlineTable(env.into_iter().collect()));
table.insert(
"env",
Value::InlineTable(env.iter().map(|(k, v)| (k, v.source())).collect()),
);
}
if let Some(description) = &process.description {
table.insert("description", description.into());
Expand Down
98 changes: 90 additions & 8 deletions crates/pixi_task/src/executable_task.rs
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,10 @@ use pixi_core::{
workspace::get_activated_environment_variables,
workspace::{Environment, HasWorkspaceRef},
};
use pixi_manifest::{Task, TaskName, task::ArgValues, task::TemplateStringError};
use pixi_manifest::{
Task, TaskName,
task::{ArgValues, TaskRenderContext, TemplateStringError},
};
use pixi_progress::await_in_progress;
use rattler_lock::LockFile;
use thiserror::Error;
Expand Down Expand Up @@ -157,7 +160,8 @@ impl<'p> ExecutableTask<'p> {
.map_err(FailedToParseShellScript::ArgumentReplacement)?;
if let Some(task) = task {
// Get the export specific environment variables
let export = get_export_specific_task_env(self.task.as_ref());
let export = get_export_specific_task_env(self.task.as_ref(), &context)
.map_err(FailedToParseShellScript::ArgumentReplacement)?;

// Append the command line arguments verbatim
let extra = self.args.extra_args();
Expand Down Expand Up @@ -498,16 +502,22 @@ fn get_output_writer_and_handle() -> (ShellPipeWriter, JoinHandle<String>) {
/// task script. At runtime they are interpreted by `deno_task_shell`, not by an
/// external OS shell, so `$VAR`-style expansion follows deno-task-shell’s
/// semantics.
fn get_export_specific_task_env(task: &Task) -> String {
// Append the environment variables if they don't exist
fn get_export_specific_task_env(
task: &Task,
context: &TaskRenderContext,
) -> Result<String, TemplateStringError> {
// Append the environment variables if they don’t exist
let mut export = String::new();
if let Some(env) = task.env() {
for (key, value) in env {
tracing::debug!("Setting environment variable: {}=\"{}\"", key, value);
export.push_str(&format!("export \"{key}={value}\";\n"));
let rendered = value.render(context)?;
// Escape double quotes so the export statement remains valid shell.
let escaped = rendered.replace('"', "\\\"");
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

why it wasn't needed before?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yeah it should've been I think. Now I just reckoned it would happen more quickly.

tracing::debug!("Setting environment variable: {}=\"{}\"", key, escaped);
export.push_str(&format!("export \"{key}={escaped}\";\n"));
}
}
export
Ok(export)
}

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

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

let export = get_export_specific_task_env(task);
let context = TaskRenderContext::default();
let export = get_export_specific_task_env(task, &context).unwrap();

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

#[test]
fn test_export_specific_task_env_with_template() {
let file_contents = r#"
[tasks]
test = {cmd = "test", env = {BACKEND = "{{ backend }}"}, args = [{arg = "backend", default = "Numba"}]}
"#;
let workspace = Workspace::from_str(
Path::new("pixi.toml"),
&format!("{PROJECT_BOILERPLATE}\n{file_contents}"),
)
.unwrap();

let task = workspace
.default_environment()
.task(&TaskName::from("test"), None)
.unwrap();

// Test with explicit arg value
let args = ArgValues::TypedArgs {
args: vec![TypedArg {
name: "backend".into(),
value: "CuPy".into(),
}],
extra: vec![],
};
let context = TaskRenderContext {
args: Some(&args),
..Default::default()
};
let export = get_export_specific_task_env(task, &context).unwrap();
assert_eq!(export, "export \"BACKEND=CuPy\";\n");

// Test with default context (no args renders the default)
let default_args = ArgValues::TypedArgs {
args: vec![TypedArg {
name: "backend".into(),
value: "Numba".into(),
}],
extra: vec![],
};
let context = TaskRenderContext {
args: Some(&default_args),
..Default::default()
};
let export = get_export_specific_task_env(task, &context).unwrap();
assert_eq!(export, "export \"BACKEND=Numba\";\n");
}

#[test]
fn test_export_env_escapes_double_quotes() {
let file_contents = r#"
[tasks]
test = {cmd = "test", env = {JSON = '{"key": "value"}'}}
"#;
let workspace = Workspace::from_str(
Path::new("pixi.toml"),
&format!("{PROJECT_BOILERPLATE}\n{file_contents}"),
)
.unwrap();

let task = workspace
.default_environment()
.task(&TaskName::from("test"), None)
.unwrap();

let context = TaskRenderContext::default();
let export = get_export_specific_task_env(task, &context).unwrap();
assert_eq!(export, "export \"JSON={\\\"key\\\": \\\"value\\\"}\";\n");
}

#[test]
fn test_as_script() {
let file_contents = r#"
Expand Down
1 change: 1 addition & 0 deletions docs/reference/pixi_manifest.md
Original file line number Diff line number Diff line change
Expand Up @@ -407,6 +407,7 @@ alias = { depends-on=["depending"] }
download = { cmd="curl -o file.txt https://example.com/file.txt" , outputs=["file.txt"] }
build = { cmd="npm build", cwd="frontend", inputs=["frontend/package.json", "frontend/*.js"] }
run = { cmd="python run.py $ARGUMENT", env={ ARGUMENT="value" }} # Set an environment variable
backend = { cmd="pytest", env={ BACKEND="{{ backend }}" }, args=[{arg="backend", default="numpy"}] } # Template strings in env
format = { cmd="black $INIT_CWD" } # runs black where you run pixi run format
clean-env = { cmd="python isolated.py", clean-env=true } # Only on Unix!
test = { cmd="pytest", default-environment="test" } # Set a default pixi environment
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -7,4 +7,4 @@ version = "0.1.0"

[tool.pixi]
workspace.channels = ["conda-forge"]
workspace.platforms = ["osx-arm64"]
workspace.platforms = ["osx-arm64"]
Loading