Skip to content

fix: Override environment variables based on priority #3940

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Open
wants to merge 59 commits into
base: main
Choose a base branch
from

Conversation

magentaqin
Copy link
Collaborator

@magentaqin magentaqin commented Jun 12, 2025

Description

This PR is created to solve Environment variable of tasks are broken when defined in surrounding scope , Unit tests and integration

What functionality changes I made

1.Change the priority of environment variables
Original Order(highest to lowest):
activation scripts > activation.env > activation scripts of dependencies > environment variables > task specific envs
Current Order(highest to lowest):
task specific envs > activation scripts > activation.env > activation scripts of dependencies > environment variables

2.What code changes I made
1) get_export_specific_task_env function refactor in executable_task.rs.
Previously, std::env::var has deterministic issues. So I updated get_export_specific_task_env to let it explicitly receive the parameter command_env.
priority array: store the keys of the environment variable map. For extension, as in the future, there may be other kinds of environment variable map passing in.
env_map map:

{
  "COMMAND_ENV": {...},
  "TASK_SPECIFIC_ENV": {...}
}

and we can merge the map according to the priority order. Finally, we get the export_merged array and export all the environment variables to the shell.
I also add escape logic and exclude environment variables that can't be overidden.
2) run_activation in activation.rs
process the activator.run_activation() result and handling the overidden stuff.

TO FIX

Test compatibility issues on windows platform, which causes the pipeline fails.

@magentaqin
Copy link
Collaborator Author

I find the priority order of activation scripts and activation.env is changed by activator.run_activation, which is the function run_activation in rattler_shell crate. Details can be seen here: https://github.com/prefix-dev/pixi/pull/3940/files#diff-f84977a6e7fdf8147b0d4b75f15a45112eff4507146389b1fb84e4647e0489c8

As required, we need to change the priority order. So I need to pass the second parameter to run_activation to override the environment variable.

To test whether the override function works, I constructed a hash map called opt_map, it has the key "CMAKE_ARGS" and the value "Foo123".

Before run_activation is called, I printed the current activator. It reads environment variable I defined in activation.env, that is "CMAKE_ARGS": "-DCMAKE_BUILD_TYPE=Debug"
Screenshot 2025-06-18 at 11 04 34

After run_activation is called, I printed activator again. It reads environment variable I defined in activation_scripts, that is CMAKE_ARGS": "from_activation_script
Screenshot 2025-06-18 at 11 08 13

Expected behavior:
After run_activation is called, activator should be CMAKE_ARGS": "Foo123, as I passed it as second parameter and overrides it.

@Hofer-Julian Hofer-Julian force-pushed the bugfix/environment-variable-priority branch from c963cd4 to 0886b67 Compare June 18, 2025 13:43
@ruben-arts
Copy link
Contributor

ruben-arts commented Jun 23, 2025

This change will introduce an issue for a use case we supported previously. That is the use of environment variables as arguments for functions. e.g.:

[tasks.start]
cmd = "echo $ARG"
env = { ARG="default" }

Usage:

~/envs/args  
➜ pixi run start
✨ Pixi task (start): echo $ARG
default

~/envs/args  
➜ ARG=hoi pixi run start
✨ Pixi task (start): echo $ARG
hoi

Users could define their task like that. With this change that use-case will break and will always have the default value.

We currently support arguments. But these are not named, only positional. So to get something like this working:

[tasks.problem]
cmd = "echo {{ ARG1}} {{ ARG2 }} {{ ARG3 }}"
args = [
  {arg = "ARG1", default = "default1"},
  {arg = "ARG2", default = "default2"},
  {arg = "ARG3", default = "default3"}
]

You need to run the following

➜ pixi run problem "hello 1" "test 2" "test 3"
✨ Pixi task (problem): echo hello 1 test 2 test 3
hello 1 test 2 test 3

The problem arrises when you only want to override the 3 option. you need to do:

➜ pixi run problem "" "" "test 3"              
✨ Pixi task (problem): echo   test 3
test 3

I think before we break that other behavior we need to improve the ux of tasks. As there is no way to define named variables right now. There is one more trick people could use:

[tasks.trick]
cmd = "echo {{ ARG }}"
args = [{arg = "ARG", default = "$ARG"}]

This would bring back the use of environment variables. But it doesn't allow a user to set a default as the evaluation happens after the template replacement.

Proposal

I think we can already improve this experience with a few convincing features:

  • Named arguments, in the cli, have a way to specify which argument you want to modify.
  • Extend the minijinja functions with the ability to read env vars: args = [{arg = "name", default = "{{ env.ARG or "default_value" }}"}]

@Hofer-Julian & @magentaqin please let me know what you think.

When we merge this we should see if it introduces really non-working setups and work to improve the UX of tasks as required.

@magentaqin magentaqin force-pushed the bugfix/environment-variable-priority branch from b6a5b74 to 9186613 Compare June 24, 2025 11:50
@magentaqin
Copy link
Collaborator Author

Hi, @ruben-arts! Thanks for your suggestion.

My Question

  1. Could you please explain more about the [tasks.trick] problem?
[tasks.trick]
cmd = "echo {{ ARG }}"
args = [{arg = "ARG", default = "$ARG"}]
`

If you run ARG=123 pixi run trick
✨ Pixi task (trick): echo $ARG

123

From my understanding, this satisfies our expectation.

  1. As this PR is to solve priority issues, for the CLI optimization part, shall we open another PR and another issue to solve it separately?

My Proposal

Based on your proposal, my proposal is:
1.Support named arguments and alias for named arguments.

[tasks.foo]
cmd = "echo {{ input }} {{ output }} {{ verbose }}"
args = [
    {name = "input", alias = "i", default = "Input file"},
    {name = "output", alias = "o", default = "out.txt"},
    {name = "verbose", alias = "v", default = false}
]

Then when you run like this:

pixi run foo -i input.txt
✨ Output
input.txt "out.txt" false

2.Support both default value and environment variable.

[tasks.foo]
cmd = "echo {{ ARG }}"
args = [{name = "ARG", default = "${ARG:-Guest"}}]

Then when you run like this:

ARG=123 pixi run foo
✨ Output
123
Then when you run like this:
pixi run foo
✨ Output
Guest

3.Support required field to force the user set value to override and support interactive prompts.(prompt, type and options)

[tasks.deploy]
cmd = "deploy {{ env }} {{ version }} {{ confirm }}"
args = [
    {
     name = "env",
     required = true, 
     prompt = "Deployment environment", 
     type = "select", 
     options = ["test", "staging", "production"]
   },
   {name = "version", default = "latest", prompt = "Version to deploy"},
   {name = "confirm", type = "bool", required = true, prompt = "Confirm deployment"}
]

$ pixi run deploy
✨ Output
Deployment environment: production
Version to deploy (latest): v1.2.3
Confirm deployment (y/N): y
Running: deploy production v1.2.3 true

@magentaqin magentaqin changed the title Override environment variables based on priority bugfix: sort environment variables based on priority Jun 26, 2025
@magentaqin magentaqin force-pushed the bugfix/environment-variable-priority branch from 5c63837 to c8bc900 Compare June 26, 2025 08:54
@magentaqin magentaqin changed the title bugfix: sort environment variables based on priority fix: Sort environment variables based on priority Jun 26, 2025
@magentaqin magentaqin force-pushed the bugfix/environment-variable-priority branch from e96b34a to c8bc900 Compare June 26, 2025 09:17
@magentaqin magentaqin marked this pull request as ready for review June 26, 2025 15:52
@magentaqin magentaqin changed the title fix: Sort environment variables based on priority fix: Override environment variables based on priority Jun 26, 2025
Copy link
Contributor

@Hofer-Julian Hofer-Julian left a comment

Choose a reason for hiding this comment

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

Looks promising at first glance.

Added a few comments for now, one of them might be the cause of your Windows struggles.

Will do a more extensive review next round

Comment on lines 403 to 419
"PIXI_PROJECT_ROOT",
"PIXI_PROJECT_NAME",
"PIXI_PROJECT_MANIFEST",
"PIXI_PROJECT_VERSION",
"PIXI_PROMPT",
"PIXI_ENVIRONMENT_NAME",
"PIXI_ENVIRONMENT_PLATFORMS",
"CONDA_PREFIX",
"CONDA_DEFAULT_ENV",
"PATH",
"INIT_CWD",
"PWD",
]
Copy link
Contributor

Choose a reason for hiding this comment

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

Good that you thought of that!

fn get_export_specific_task_env(task: &Task) -> String {
// Append the environment variables if they don't exist
/// Get the environment variable based on their priority
fn get_export_specific_task_env(task: &Task, command_env: &HashMap<OsString, OsString>) -> String {
Copy link
Contributor

Choose a reason for hiding this comment

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

Instead of converting it later on, just require HashMap<String, String> right away

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

good suggestion!

export.push_str(&format!("export \"{}={}\";\n", key, value));
// If task.env() and command_env don't have duplicated keys, simply export task.env().
if env.keys().all(|k| !command_env_converted.contains_key(k)) {
println!("env.keys() {:?}", env.keys());
Copy link
Contributor

Choose a reason for hiding this comment

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

Get rid of this println

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

sorry, I'll remove it. I was thinking about debugging on the windows pipeline.

}
} else {
// Env map
let mut env_map: HashMap<&'static str, Option<IndexMap<String, String>>> =
Copy link
Contributor

Choose a reason for hiding this comment

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

Suggested change
let mut env_map: HashMap<&'static str, Option<IndexMap<String, String>>> =
let mut env_map: HashMap<&'static str, IndexMap<String, String>> =

Is the Option really necessary here?

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

yes, because the value of TASK_SPECIFIC_ENVS can be None, and the value of env_map should stay the same. So I use Option here.

let should_exclude = override_excluded_keys.contains(key.as_str());
if !should_exclude {
tracing::info!("Setting environment variable: {}=\"{}\"", key, value);
// Platform-specific export format with proper escaping
Copy link
Contributor

Choose a reason for hiding this comment

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

We didn't have any of this escaping beforehand. Why is it necessary now?

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

I was trying to solve the windows pipeline error. Let me first remove it!

Copy link
Contributor

Choose a reason for hiding this comment

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

this platform specific export format should be deleted and just

export.push_str(&format!("export \"{}={}\";\n", key, value));

will be enough

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

let me try this!

manifest = tmp_pixi_workspace.joinpath("pixi.toml")
script_manifest = tmp_pixi_workspace.joinpath("env_setup.sh")
toml = f"""
[project]
Copy link
Contributor

Choose a reason for hiding this comment

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

Suggested change
[project]
[workspace]

We try to use workspace everywhere nowadays

pixi-foobar = "*"
"""
test_script_file = """
#!/bin/bash
Copy link
Contributor

Choose a reason for hiding this comment

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

This will only work on Unix.

Something like this should work:

[target.unix.activation]
scripts = ["scripts/activate.sh"]
[target.win-64.activation]
scripts = ["scripts/activate.bat"]

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

This will only work on Unix.

Something like this should work:

[target.unix.activation]
scripts = ["scripts/activate.sh"]
[target.win-64.activation]
scripts = ["scripts/activate.bat"]

good catch!

@magentaqin magentaqin force-pushed the bugfix/environment-variable-priority branch 3 times, most recently from 382b459 to 35cdb9f Compare July 3, 2025 05:37
@magentaqin magentaqin requested a review from Hofer-Julian July 3, 2025 08:58
@magentaqin magentaqin force-pushed the bugfix/environment-variable-priority branch 2 times, most recently from acb9791 to 1499388 Compare July 9, 2025 13:42
Copy link
Contributor

@Hofer-Julian Hofer-Julian left a comment

Choose a reason for hiding this comment

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

Let's clean up the tests a bit. Will then focus on the actual implementation.

Good work so far!

Comment on lines 1383 to 1390
[tasks.task]
cmd = "echo $MY_ENV"
[tasks.foo]
cmd = "echo $MY_ENV"
[tasks.foobar]
cmd = "echo $FOO_PATH"
[tasks.task.env]
MY_ENV = "test456"
Copy link
Contributor

Choose a reason for hiding this comment

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

Suggested change
[tasks.task]
cmd = "echo $MY_ENV"
[tasks.foo]
cmd = "echo $MY_ENV"
[tasks.foobar]
cmd = "echo $FOO_PATH"
[tasks.task.env]
MY_ENV = "test456"
[tasks.task]
cmd = "echo $MY_ENV"
env = { MY_ENV = "test456" }
[tasks.foo]
cmd = "echo $MY_ENV"
[tasks.foobar]
cmd = "echo $FOO_PATH"

this makes it a bit more readable IMO

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

fixed!

Comment on lines 1394 to 1399
windows_toml = f"""
[workspace]
name = "test"
channels = ["{dummy_channel_1}"]
platforms = ["linux-64", "osx-64", "osx-arm64", "win-64"]
[activation.env]
MY_ENV = "test123"
[target.unix.activation]
scripts = ["env_setup.sh"]
[target.win-64.activation]
scripts = ["env_setup.bat"]
[tasks.task]
cmd = "echo $env:MY_ENV"
[tasks.foo]
cmd = "echo $env:MY_ENV"
[tasks.foobar]
cmd = "echo $env:FOO_PATH"
[tasks.task.env]
MY_ENV = "test456"
[dependencies]
pixi-foobar = "*"
"""
Copy link
Contributor

Choose a reason for hiding this comment

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

What is the difference to the unix_toml?
Can't we use the same toml here?

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

it's the compatible windows test pipeline issue, let me revert to the original one!

Comment on lines 1416 to 1428
if platform.system() == "Windows":
manifest.write_text(windows_toml)
script_manifest.write_text("""
$env:MY_ENV = "activation_script"
$env:FOO_PATH = "activation_script"
""")
else:
manifest.write_text(unix_toml)
script_manifest.write_text("""
#!/bin/bash
export MY_ENV="activation_script"
export FOO_PATH="activation_script"
""")
Copy link
Contributor

Choose a reason for hiding this comment

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

We don't need an if condition here.
Just write it in both cases

Comment on lines 1436 to 1437
# Validate that without experimental it does not use the cache
assert not tmp_pixi_workspace.joinpath(".pixi/activation-env-v0").exists()
Copy link
Contributor

Choose a reason for hiding this comment

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

Suggested change
# Validate that without experimental it does not use the cache
assert not tmp_pixi_workspace.joinpath(".pixi/activation-env-v0").exists()
# Validate that without experimental caching it does not use the cache
assert not tmp_pixi_workspace.joinpath(".pixi/activation-env-v0").exists()

Comment on lines 1439 to 1428
# Enable the experimental cache config
verify_cli_command(
[
pixi,
"config",
"set",
"--manifest-path",
manifest,
"--local",
"experimental.use-environment-activation-cache",
"true",
],
)
Copy link
Contributor

Choose a reason for hiding this comment

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

I don't really see why you'd set this config here. I feel the test is already pretty complicated as is.

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

I'll remove it!

verify_cli_command(
[pixi, "run", "--manifest-path", manifest, "task"],
stdout_contains="test456",
env={"MY_ENV": "outside_ev"},
Copy link
Contributor

Choose a reason for hiding this comment

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

Suggested change
env={"MY_ENV": "outside_ev"},
env={"MY_ENV": "outside_env"},

verify_cli_command(
[pixi, "run", "--manifest-path", manifest, "foo"],
stdout_contains="test123",
env={"MY_ENV": "outside_ev"},
Copy link
Contributor

Choose a reason for hiding this comment

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

Suggested change
env={"MY_ENV": "outside_ev"},
env={"MY_ENV": "outside_env"},

# Test 3: activation.env > outside environment variable - should use activation.env
verify_cli_command(
[pixi, "run", "--manifest-path", manifest, "foo"],
stdout_contains="test123",
Copy link
Contributor

Choose a reason for hiding this comment

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

Suggested change
stdout_contains="test123",
stdout_contains="test123",
stdout_excludes="outside_env"

Comment on lines 1480 to 1483
# Run an actual install
verify_cli_command(
[pixi, "install", "--manifest-path", manifest],
)
Copy link
Contributor

Choose a reason for hiding this comment

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

Suggested change
# Run an actual install
verify_cli_command(
[pixi, "install", "--manifest-path", manifest],
)

That shouldn't be necessary

verify_cli_command(
[pixi, "run", "--manifest-path", manifest, "foobar"],
stdout_contains="activation_script",
)
Copy link
Contributor

Choose a reason for hiding this comment

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

I am missing tests here, that environment variables that are set from the outside and are NOT overridden by anything inside Pixi still work as expected

Copy link
Contributor

Choose a reason for hiding this comment

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

I would also choose the tests a bit different.

So you have this goal:

  1. outside environment variables
  2. activation scripts of dependencies
  3. activation.scripts
  4. activation.env
  5. task specific envs like tasks.start = {cmd = "…", env={ENV_VAR="SOME"}}

So add tests that 5>4, 4>3, 3>2, 2>1 and finally that 1 on its own works
Then you have everything covered

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

Sorry, I don't quite understand what you mean. Is the test order thing?

Copy link
Contributor

Choose a reason for hiding this comment

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

I want you to add asserts that "activation scripts of dependencies" wins over "outside environment variables".
"activation.scripts" should win over "activation scripts of dependencies", and so on. If nothing else is set "outside environment variables" should be considered.

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

2728253#diff-e6813bccd5c3cad11b9c048122f69487eafb612de804912383e94995b10f188b I added the test '"activation scripts of dependencies" wins over "outside environment variables", and update the test order. Have a look!

Comment on lines 630 to 634
let expected_prefix = if cfg!(windows) {
"env:FOO = \"bar\""
} else {
"export \"FOO=bar\""
};
Copy link
Contributor

Choose a reason for hiding this comment

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

Suggested change
let expected_prefix = if cfg!(windows) {
"env:FOO = \"bar\""
} else {
"export \"FOO=bar\""
};
let expected_prefix = "export \"FOO=bar\"";

@zelosleone
Copy link
Contributor

zelosleone commented Jul 11, 2025

I think there are more than one problem here for windows and in general for the logic of priority listing.

  • I don't think we should show all the system exports in output, there was a similar case with debugging mode being added in rattler-build and this looks very similar to that in terms of output, since we don't need all of them to be seen and just task-related ones will be enough. This is one of the problems for windows, as deno shell cannot parse everything related to windows correctly (programfiles env names etc.) not to mention that many of them at once, its having hard time even with a special logic to escape quotes.
  • Additionally, I think the order for placing the function that orders the tasks in priority should be moved to activation state as an async function. Something like:
pub async fn get_task_env_for_task(
    task: &Task,
    environment: &Environment<'_>,
    clean_env: bool,
    lock_file: Option<&LockFile>,
    force_activate: bool,
    experimental_cache: bool,
) -> miette::Result<HashMap<String, String>> {
    let mut env = get_task_env(
        environment,
        clean_env,
        lock_file,
        force_activate,
        experimental_cache,
    )
    .await?;
    if let Some(task_env) = task.env() {
        for (key, value) in task_env {
            env.insert(key.clone(), value.clone());
        }
    }

    Ok(env)
}

Should be more than enough. This will also limit the COMMAND_ENV and TASK_SPECIFIC_ENVS additions where you can safely delete them and just get them from the activation/runtime instead.

  • Additionally, tests should use .bat for windows (.ps1 extension will give errors)
  • Also, to add to backwards tick problem of windows you can place
.replace('\\', "\\\\")
.replace('"', "\\\"")
.replace('$', "\\$");

etc. before exporting them as combined env variables (before export push str)

@magentaqin
Copy link
Collaborator Author

(^▽^)わくわく, @zelosleone! Thanks for your great effort on this PR and nice suggestion.👍🏻

1.For the exported script, I agree with u. Now I update my strategy: only export the statement that has key in Task_Specific_Env map.
2. update tests using .bat for windows

Based on the above updates, the test pipeline on windows passed! 👏🏻

  1. For the priority order handling logic, I think we need further discussion. To be more decoupled, I still think they should be put in executable_task module instead of activation module.
  2. In the previous commits, I've tried escaping characters. But it has nothing to do with the error. As the old version of the functionality doesn't have escaping and there're a lot of escaping cases, I would suggest the task in a separate PR(...this PR hasn't been closed for a quite long time😭)

@magentaqin magentaqin force-pushed the bugfix/environment-variable-priority branch from 304022c to b74990b Compare July 16, 2025 12:22
@magentaqin magentaqin requested a review from Hofer-Julian July 18, 2025 08:57
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

4 participants