Skip to content
12 changes: 6 additions & 6 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -542,7 +542,7 @@ jobs:
path: ${{ env.TARGET_RELEASE }}
- name: Setup unix binary, add to github path
run: |
chmod a+x ${{ env.TARGET_RELEASE }}/pixi
chmod a+x ${{ env.TARGET_RELEASE }}/pixi ${{ env.TARGET_RELEASE }}/pixi-build-*
echo "${{ env.TARGET_RELEASE }}" >> $GITHUB_PATH
- name: Verify pixi installation
run: pixi info
Expand All @@ -568,7 +568,7 @@ jobs:
path: ${{ env.TARGET_RELEASE }}
- name: Setup unix binary, add to github path
run: |
chmod a+x ${{ env.TARGET_RELEASE }}/pixi
chmod a+x ${{ env.TARGET_RELEASE }}/pixi ${{ env.TARGET_RELEASE }}/pixi-build-*
echo "${{ env.TARGET_RELEASE }}" >> $GITHUB_PATH
- name: Verify pixi installation
run: pixi info
Expand Down Expand Up @@ -632,7 +632,7 @@ jobs:
path: ${{ env.TARGET_RELEASE }}
- name: Setup unix binary, add to github path
run: |
chmod a+x ${{ env.TARGET_RELEASE }}/pixi
chmod a+x ${{ env.TARGET_RELEASE }}/pixi ${{ env.TARGET_RELEASE }}/pixi-build-*
echo "${{ env.TARGET_RELEASE }}" >> $GITHUB_PATH
- name: Verify pixi installation
run: pixi info
Expand Down Expand Up @@ -668,7 +668,7 @@ jobs:
path: ${{ env.TARGET_RELEASE }}
- name: Setup unix binary, add to github path
run: |
chmod a+x ${{ env.TARGET_RELEASE }}/pixi
chmod a+x ${{ env.TARGET_RELEASE }}/pixi ${{ env.TARGET_RELEASE }}/pixi-build-*
echo "${{ env.TARGET_RELEASE }}" >> $GITHUB_PATH
- name: Verify pixi installation
run: pixi info
Expand Down Expand Up @@ -766,7 +766,7 @@ jobs:
path: ${{ env.TARGET_RELEASE }}
- name: Setup unix binary, add to github path
run: |
chmod a+x ${{ env.TARGET_RELEASE }}/pixi
chmod a+x ${{ env.TARGET_RELEASE }}/pixi ${{ env.TARGET_RELEASE }}/pixi-build-*
echo "${{ env.TARGET_RELEASE }}" >> $GITHUB_PATH
- name: Verify pixi installation
run: pixi info
Expand Down Expand Up @@ -821,7 +821,7 @@ jobs:
path: ${{ env.TARGET_RELEASE }}
- name: Setup unix binary, add to github path
run: |
chmod a+x ${{ env.TARGET_RELEASE }}/pixi
chmod a+x ${{ env.TARGET_RELEASE }}/pixi ${{ env.TARGET_RELEASE }}/pixi-build-*
echo "${{ env.TARGET_RELEASE }}" >> $GITHUB_PATH
- name: Verify pixi installation
run: pixi info
Expand Down
13 changes: 12 additions & 1 deletion crates/pixi_core/src/workspace/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -452,11 +452,21 @@ impl Workspace {

/// Returns an environment in this project based on a name or an environment
/// variable.
///
/// If no explicit name is provided, this function will try to read the
/// environment name from the `PIXI_ENVIRONMENT_NAME` environment variable.
/// However, if `PIXI_PROJECT_ROOT` is set and differs from this workspace's
/// root, the environment variable is ignored and the default environment
/// is returned instead. This handles the case where a pixi task runs
/// another pixi project via `--manifest-path` - the child process should
/// not inherit the parent's environment name.
pub fn environment_from_name_or_env_var(
&self,
name: Option<String>,
) -> miette::Result<Environment<'_>> {
let environment_name = EnvironmentName::from_arg_or_env_var(name).into_diagnostic()?;
let environment_name =
EnvironmentName::from_arg_or_env_var(name, self.root()).into_diagnostic()?;

self.environment(&environment_name)
.ok_or_else(|| miette::miette!("unknown environment '{environment_name}'"))
}
Expand Down Expand Up @@ -928,6 +938,7 @@ mod tests {
use pixi_manifest::{FeatureName, FeaturesExt};
use rattler_conda_types::{Platform, Version};
use rattler_virtual_packages::{LibC, VirtualPackage};
use std::env;

use super::*;

Expand Down
26 changes: 24 additions & 2 deletions crates/pixi_manifest/src/environment.rs
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ use std::{
borrow::Borrow,
fmt,
hash::{Hash, Hasher},
path::Path,
str::FromStr,
};

Expand Down Expand Up @@ -59,20 +60,41 @@ impl EnvironmentName {

/// Tries to read the environment name from an argument, then it will try
/// to read from an environment variable, otherwise it will fall back to
/// default
/// default.
///
/// If `PIXI_PROJECT_ROOT` is set to a path different from `workspace_root`,
/// the environment variable fallback is skipped. This handles the case
/// where a pixi task runs another pixi project via `--manifest-path` - the
/// child process should not inherit the parent's environment name.
pub fn from_arg_or_env_var(
arg_name: Option<String>,
workspace_root: &Path,
) -> Result<Self, ParseEnvironmentNameError> {
// If an explicit name is provided, use it
if let Some(arg_name) = arg_name {
return EnvironmentName::from_str(&arg_name);
} else if std::env::var("PIXI_IN_SHELL").is_ok()
}

// Check if we should ignore PIXI_ env vars because they belong to a
// different workspace
let should_ignore_env_vars = std::env::var("PIXI_PROJECT_ROOT")
.ok()
.is_some_and(|pixi_root| Path::new(&pixi_root) != workspace_root);

if should_ignore_env_vars {
return Ok(EnvironmentName::Default);
}

// Try to read from environment variable
if std::env::var("PIXI_IN_SHELL").is_ok()
&& let Ok(env_var_name) = std::env::var("PIXI_ENVIRONMENT_NAME")
{
if env_var_name == DEFAULT_ENVIRONMENT_NAME {
return Ok(EnvironmentName::Default);
}
return Ok(EnvironmentName::Named(env_var_name));
}

Ok(EnvironmentName::Default)
}
}
Expand Down
2 changes: 0 additions & 2 deletions tests/integration_python/common.py
Original file line number Diff line number Diff line change
Expand Up @@ -67,8 +67,6 @@ def verify_cli_command(
strip_ansi: bool = False,
) -> Output:
base_env = {} if reset_env else dict(os.environ)
# Remove all PIXI_ prefixed env vars to avoid interference from the outer environment
base_env = {k: v for k, v in base_env.items() if not k.startswith("PIXI_")}
complete_env = base_env if env is None else base_env | env
# Set `PIXI_NO_WRAP` to avoid to have miette wrapping lines
complete_env |= {"PIXI_NO_WRAP": "1"}
Expand Down
15 changes: 0 additions & 15 deletions tests/integration_python/conftest.py
Original file line number Diff line number Diff line change
@@ -1,25 +1,10 @@
import os
from pathlib import Path

import pytest

from .common import CONDA_FORGE_CHANNEL, exec_extension


@pytest.fixture(autouse=True)
def clean_pixi_env_vars(monkeypatch: pytest.MonkeyPatch) -> None:
"""Remove all PIXI_ prefixed environment variables before each test.

Since tests are run via `pixi run`, the environment contains PIXI_ variables
(like PIXI_IN_SHELL, PIXI_PROJECT_ROOT, etc.) that can interfere with the
pixi commands being tested. This fixture ensures each test starts with a
clean environment.
"""
for key in list(os.environ.keys()):
if key.startswith("PIXI_"):
monkeypatch.delenv(key, raising=False)


def pytest_addoption(parser: pytest.Parser) -> None:
parser.addoption(
"--pixi-build",
Expand Down
2 changes: 1 addition & 1 deletion tests/integration_python/pixi_build/common.py
Original file line number Diff line number Diff line change
Expand Up @@ -167,4 +167,4 @@ def git_test_repo(source_dir: Path, repo_name: str, target_dir: Path) -> str:
capture_output=True,
)

return f"file://{repo_path}"
return repo_path.as_uri()
Original file line number Diff line number Diff line change
Expand Up @@ -59,7 +59,7 @@ def configure_local_git_source(
source = manifest.setdefault("package", {}).setdefault("build", {}).setdefault("source", {})
for key in ("branch", "tag", "rev"):
source.pop(key, None)
source["git"] = "file://" + str(repo.path.as_posix())
source["git"] = repo.path.as_uri()
source["subdirectory"] = subdirectory
if rev is not None:
source["rev"] = rev
Expand Down
50 changes: 49 additions & 1 deletion tests/integration_python/test_manifest_path.py
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import json
from pathlib import Path

from .common import EMPTY_BOILERPLATE_PROJECT, verify_cli_command
from .common import CURRENT_PLATFORM, EMPTY_BOILERPLATE_PROJECT, ExitCode, verify_cli_command


def test_explicit_manifest_correct_location(pixi: Path, tmp_path: Path) -> None:
Expand Down Expand Up @@ -31,3 +31,51 @@ def test_explicit_manifest_correct_location(pixi: Path, tmp_path: Path) -> None:
expected = (target_dir / "pixi.toml").resolve()
actual = Path(value).resolve()
assert actual == expected


def test_ignore_env_vars_when_manifest_path_differs(pixi: Path, tmp_pixi_workspace: Path) -> None:
"""When running a nested pixi command with --manifest-path pointing to a different
project, the inherited PIXI_ENVIRONMENT_NAME should be ignored because it refers
to an environment in the parent project, not the child project.

Scenario:
- Parent project (at /parent) has a "test" environment with a task that runs
`pixi run --manifest-path /child`
- Child project (at /child) does NOT have a "test" environment
- When the parent task runs, it sets PIXI_PROJECT_ROOT=/parent,
PIXI_ENVIRONMENT_NAME=test, PIXI_IN_SHELL=1
- The child pixi command should ignore PIXI_ENVIRONMENT_NAME because
PIXI_PROJECT_ROOT differs from the child's workspace root
"""
# Create a child project that only has the default environment (no "test" env)
child_dir = tmp_pixi_workspace / "child"
child_dir.mkdir()
child_manifest = child_dir / "pixi.toml"
child_manifest.write_text(f"""
[workspace]
name = "child-project"
channels = []
platforms = ["{CURRENT_PLATFORM}"]
""")

# Simulate being inside a parent pixi shell by setting env vars
# that point to a DIFFERENT project root
different_project_root = "/some/other/project"

# The child project root should differ from our simulated parent
assert str(child_dir.resolve()) != different_project_root

# Run pixi shell-hook with --manifest-path pointing to child project,
# while env vars simulate being in a parent shell with a "test" environment.
# shell-hook needs to select an environment, so if PIXI_ENVIRONMENT_NAME is
# NOT ignored, this would fail with "unknown environment 'test'" since the
# child project has no "test" env.
verify_cli_command(
[pixi, "shell-hook", "--manifest-path", child_manifest],
env={
"PIXI_PROJECT_ROOT": different_project_root,
"PIXI_IN_SHELL": "1",
"PIXI_ENVIRONMENT_NAME": "test",
},
expected_exit_code=ExitCode.SUCCESS,
)
6 changes: 5 additions & 1 deletion tests/integration_python/test_run_cli.py
Original file line number Diff line number Diff line change
Expand Up @@ -58,7 +58,11 @@ def test_run_in_shell_environment(pixi: Path, tmp_pixi_workspace: Path) -> None:
)

# Simulate activated shell in environment 'a'
env = {"PIXI_IN_SHELL": "true", "PIXI_ENVIRONMENT_NAME": "a"}
env = {
"PIXI_IN_SHELL": "true",
"PIXI_ENVIRONMENT_NAME": "a",
"PIXI_PROJECT_ROOT": str(tmp_pixi_workspace),
}
verify_cli_command(
[pixi, "run", "--manifest-path", manifest, "task"],
stdout_contains=["a", "a1"],
Expand Down
Loading