diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index e86d2cf3f0..f34101e513 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -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 @@ -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 @@ -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 @@ -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 @@ -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 @@ -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 diff --git a/crates/pixi_core/src/workspace/mod.rs b/crates/pixi_core/src/workspace/mod.rs index 10299e26f4..af530b2d88 100644 --- a/crates/pixi_core/src/workspace/mod.rs +++ b/crates/pixi_core/src/workspace/mod.rs @@ -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, ) -> miette::Result> { - 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}'")) } @@ -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::*; diff --git a/crates/pixi_manifest/src/environment.rs b/crates/pixi_manifest/src/environment.rs index 3041f1ed03..a51c9f5b22 100644 --- a/crates/pixi_manifest/src/environment.rs +++ b/crates/pixi_manifest/src/environment.rs @@ -2,6 +2,7 @@ use std::{ borrow::Borrow, fmt, hash::{Hash, Hasher}, + path::Path, str::FromStr, }; @@ -59,13 +60,33 @@ 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, + workspace_root: &Path, ) -> Result { + // 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 { @@ -73,6 +94,7 @@ impl EnvironmentName { } return Ok(EnvironmentName::Named(env_var_name)); } + Ok(EnvironmentName::Default) } } diff --git a/tests/integration_python/common.py b/tests/integration_python/common.py index 949362efba..bb701f7065 100644 --- a/tests/integration_python/common.py +++ b/tests/integration_python/common.py @@ -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"} diff --git a/tests/integration_python/conftest.py b/tests/integration_python/conftest.py index a6acd13f8a..be5ade832a 100644 --- a/tests/integration_python/conftest.py +++ b/tests/integration_python/conftest.py @@ -1,4 +1,3 @@ -import os from pathlib import Path import pytest @@ -6,20 +5,6 @@ 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", diff --git a/tests/integration_python/pixi_build/common.py b/tests/integration_python/pixi_build/common.py index 0d67d10ee0..a7d703a785 100644 --- a/tests/integration_python/pixi_build/common.py +++ b/tests/integration_python/pixi_build/common.py @@ -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() diff --git a/tests/integration_python/pixi_build/test_specified_build_source/test_git.py b/tests/integration_python/pixi_build/test_specified_build_source/test_git.py index 4fee272636..4c4cec4187 100644 --- a/tests/integration_python/pixi_build/test_specified_build_source/test_git.py +++ b/tests/integration_python/pixi_build/test_specified_build_source/test_git.py @@ -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 diff --git a/tests/integration_python/test_manifest_path.py b/tests/integration_python/test_manifest_path.py index 27926d8e1d..359aac534e 100644 --- a/tests/integration_python/test_manifest_path.py +++ b/tests/integration_python/test_manifest_path.py @@ -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: @@ -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, + ) diff --git a/tests/integration_python/test_run_cli.py b/tests/integration_python/test_run_cli.py index e63384f122..6abaf501af 100644 --- a/tests/integration_python/test_run_cli.py +++ b/tests/integration_python/test_run_cli.py @@ -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"],