@@ -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+ } ;
2326use pixi_progress:: await_in_progress;
2427use rattler_lock:: LockFile ;
2528use 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) ]
558568mod 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\" ;\n export \" 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#"
0 commit comments