From a235170802efa309430355c2e8ca5c8530a91632 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Denizhan=20Dak=C4=B1l=C4=B1r?= Date: Tue, 8 Jul 2025 17:57:09 +0300 Subject: [PATCH 01/20] feat: add support for PIXI_ENVIRONMENT_NAME and PS1 prompt modification --- src/cli/exec.rs | 50 ++++++++++++++--- tests/integration_python/common.py | 40 ++++++++++++-- tests/integration_python/test_exec.py | 79 ++++++++++++++++++++++++++- 3 files changed, 156 insertions(+), 13 deletions(-) diff --git a/src/cli/exec.rs b/src/cli/exec.rs index 8b5e560e46..70f2093cbe 100644 --- a/src/cli/exec.rs +++ b/src/cli/exec.rs @@ -1,4 +1,4 @@ -use std::{path::Path, str::FromStr, sync::LazyLock}; +use std::{collections::BTreeSet, path::Path, str::FromStr, sync::LazyLock}; use clap::{Parser, ValueHint}; use itertools::Itertools; @@ -59,6 +59,10 @@ pub struct Args { #[clap(long = "list", num_args = 0..=1, default_missing_value = "", require_equals = true)] pub list: Option, + /// Disable modification of the PS1 prompt to indicate the temporary environment + #[clap(long, action = clap::ArgAction::SetFalse)] + pub no_modify_ps1: bool, + #[clap(flatten)] pub config: ConfigCli, } @@ -76,7 +80,38 @@ pub async fn execute(args: Args) -> miette::Result<()> { let prefix = create_exec_prefix(&args, &cache_dir, &config, &client).await?; // Get environment variables from the activation - let activation_env = run_activation(&prefix).await?; + let mut activation_env = run_activation(&prefix).await?; + + // Collect unique package names and set environment variables if any are specified + let package_names: BTreeSet = args + .specs + .iter() + .chain(args.with.iter()) + .filter_map(|spec| spec.name.as_ref().map(|n| n.as_normalized().to_string())) + .collect(); + + if !package_names.is_empty() { + let env_name = format!( + "temp:{}", + package_names.into_iter().collect::>().join(",") + ); + + activation_env.insert("PIXI_ENVIRONMENT_NAME".into(), env_name.clone()); + + if args.no_modify_ps1 && std::env::current_dir().is_ok() { + let (prompt_var, prompt_value) = if cfg!(windows) { + ("_PIXI_PROMPT", format!("(pixi:{}) $P$G", env_name)) + } else { + ("PS1", format!(r"(pixi:{}) [\w] \$ ", env_name)) + }; + + activation_env.insert(prompt_var.into(), prompt_value); + + if cfg!(windows) { + activation_env.insert("PROMPT".into(), String::from("$P$G")); + } + } + } // Ignore CTRL+C so that the child is responsible for its own signal handling. let _ctrl_c = tokio::spawn(async { while tokio::signal::ctrl_c().await.is_ok() {} }); @@ -149,7 +184,9 @@ pub async fn create_exec_prefix( let gateway = config.gateway().with_client(client.clone()).finish(); // Determine the specs to use for the environment - let specs = if args.specs.is_empty() { + let mut specs = args.specs.clone(); + specs.extend(args.with.clone()); + let specs = if specs.is_empty() { let command = args.command.first().expect("missing required command"); let guessed_spec = guess_package_spec(command); @@ -157,12 +194,9 @@ pub async fn create_exec_prefix( "no specs provided, guessed {} from command {command}", guessed_spec ); - - let mut with_specs = args.with.clone(); - with_specs.push(guessed_spec); - with_specs + vec![guessed_spec] } else { - args.specs.clone() + specs }; let channels = args.channels.resolve_from_config(config)?; diff --git a/tests/integration_python/common.py b/tests/integration_python/common.py index aafb877000..6f4f4e6da9 100644 --- a/tests/integration_python/common.py +++ b/tests/integration_python/common.py @@ -4,7 +4,8 @@ from contextlib import contextmanager from enum import IntEnum from pathlib import Path -from typing import Generator +import sys +from typing import Generator, Optional, Sequence, Tuple from rattler import Platform @@ -31,12 +32,12 @@ class ExitCode(IntEnum): class Output: - command: list[Path | str] + command: Sequence[Path | str] stdout: str stderr: str returncode: int - def __init__(self, command: list[Path | str], stdout: str, stderr: str, returncode: int): + def __init__(self, command: Sequence[Path | str], stdout: str, stderr: str, returncode: int): self.command = command self.stdout = stdout self.stderr = stderr @@ -47,7 +48,7 @@ def __str__(self) -> str: def verify_cli_command( - command: list[Path | str], + command: Sequence[Path | str], expected_exit_code: ExitCode = ExitCode.SUCCESS, stdout_contains: str | list[str] | None = None, stdout_excludes: str | list[str] | None = None, @@ -165,3 +166,34 @@ def cwd(path: str | Path) -> Generator[None, None, None]: yield finally: os.chdir(oldpwd) + + +def run_and_get_env(pixi: Path, *args: str, env_var: str) -> Tuple[Optional[str], Output]: + if sys.platform.startswith("win"): + cmd = [str(pixi), "exec", *args, "--", "cmd", "/c", f"echo %{env_var}%"] + else: + cmd = [str(pixi), "exec", *args, "--", "printenv", env_var] + + try: + result = subprocess.run( + cmd, + capture_output=True, + text=True, + encoding="utf-8", + errors="replace", + ) + + value = result.stdout.strip() + + output = Output( + command=cmd, + stdout=value, + stderr=result.stderr.strip(), + returncode=result.returncode, + ) + + return (value if value and value != f"%{env_var}%" else None, output) + except Exception as e: + print(f"Error running command: {e}") + print(f"Command: {' '.join(cmd)}") + raise diff --git a/tests/integration_python/test_exec.py b/tests/integration_python/test_exec.py index 9b4f8fe18c..24a3370269 100644 --- a/tests/integration_python/test_exec.py +++ b/tests/integration_python/test_exec.py @@ -4,7 +4,7 @@ import pytest -from .common import ExitCode, verify_cli_command +from .common import ExitCode, verify_cli_command, run_and_get_env @pytest.mark.skipif( @@ -56,6 +56,83 @@ def test_exec_list(pixi: Path, dummy_channel_1: str) -> None: stdout_excludes="dummy-b", ) + # List specific package + verify_cli_command( + [pixi, "exec", "--channel", dummy_channel_1, "--list=dummy-g", "dummy-g"], + stdout_contains=["dummy-g"], + stdout_excludes=["dummy-b"], + ) + + +def test_pixi_environment_name_and_ps1(pixi: Path, dummy_channel_1: str) -> None: + """Test that PIXI_ENVIRONMENT_NAME and PS1/PROMPT are set correctly.""" + # Test with single package + env_value, _ = run_and_get_env( + pixi, "--channel", dummy_channel_1, "-s", "dummy-a", env_var="PIXI_ENVIRONMENT_NAME" + ) + assert env_value == "temp:dummy-a" + + # Test with multiple packages (should be sorted) + env_value, _ = run_and_get_env( + pixi, + "--channel", + dummy_channel_1, + "-s", + "dummy-c", + "-s", + "dummy-a", + env_var="PIXI_ENVIRONMENT_NAME", + ) + assert env_value == "temp:dummy-a,dummy-c" + + # Test with --with flag + env_value, _ = run_and_get_env( + pixi, + "--channel", + dummy_channel_1, + "--with", + "dummy-b", + "--with", + "dummy-c", + env_var="PIXI_ENVIRONMENT_NAME", + ) + assert env_value == "temp:dummy-b,dummy-c" + + # Test with no specs (should not set the variable) + env_value, _ = run_and_get_env( + pixi, "--channel", dummy_channel_1, env_var="PIXI_ENVIRONMENT_NAME" + ) + assert env_value is None + + # Test PS1 modification + if sys.platform.startswith("win"): + prompt_var = "_PIXI_PROMPT" + expected_prompt = "(pixi:temp:dummy-a) $P$G" + else: + prompt_var = "PS1" + expected_prompt = r"(pixi:temp:dummy-a) [\w] \$ " + + # Test with default behavior (prompt should be modified) + prompt, _ = run_and_get_env( + pixi, "--channel", dummy_channel_1, "-s", "dummy-a", env_var=prompt_var + ) + assert prompt == expected_prompt + + # Test with --no-modify-ps1 (prompt should not be modified) + prompt, _ = run_and_get_env( + pixi, + "--channel", + dummy_channel_1, + "--no-modify-ps1", + "-s", + "dummy-a", + env_var=prompt_var, + ) + if sys.platform.startswith("win"): + assert prompt is None + else: + assert prompt is None or "(pixi:temp:dummy-a)" not in prompt + @pytest.mark.skipif( sys.platform.startswith("win"), From 5cdb8db1a8bb3028c4f7e6d2b37983a16cea1979 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Denizhan=20Dak=C4=B1l=C4=B1r?= Date: Wed, 9 Jul 2025 13:45:47 +0300 Subject: [PATCH 02/20] lets try this --- src/cli/exec.rs | 71 ++++++++++++++++++++++++++++--------------------- 1 file changed, 41 insertions(+), 30 deletions(-) diff --git a/src/cli/exec.rs b/src/cli/exec.rs index 70f2093cbe..b1c34bf3f7 100644 --- a/src/cli/exec.rs +++ b/src/cli/exec.rs @@ -1,4 +1,4 @@ -use std::{collections::BTreeSet, path::Path, str::FromStr, sync::LazyLock}; +use std::{collections::BTreeSet, collections::HashMap, path::Path, str::FromStr, sync::LazyLock}; use clap::{Parser, ValueHint}; use itertools::Itertools; @@ -26,10 +26,10 @@ use crate::{ /// /// Remove the temporary environments with `pixi clean cache --exec`. #[derive(Parser, Debug)] -#[clap(trailing_var_arg = true, arg_required_else_help = true)] +#[clap(arg_required_else_help = true)] pub struct Args { /// The executable to run, followed by any arguments. - #[clap(num_args = 1.., value_hint = ValueHint::CommandWithArguments)] + #[clap(value_hint = ValueHint::CommandWithArguments, last = true)] pub command: Vec, /// Matchspecs of package to install. @@ -72,21 +72,33 @@ pub async fn execute(args: Args) -> miette::Result<()> { let config = Config::with_cli_config(&args.config); let cache_dir = pixi_config::get_cache_dir().context("failed to determine cache directory")?; - let mut command_args = args.command.iter(); - let command = command_args.next().ok_or_else(|| miette::miette!(help ="i.e when specifying specs explicitly use a command at the end: `pixi exec -s python==3.12 python`", "missing required command to execute",))?; + let mut command_iter = args.command.iter(); + let command = command_iter.next().ok_or_else(|| miette::miette!(help ="i.e when specifying specs explicitly use a command at the end: `pixi exec -s python==3.12 python`", "missing required command to execute",))?; let (_, client) = build_reqwest_clients(Some(&config), None)?; + // Determine the specs to use for the environment + let mut specs = args.specs.clone(); + specs.extend(args.with.clone()); + let specs = if specs.is_empty() { + let guessed_spec = guess_package_spec(command); + tracing::debug!( + "no specs provided, guessed {} from command {command}", + guessed_spec + ); + vec![guessed_spec] + } else { + specs + }; + // Create the environment to run the command in. - let prefix = create_exec_prefix(&args, &cache_dir, &config, &client).await?; + let prefix = create_exec_prefix(&args, &specs, &cache_dir, &config, &client).await?; // Get environment variables from the activation let mut activation_env = run_activation(&prefix).await?; // Collect unique package names and set environment variables if any are specified - let package_names: BTreeSet = args - .specs + let package_names: BTreeSet = specs .iter() - .chain(args.with.iter()) .filter_map(|spec| spec.name.as_ref().map(|n| n.as_normalized().to_string())) .collect(); @@ -117,9 +129,21 @@ pub async fn execute(args: Args) -> miette::Result<()> { let _ctrl_c = tokio::spawn(async { while tokio::signal::ctrl_c().await.is_ok() {} }); // Spawn the command - let status = std::process::Command::new(command) - .args(command_args) - .envs(activation_env.iter().map(|(k, v)| (k.as_str(), v.as_str()))) + let mut cmd = std::process::Command::new(command); + let command_args_vec: Vec<_> = command_iter.collect(); + cmd.args(&command_args_vec); + + // On Windows, when using cmd.exe or cmd, we need to set environment variables in a special way + // because cmd.exe has its own environment variable expansion rules + if cfg!(windows) && (command.to_lowercase().ends_with("cmd.exe") || command == "cmd") { + let mut env = std::env::vars().collect::>(); + env.extend(activation_env); + cmd.envs(env); + } else { + cmd.envs(activation_env.iter().map(|(k, v)| (k.as_str(), v.as_str()))); + } + + let status = cmd .status() .into_diagnostic() .with_context(|| format!("failed to execute '{}'", &command))?; @@ -131,12 +155,14 @@ pub async fn execute(args: Args) -> miette::Result<()> { /// Creates a prefix for the `pixi exec` command. pub async fn create_exec_prefix( args: &Args, + specs: &[MatchSpec], cache_dir: &Path, config: &Config, client: &ClientWithMiddleware, ) -> miette::Result { let command = args.command.first().expect("missing required command"); - let specs = args.specs.clone(); + let specs = specs.to_vec(); + let channels = args .channels .resolve_from_config(config)? @@ -144,7 +170,8 @@ pub async fn create_exec_prefix( .map(|c| c.base_url.to_string()) .collect(); - let environment_hash = EnvironmentHash::new(command.clone(), specs, channels, args.platform); + let environment_hash = + EnvironmentHash::new(command.clone(), specs.clone(), channels, args.platform); let prefix = Prefix::new( cache_dir @@ -183,22 +210,6 @@ pub async fn create_exec_prefix( // Construct a gateway to get repodata. let gateway = config.gateway().with_client(client.clone()).finish(); - // Determine the specs to use for the environment - let mut specs = args.specs.clone(); - specs.extend(args.with.clone()); - let specs = if specs.is_empty() { - let command = args.command.first().expect("missing required command"); - let guessed_spec = guess_package_spec(command); - - tracing::debug!( - "no specs provided, guessed {} from command {command}", - guessed_spec - ); - vec![guessed_spec] - } else { - specs - }; - let channels = args.channels.resolve_from_config(config)?; // Get the repodata for the specs From 011ebdb711680cf5ad571c0488a6dbe9ea4a55f5 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Denizhan=20Dak=C4=B1l=C4=B1r?= Date: Wed, 9 Jul 2025 13:53:42 +0300 Subject: [PATCH 03/20] docs --- docs/reference/cli/pixi/exec.md | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/docs/reference/cli/pixi/exec.md b/docs/reference/cli/pixi/exec.md index 496fed2c7a..e1f6b94c12 100644 --- a/docs/reference/cli/pixi/exec.md +++ b/docs/reference/cli/pixi/exec.md @@ -8,7 +8,7 @@ Run a command and install it in a temporary environment ## Usage ``` -pixi exec [OPTIONS] [COMMAND]... +pixi exec [OPTIONS] [-- ...] ``` ## Arguments @@ -33,6 +33,8 @@ pixi exec [OPTIONS] [COMMAND]... : If specified a new environment is always created even if one already exists - `--list ` : Before executing the command, list packages in the environment Specify `--list=some_regex` to filter the shown packages +- `--no-modify-ps1` +: Disable modification of the PS1 prompt to indicate the temporary environment ## Config Options - `--tls-no-verify` From dec0d22036c17eab443f1dd38f27ddcf9c2396a8 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Denizhan=20Dak=C4=B1l=C4=B1r?= Date: Wed, 9 Jul 2025 14:09:57 +0300 Subject: [PATCH 04/20] test --- tests/integration_python/test_exec.py | 20 +++++++++++--------- 1 file changed, 11 insertions(+), 9 deletions(-) diff --git a/tests/integration_python/test_exec.py b/tests/integration_python/test_exec.py index 24a3370269..eb57a28489 100644 --- a/tests/integration_python/test_exec.py +++ b/tests/integration_python/test_exec.py @@ -17,12 +17,12 @@ def test_concurrent_exec(pixi: Path, dummy_channel_1: str) -> None: futures = [ executor.submit( verify_cli_command, - [pixi, "exec", "-c", dummy_channel_1, "dummy-f"], + [pixi, "exec", "-c", dummy_channel_1, "--", "dummy-f"], stdout_contains=["dummy-f on"], ), executor.submit( verify_cli_command, - [pixi, "exec", "-c", dummy_channel_1, "dummy-f"], + [pixi, "exec", "-c", dummy_channel_1, "--", "dummy-f"], stdout_contains=["dummy-f on"], ), ] @@ -39,26 +39,26 @@ def test_concurrent_exec(pixi: Path, dummy_channel_1: str) -> None: def test_exec_list(pixi: Path, dummy_channel_1: str) -> None: # Without `--list`, nothing is listed verify_cli_command( - [pixi, "exec", "--channel", dummy_channel_1, "dummy-g"], + [pixi, "exec", "--channel", dummy_channel_1, "--", "dummy-g"], stdout_excludes=["dummy-g"], ) # List all packages in environment verify_cli_command( - [pixi, "exec", "--channel", dummy_channel_1, "--list", "dummy-g"], + [pixi, "exec", "--channel", dummy_channel_1, "--list", "--", "dummy-g"], stdout_contains=["dummy-g", "dummy-b"], ) # List only packages that match regex "g" verify_cli_command( - [pixi, "exec", "--channel", dummy_channel_1, "--list=g", "dummy-g"], + [pixi, "exec", "--channel", dummy_channel_1, "--list=g", "--", "dummy-g"], stdout_contains="dummy-g", stdout_excludes="dummy-b", ) # List specific package verify_cli_command( - [pixi, "exec", "--channel", dummy_channel_1, "--list=dummy-g", "dummy-g"], + [pixi, "exec", "--channel", dummy_channel_1, "--list=dummy-g", "--", "dummy-g"], stdout_contains=["dummy-g"], stdout_excludes=["dummy-b"], ) @@ -110,7 +110,7 @@ def test_pixi_environment_name_and_ps1(pixi: Path, dummy_channel_1: str) -> None expected_prompt = "(pixi:temp:dummy-a) $P$G" else: prompt_var = "PS1" - expected_prompt = r"(pixi:temp:dummy-a) [\w] \$ " + expected_prompt = r"(pixi:temp:dummy-a) [\w] \$" # Test with default behavior (prompt should be modified) prompt, _ = run_and_get_env( @@ -141,12 +141,12 @@ def test_pixi_environment_name_and_ps1(pixi: Path, dummy_channel_1: str) -> None def test_exec_with(pixi: Path, dummy_channel_1: str) -> None: # A package is guessed from the command when `--with` is provided verify_cli_command( - [pixi, "exec", "--channel", dummy_channel_1, "--list", "--spec=dummy-a", "dummy-b"], + [pixi, "exec", "--channel", dummy_channel_1, "--list", "--spec=dummy-a", "--", "dummy-b"], stdout_excludes="dummy-b", expected_exit_code=ExitCode.FAILURE, ) verify_cli_command( - [pixi, "exec", "--channel", dummy_channel_1, "--list", "--with=dummy-a", "dummy-b"], + [pixi, "exec", "--channel", dummy_channel_1, "--list", "--with=dummy-a", "--", "dummy-b"], stdout_contains="dummy-b", ) @@ -160,6 +160,7 @@ def test_exec_with(pixi: Path, dummy_channel_1: str) -> None: "--list", "--with=dummy-a", "--with=dummy-b", + "--", "dummy-f", ], stdout_contains=["dummy-a", "dummy-b", "dummy-f"], @@ -175,6 +176,7 @@ def test_exec_with(pixi: Path, dummy_channel_1: str) -> None: "--list", "--with=dummy-a", "--spec=dummy-b", + "--", "dummy-f", ], expected_exit_code=ExitCode.INCORRECT_USAGE, From beea9bda88fc2f4d09ca11a49c76c5166467b4ae Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Denizhan=20Dak=C4=B1l=C4=B1r?= Date: Wed, 9 Jul 2025 14:18:04 +0300 Subject: [PATCH 05/20] dummy b to dummy f --- tests/integration_python/test_exec.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/tests/integration_python/test_exec.py b/tests/integration_python/test_exec.py index eb57a28489..40c603160b 100644 --- a/tests/integration_python/test_exec.py +++ b/tests/integration_python/test_exec.py @@ -141,13 +141,13 @@ def test_pixi_environment_name_and_ps1(pixi: Path, dummy_channel_1: str) -> None def test_exec_with(pixi: Path, dummy_channel_1: str) -> None: # A package is guessed from the command when `--with` is provided verify_cli_command( - [pixi, "exec", "--channel", dummy_channel_1, "--list", "--spec=dummy-a", "--", "dummy-b"], - stdout_excludes="dummy-b", + [pixi, "exec", "--channel", dummy_channel_1, "--list", "--spec=dummy-a", "--", "dummy-f"], + stdout_excludes="dummy-f", expected_exit_code=ExitCode.FAILURE, ) verify_cli_command( - [pixi, "exec", "--channel", dummy_channel_1, "--list", "--with=dummy-a", "--", "dummy-b"], - stdout_contains="dummy-b", + [pixi, "exec", "--channel", dummy_channel_1, "--list", "--with=dummy-a", "--", "dummy-f"], + stdout_contains="dummy-f", ) # Correct behaviour with multiple 'with' options From ed086b1429a4e5e05bf4d10bdde57b6395554d7b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Denizhan=20Dak=C4=B1l=C4=B1r?= Date: Wed, 9 Jul 2025 16:44:58 +0300 Subject: [PATCH 06/20] Trigger CI pipeline From 0d0515a94651568d3f54837521c07761fbeb38c3 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Denizhan=20Dak=C4=B1l=C4=B1r?= Date: Wed, 9 Jul 2025 16:52:50 +0300 Subject: [PATCH 07/20] . --- src/cli/exec.rs | 25 ++++++++++++++++--------- 1 file changed, 16 insertions(+), 9 deletions(-) diff --git a/src/cli/exec.rs b/src/cli/exec.rs index b1c34bf3f7..514854b35b 100644 --- a/src/cli/exec.rs +++ b/src/cli/exec.rs @@ -79,16 +79,23 @@ pub async fn execute(args: Args) -> miette::Result<()> { // Determine the specs to use for the environment let mut specs = args.specs.clone(); specs.extend(args.with.clone()); - let specs = if specs.is_empty() { + + // If --with is used or no specs are provided, guess the package from the command + if !args.with.is_empty() || specs.is_empty() { let guessed_spec = guess_package_spec(command); - tracing::debug!( - "no specs provided, guessed {} from command {command}", - guessed_spec - ); - vec![guessed_spec] - } else { - specs - }; + if !args.with.is_empty() { + tracing::debug!( + "using --with, adding guessed spec {} from command {command}", + guessed_spec + ); + } else { + tracing::debug!( + "no specs provided, guessed {} from command {command}", + guessed_spec + ); + } + specs.push(guessed_spec); + } // Create the environment to run the command in. let prefix = create_exec_prefix(&args, &specs, &cache_dir, &config, &client).await?; From 73967b1c27c2e3a7e3a7d985aeb9849b600f9eb4 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Denizhan=20Dak=C4=B1l=C4=B1r?= Date: Wed, 9 Jul 2025 17:03:22 +0300 Subject: [PATCH 08/20] . --- src/cli/exec.rs | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/src/cli/exec.rs b/src/cli/exec.rs index 514854b35b..f7d16b3365 100644 --- a/src/cli/exec.rs +++ b/src/cli/exec.rs @@ -80,6 +80,9 @@ pub async fn execute(args: Args) -> miette::Result<()> { let mut specs = args.specs.clone(); specs.extend(args.with.clone()); + // Track explicit specs for environment naming (excluding guessed packages) + let explicit_specs = specs.clone(); + // If --with is used or no specs are provided, guess the package from the command if !args.with.is_empty() || specs.is_empty() { let guessed_spec = guess_package_spec(command); @@ -103,8 +106,8 @@ pub async fn execute(args: Args) -> miette::Result<()> { // Get environment variables from the activation let mut activation_env = run_activation(&prefix).await?; - // Collect unique package names and set environment variables if any are specified - let package_names: BTreeSet = specs + // Collect unique package names from explicit specs only for environment naming + let package_names: BTreeSet = explicit_specs .iter() .filter_map(|spec| spec.name.as_ref().map(|n| n.as_normalized().to_string())) .collect(); From d206d490429e2aeb2ea493f8ee1c93bc71c0eaec Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Denizhan=20Dak=C4=B1l=C4=B1r?= Date: Wed, 9 Jul 2025 17:32:51 +0300 Subject: [PATCH 09/20] . --- src/cli/exec.rs | 27 +++++++++++++++++++-------- 1 file changed, 19 insertions(+), 8 deletions(-) diff --git a/src/cli/exec.rs b/src/cli/exec.rs index f7d16b3365..8a28f1a048 100644 --- a/src/cli/exec.rs +++ b/src/cli/exec.rs @@ -80,9 +80,6 @@ pub async fn execute(args: Args) -> miette::Result<()> { let mut specs = args.specs.clone(); specs.extend(args.with.clone()); - // Track explicit specs for environment naming (excluding guessed packages) - let explicit_specs = specs.clone(); - // If --with is used or no specs are provided, guess the package from the command if !args.with.is_empty() || specs.is_empty() { let guessed_spec = guess_package_spec(command); @@ -106,11 +103,25 @@ pub async fn execute(args: Args) -> miette::Result<()> { // Get environment variables from the activation let mut activation_env = run_activation(&prefix).await?; - // Collect unique package names from explicit specs only for environment naming - let package_names: BTreeSet = explicit_specs - .iter() - .filter_map(|spec| spec.name.as_ref().map(|n| n.as_normalized().to_string())) - .collect(); + // Collect unique package names for environment naming + // Only include explicitly specified packages (--spec and --with), not guessed ones + let mut explicit_package_names: BTreeSet = BTreeSet::new(); + + // Add packages from --spec flags + for spec in &args.specs { + if let Some(name) = &spec.name { + explicit_package_names.insert(name.as_normalized().to_string()); + } + } + + // Add packages from --with flags + for spec in &args.with { + if let Some(name) = &spec.name { + explicit_package_names.insert(name.as_normalized().to_string()); + } + } + + let package_names = explicit_package_names; if !package_names.is_empty() { let env_name = format!( From cf1cbc2c6a765fb36eec7be57626535f2a160529 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Denizhan=20Dak=C4=B1l=C4=B1r?= Date: Wed, 9 Jul 2025 17:44:50 +0300 Subject: [PATCH 10/20] . --- src/cli/exec.rs | 29 ++++++++++++----------------- 1 file changed, 12 insertions(+), 17 deletions(-) diff --git a/src/cli/exec.rs b/src/cli/exec.rs index 8a28f1a048..bd28ed668f 100644 --- a/src/cli/exec.rs +++ b/src/cli/exec.rs @@ -104,24 +104,19 @@ pub async fn execute(args: Args) -> miette::Result<()> { let mut activation_env = run_activation(&prefix).await?; // Collect unique package names for environment naming - // Only include explicitly specified packages (--spec and --with), not guessed ones - let mut explicit_package_names: BTreeSet = BTreeSet::new(); - - // Add packages from --spec flags - for spec in &args.specs { - if let Some(name) = &spec.name { - explicit_package_names.insert(name.as_normalized().to_string()); - } - } - - // Add packages from --with flags - for spec in &args.with { - if let Some(name) = &spec.name { - explicit_package_names.insert(name.as_normalized().to_string()); - } - } + // Exclude the guessed package (which is always the last one if --with is used or no specs provided) + let specs_for_env_name = if !args.with.is_empty() || (args.specs.is_empty() && specs.len() > 0) { + // If we added a guessed package, exclude the last spec from environment naming + &specs[..specs.len().saturating_sub(1)] + } else { + // No guessed package was added, use all specs + &specs[..] + }; - let package_names = explicit_package_names; + let package_names: BTreeSet = specs_for_env_name + .iter() + .filter_map(|spec| spec.name.as_ref().map(|n| n.as_normalized().to_string())) + .collect(); if !package_names.is_empty() { let env_name = format!( From da93dadab90c6b38b13114c2f6806e2c3ddaa4d8 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Denizhan=20Dak=C4=B1l=C4=B1r?= Date: Wed, 9 Jul 2025 20:16:14 +0300 Subject: [PATCH 11/20] . --- src/cli/exec.rs | 36 +++++++++++++++++++----------------- 1 file changed, 19 insertions(+), 17 deletions(-) diff --git a/src/cli/exec.rs b/src/cli/exec.rs index bd28ed668f..0207e93e94 100644 --- a/src/cli/exec.rs +++ b/src/cli/exec.rs @@ -80,20 +80,13 @@ pub async fn execute(args: Args) -> miette::Result<()> { let mut specs = args.specs.clone(); specs.extend(args.with.clone()); - // If --with is used or no specs are provided, guess the package from the command - if !args.with.is_empty() || specs.is_empty() { + // Only add the guessed package if both args.specs and args.with are empty + if args.specs.is_empty() && args.with.is_empty() { let guessed_spec = guess_package_spec(command); - if !args.with.is_empty() { - tracing::debug!( - "using --with, adding guessed spec {} from command {command}", - guessed_spec - ); - } else { - tracing::debug!( - "no specs provided, guessed {} from command {command}", - guessed_spec - ); - } + tracing::debug!( + "no specs provided, guessed {} from command {command}", + guessed_spec + ); specs.push(guessed_spec); } @@ -104,12 +97,21 @@ pub async fn execute(args: Args) -> miette::Result<()> { let mut activation_env = run_activation(&prefix).await?; // Collect unique package names for environment naming - // Exclude the guessed package (which is always the last one if --with is used or no specs provided) - let specs_for_env_name = if !args.with.is_empty() || (args.specs.is_empty() && specs.len() > 0) { - // If we added a guessed package, exclude the last spec from environment naming + // Only exclude the guessed package if it was actually added due to no specs being provided + let guessed_spec = guess_package_spec(command); + let guessed_package_added = specs.last().map(|s| s.name.as_ref()) == Some(guessed_spec.name.as_ref()); + + let specs_for_env_name = if !args.with.is_empty() { + // If --with is used, use all --with specs for the environment name + &args.with[..] + } else if !args.specs.is_empty() { + // If -s is used, use all -s specs for the environment name + &args.specs[..] + } else if guessed_package_added && specs.len() > 1 { + // If a guessed package was added (and there are other specs), exclude the last spec &specs[..specs.len().saturating_sub(1)] } else { - // No guessed package was added, use all specs + // Use all specs (should only be the guessed one) &specs[..] }; From 41d8d33f9ccf66bb819585693cb60bddf3f4e1f3 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Denizhan=20Dak=C4=B1l=C4=B1r?= Date: Thu, 10 Jul 2025 12:26:09 +0300 Subject: [PATCH 12/20] . --- src/cli/exec.rs | 43 +++++++++++-------------------------------- 1 file changed, 11 insertions(+), 32 deletions(-) diff --git a/src/cli/exec.rs b/src/cli/exec.rs index 0207e93e94..ae7016cd1a 100644 --- a/src/cli/exec.rs +++ b/src/cli/exec.rs @@ -76,46 +76,25 @@ pub async fn execute(args: Args) -> miette::Result<()> { let command = command_iter.next().ok_or_else(|| miette::miette!(help ="i.e when specifying specs explicitly use a command at the end: `pixi exec -s python==3.12 python`", "missing required command to execute",))?; let (_, client) = build_reqwest_clients(Some(&config), None)?; - // Determine the specs to use for the environment - let mut specs = args.specs.clone(); - specs.extend(args.with.clone()); - - // Only add the guessed package if both args.specs and args.with are empty - if args.specs.is_empty() && args.with.is_empty() { - let guessed_spec = guess_package_spec(command); - tracing::debug!( - "no specs provided, guessed {} from command {command}", - guessed_spec - ); - specs.push(guessed_spec); + // Determine the specs for installation and for the environment name. + let mut name_specs = args.specs.clone(); + name_specs.extend(args.with.clone()); + + let mut install_specs = name_specs.clone(); + + // Only guess a package from the command if no specs were provided at all + if name_specs.is_empty() { + install_specs.push(guess_package_spec(command)); } // Create the environment to run the command in. - let prefix = create_exec_prefix(&args, &specs, &cache_dir, &config, &client).await?; + let prefix = create_exec_prefix(&args, &install_specs, &cache_dir, &config, &client).await?; // Get environment variables from the activation let mut activation_env = run_activation(&prefix).await?; // Collect unique package names for environment naming - // Only exclude the guessed package if it was actually added due to no specs being provided - let guessed_spec = guess_package_spec(command); - let guessed_package_added = specs.last().map(|s| s.name.as_ref()) == Some(guessed_spec.name.as_ref()); - - let specs_for_env_name = if !args.with.is_empty() { - // If --with is used, use all --with specs for the environment name - &args.with[..] - } else if !args.specs.is_empty() { - // If -s is used, use all -s specs for the environment name - &args.specs[..] - } else if guessed_package_added && specs.len() > 1 { - // If a guessed package was added (and there are other specs), exclude the last spec - &specs[..specs.len().saturating_sub(1)] - } else { - // Use all specs (should only be the guessed one) - &specs[..] - }; - - let package_names: BTreeSet = specs_for_env_name + let package_names: BTreeSet = name_specs .iter() .filter_map(|spec| spec.name.as_ref().map(|n| n.as_normalized().to_string())) .collect(); From babae6260e7d0236f5b2b21123b3728f93bf3065 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Denizhan=20Dak=C4=B1l=C4=B1r?= Date: Thu, 10 Jul 2025 14:26:34 +0300 Subject: [PATCH 13/20] . --- src/cli/exec.rs | 80 ++++++++++++++++++++++++++++++++++++------------- 1 file changed, 60 insertions(+), 20 deletions(-) diff --git a/src/cli/exec.rs b/src/cli/exec.rs index ae7016cd1a..2325752f99 100644 --- a/src/cli/exec.rs +++ b/src/cli/exec.rs @@ -60,7 +60,7 @@ pub struct Args { pub list: Option, /// Disable modification of the PS1 prompt to indicate the temporary environment - #[clap(long, action = clap::ArgAction::SetFalse)] + #[clap(long, action = clap::ArgAction::SetTrue, default_value = "false")] pub no_modify_ps1: bool, #[clap(flatten)] @@ -82,13 +82,22 @@ pub async fn execute(args: Args) -> miette::Result<()> { let mut install_specs = name_specs.clone(); - // Only guess a package from the command if no specs were provided at all - if name_specs.is_empty() { + // Guess a package from the command if no specs were provided at all OR if --with is used + let should_guess_package = name_specs.is_empty() || !args.with.is_empty(); + if should_guess_package { install_specs.push(guess_package_spec(command)); } // Create the environment to run the command in. - let prefix = create_exec_prefix(&args, &install_specs, &cache_dir, &config, &client).await?; + let prefix = create_exec_prefix( + &args, + &install_specs, + &cache_dir, + &config, + &client, + should_guess_package, + ) + .await?; // Get environment variables from the activation let mut activation_env = run_activation(&prefix).await?; @@ -107,11 +116,11 @@ pub async fn execute(args: Args) -> miette::Result<()> { activation_env.insert("PIXI_ENVIRONMENT_NAME".into(), env_name.clone()); - if args.no_modify_ps1 && std::env::current_dir().is_ok() { + if !args.no_modify_ps1 && std::env::current_dir().is_ok() { let (prompt_var, prompt_value) = if cfg!(windows) { ("_PIXI_PROMPT", format!("(pixi:{}) $P$G", env_name)) } else { - ("PS1", format!(r"(pixi:{}) [\w] \$ ", env_name)) + ("PS1", format!(r"(pixi:{}) [\w] \$", env_name)) }; activation_env.insert(prompt_var.into(), prompt_value); @@ -156,6 +165,7 @@ pub async fn create_exec_prefix( cache_dir: &Path, config: &Config, client: &ClientWithMiddleware, + has_guessed_package: bool, ) -> miette::Result { let command = args.command.first().expect("missing required command"); let specs = specs.to_vec(); @@ -222,13 +232,14 @@ pub async fn create_exec_prefix( .context("failed to get repodata")?; // Determine virtual packages of the current platform - let virtual_packages = VirtualPackage::detect(&VirtualPackageOverrides::from_env()) - .into_diagnostic() - .context("failed to determine virtual packages")? - .iter() - .cloned() - .map(GenericVirtualPackage::from) - .collect(); + let virtual_packages: Vec = + VirtualPackage::detect(&VirtualPackageOverrides::from_env()) + .into_diagnostic() + .context("failed to determine virtual packages")? + .iter() + .cloned() + .map(GenericVirtualPackage::from) + .collect(); // Solve the environment tracing::info!( @@ -239,15 +250,44 @@ pub async fn create_exec_prefix( .display() ); let specs_clone = specs.clone(); - let solved_records = wrap_in_progress("solving environment", move || { + let virtual_packages_clone = virtual_packages.clone(); + let repodata_clone = repodata.clone(); + let solve_result = wrap_in_progress("solving environment", move || { Solver.solve(SolverTask { specs: specs_clone, - virtual_packages, - ..SolverTask::from_iter(&repodata) + virtual_packages: virtual_packages_clone, + ..SolverTask::from_iter(&repodata_clone) }) - }) - .into_diagnostic() - .context("failed to solve environment")?; + }); + + let (solved_records, final_specs) = match solve_result { + Ok(records) => (records, specs.to_vec()), + Err(err) if has_guessed_package && !args.with.is_empty() => { + // If solving failed and we guessed a package while using --with, + // try again without the guessed package (last spec) + tracing::debug!( + "Solver failed with guessed package, retrying without it: {}", + err + ); + let specs_without_guess = &specs[..specs.len() - 1]; + let specs_clone = specs_without_guess.to_vec(); + let records = wrap_in_progress("retrying solve without guessed package", move || { + Solver.solve(SolverTask { + specs: specs_clone, + virtual_packages, + ..SolverTask::from_iter(&repodata) + }) + }) + .into_diagnostic() + .context("failed to solve environment even without guessed package")?; + (records, specs_without_guess.to_vec()) + } + Err(err) => { + return Err(err) + .into_diagnostic() + .context("failed to solve environment"); + } + }; // Force the initialization of the rayon thread pool to avoid implicit creation // by the Installer. @@ -274,7 +314,7 @@ pub async fn create_exec_prefix( write_guard.finish().await.into_diagnostic()?; if let Some(ref regex) = args.list { - list_exec_environment(specs, solved_records, regex.clone())?; + list_exec_environment(final_specs, solved_records, regex.clone())?; } Ok(prefix) From 38a653bdd4320b06cfd6ba92f1438a43e2d46534 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Denizhan=20Dak=C4=B1l=C4=B1r?= Date: Thu, 10 Jul 2025 14:35:17 +0300 Subject: [PATCH 14/20] docs --- docs/reference/cli/pixi/exec.md | 1 + 1 file changed, 1 insertion(+) diff --git a/docs/reference/cli/pixi/exec.md b/docs/reference/cli/pixi/exec.md index e1f6b94c12..46d100e033 100644 --- a/docs/reference/cli/pixi/exec.md +++ b/docs/reference/cli/pixi/exec.md @@ -35,6 +35,7 @@ pixi exec [OPTIONS] [-- ...] : Before executing the command, list packages in the environment Specify `--list=some_regex` to filter the shown packages - `--no-modify-ps1` : Disable modification of the PS1 prompt to indicate the temporary environment +
**default**: `false` ## Config Options - `--tls-no-verify` From 1b47c3813451c3fc5270563e2eb8e9d0f7fca083 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Denizhan=20Dak=C4=B1l=C4=B1r?= Date: Thu, 10 Jul 2025 15:43:56 +0300 Subject: [PATCH 15/20] direct calls --- src/cli/exec.rs | 23 +++++++++-------------- 1 file changed, 9 insertions(+), 14 deletions(-) diff --git a/src/cli/exec.rs b/src/cli/exec.rs index 2325752f99..acf063b5d5 100644 --- a/src/cli/exec.rs +++ b/src/cli/exec.rs @@ -249,14 +249,11 @@ pub async fn create_exec_prefix( .unwrap_or(prefix.root()) .display() ); - let specs_clone = specs.clone(); - let virtual_packages_clone = virtual_packages.clone(); - let repodata_clone = repodata.clone(); - let solve_result = wrap_in_progress("solving environment", move || { + let solve_result = wrap_in_progress("solving environment", || { Solver.solve(SolverTask { - specs: specs_clone, - virtual_packages: virtual_packages_clone, - ..SolverTask::from_iter(&repodata_clone) + specs: specs.clone(), + virtual_packages: virtual_packages.clone(), + ..SolverTask::from_iter(&repodata.clone()) }) }); @@ -269,18 +266,16 @@ pub async fn create_exec_prefix( "Solver failed with guessed package, retrying without it: {}", err ); - let specs_without_guess = &specs[..specs.len() - 1]; - let specs_clone = specs_without_guess.to_vec(); - let records = wrap_in_progress("retrying solve without guessed package", move || { + let records = wrap_in_progress("retrying solve without guessed package", || { Solver.solve(SolverTask { - specs: specs_clone, - virtual_packages, - ..SolverTask::from_iter(&repodata) + specs: specs[..specs.len() - 1].to_vec(), + virtual_packages: virtual_packages.clone(), + ..SolverTask::from_iter(&repodata.clone()) }) }) .into_diagnostic() .context("failed to solve environment even without guessed package")?; - (records, specs_without_guess.to_vec()) + (records, specs[..specs.len() - 1].to_vec()) } Err(err) => { return Err(err) From 7c8db20845b0ee8b40b8a7e24fa9d7682e1bec8f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Denizhan=20Dak=C4=B1l=C4=B1r?= Date: Thu, 10 Jul 2025 16:34:55 +0300 Subject: [PATCH 16/20] cli changes revert and trailing_var_Arg to clap --- src/cli/exec.rs | 4 ++-- tests/integration_python/test_exec.py | 18 ++++++++---------- 2 files changed, 10 insertions(+), 12 deletions(-) diff --git a/src/cli/exec.rs b/src/cli/exec.rs index acf063b5d5..a87811d85f 100644 --- a/src/cli/exec.rs +++ b/src/cli/exec.rs @@ -26,10 +26,10 @@ use crate::{ /// /// Remove the temporary environments with `pixi clean cache --exec`. #[derive(Parser, Debug)] -#[clap(arg_required_else_help = true)] +#[clap(trailing_var_arg = true, arg_required_else_help = true)] pub struct Args { /// The executable to run, followed by any arguments. - #[clap(value_hint = ValueHint::CommandWithArguments, last = true)] + #[clap(num_args = 1.., value_hint = ValueHint::CommandWithArguments)] pub command: Vec, /// Matchspecs of package to install. diff --git a/tests/integration_python/test_exec.py b/tests/integration_python/test_exec.py index 40c603160b..7395889448 100644 --- a/tests/integration_python/test_exec.py +++ b/tests/integration_python/test_exec.py @@ -17,12 +17,12 @@ def test_concurrent_exec(pixi: Path, dummy_channel_1: str) -> None: futures = [ executor.submit( verify_cli_command, - [pixi, "exec", "-c", dummy_channel_1, "--", "dummy-f"], + [pixi, "exec", "-c", dummy_channel_1, "dummy-f"], stdout_contains=["dummy-f on"], ), executor.submit( verify_cli_command, - [pixi, "exec", "-c", dummy_channel_1, "--", "dummy-f"], + [pixi, "exec", "-c", dummy_channel_1, "dummy-f"], stdout_contains=["dummy-f on"], ), ] @@ -39,26 +39,26 @@ def test_concurrent_exec(pixi: Path, dummy_channel_1: str) -> None: def test_exec_list(pixi: Path, dummy_channel_1: str) -> None: # Without `--list`, nothing is listed verify_cli_command( - [pixi, "exec", "--channel", dummy_channel_1, "--", "dummy-g"], + [pixi, "exec", "--channel", dummy_channel_1, "dummy-g"], stdout_excludes=["dummy-g"], ) # List all packages in environment verify_cli_command( - [pixi, "exec", "--channel", dummy_channel_1, "--list", "--", "dummy-g"], + [pixi, "exec", "--channel", dummy_channel_1, "--list", "dummy-g"], stdout_contains=["dummy-g", "dummy-b"], ) # List only packages that match regex "g" verify_cli_command( - [pixi, "exec", "--channel", dummy_channel_1, "--list=g", "--", "dummy-g"], + [pixi, "exec", "--channel", dummy_channel_1, "--list=g", "dummy-g"], stdout_contains="dummy-g", stdout_excludes="dummy-b", ) # List specific package verify_cli_command( - [pixi, "exec", "--channel", dummy_channel_1, "--list=dummy-g", "--", "dummy-g"], + [pixi, "exec", "--channel", dummy_channel_1, "--list=dummy-g", "dummy-g"], stdout_contains=["dummy-g"], stdout_excludes=["dummy-b"], ) @@ -141,12 +141,12 @@ def test_pixi_environment_name_and_ps1(pixi: Path, dummy_channel_1: str) -> None def test_exec_with(pixi: Path, dummy_channel_1: str) -> None: # A package is guessed from the command when `--with` is provided verify_cli_command( - [pixi, "exec", "--channel", dummy_channel_1, "--list", "--spec=dummy-a", "--", "dummy-f"], + [pixi, "exec", "--channel", dummy_channel_1, "--list", "--spec=dummy-a", "dummy-f"], stdout_excludes="dummy-f", expected_exit_code=ExitCode.FAILURE, ) verify_cli_command( - [pixi, "exec", "--channel", dummy_channel_1, "--list", "--with=dummy-a", "--", "dummy-f"], + [pixi, "exec", "--channel", dummy_channel_1, "--list", "--with=dummy-a", "dummy-f"], stdout_contains="dummy-f", ) @@ -160,7 +160,6 @@ def test_exec_with(pixi: Path, dummy_channel_1: str) -> None: "--list", "--with=dummy-a", "--with=dummy-b", - "--", "dummy-f", ], stdout_contains=["dummy-a", "dummy-b", "dummy-f"], @@ -176,7 +175,6 @@ def test_exec_with(pixi: Path, dummy_channel_1: str) -> None: "--list", "--with=dummy-a", "--spec=dummy-b", - "--", "dummy-f", ], expected_exit_code=ExitCode.INCORRECT_USAGE, From 11de5fd459ac88fa3c411be4d40cbb5b5b9ec62d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Denizhan=20Dak=C4=B1l=C4=B1r?= Date: Thu, 10 Jul 2025 16:39:25 +0300 Subject: [PATCH 17/20] docs update --- docs/reference/cli/pixi/exec.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/reference/cli/pixi/exec.md b/docs/reference/cli/pixi/exec.md index 46d100e033..b52ef9f4d7 100644 --- a/docs/reference/cli/pixi/exec.md +++ b/docs/reference/cli/pixi/exec.md @@ -8,7 +8,7 @@ Run a command and install it in a temporary environment ## Usage ``` -pixi exec [OPTIONS] [-- ...] +pixi exec [OPTIONS] [COMMAND]... ``` ## Arguments From d9f35e11f8ba4c8323bdde38c95c87ec76802fae Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Denizhan=20Dak=C4=B1l=C4=B1r?= Date: Mon, 14 Jul 2025 16:02:56 +0300 Subject: [PATCH 18/20] refining. --- src/cli/exec.rs | 18 +++++++++++------- 1 file changed, 11 insertions(+), 7 deletions(-) diff --git a/src/cli/exec.rs b/src/cli/exec.rs index a87811d85f..b0b6a44802 100644 --- a/src/cli/exec.rs +++ b/src/cli/exec.rs @@ -12,7 +12,7 @@ use rattler::{ }; use rattler_conda_types::{GenericVirtualPackage, MatchSpec, PackageName, Platform}; use rattler_solve::{SolverImpl, SolverTask, resolvo::Solver}; -use rattler_virtual_packages::{VirtualPackage, VirtualPackageOverrides}; +use rattler_virtual_packages::{VirtualPackageOverrides, VirtualPackages}; use reqwest_middleware::ClientWithMiddleware; use uv_configuration::RAYON_INITIALIZE; @@ -111,7 +111,7 @@ pub async fn execute(args: Args) -> miette::Result<()> { if !package_names.is_empty() { let env_name = format!( "temp:{}", - package_names.into_iter().collect::>().join(",") + package_names.into_iter().format(",") ); activation_env.insert("PIXI_ENVIRONMENT_NAME".into(), env_name.clone()); @@ -233,12 +233,10 @@ pub async fn create_exec_prefix( // Determine virtual packages of the current platform let virtual_packages: Vec = - VirtualPackage::detect(&VirtualPackageOverrides::from_env()) + VirtualPackages::detect(&VirtualPackageOverrides::from_env()) .into_diagnostic() .context("failed to determine virtual packages")? - .iter() - .cloned() - .map(GenericVirtualPackage::from) + .into_generic_virtual_packages() .collect(); // Solve the environment @@ -262,8 +260,14 @@ pub async fn create_exec_prefix( Err(err) if has_guessed_package && !args.with.is_empty() => { // If solving failed and we guessed a package while using --with, // try again without the guessed package (last spec) + let guessed_package_name = specs[specs.len() - 1] + .name + .as_ref() + .map(|name| name.as_source()) + .unwrap_or(""); tracing::debug!( - "Solver failed with guessed package, retrying without it: {}", + "Solver failed with guessed package '{}', retrying without it: {}", + guessed_package_name, err ); let records = wrap_in_progress("retrying solve without guessed package", || { From c9379c8c7e6ce2300f0eeaaad4d1fa53d66b6128 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Denizhan=20Dak=C4=B1l=C4=B1r?= Date: Thu, 31 Jul 2025 11:51:25 +0300 Subject: [PATCH 19/20] Update src/cli/exec.rs Co-authored-by: Hofer-Julian <30049909+Hofer-Julian@users.noreply.github.com> --- src/cli/exec.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/cli/exec.rs b/src/cli/exec.rs index b0b6a44802..c337e5f6af 100644 --- a/src/cli/exec.rs +++ b/src/cli/exec.rs @@ -60,7 +60,7 @@ pub struct Args { pub list: Option, /// Disable modification of the PS1 prompt to indicate the temporary environment - #[clap(long, action = clap::ArgAction::SetTrue, default_value = "false")] + #[clap(long)] pub no_modify_ps1: bool, #[clap(flatten)] From 167cac45362cfba6a836f435a69d92204f06f5e8 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Denizhan=20Dak=C4=B1l=C4=B1r?= Date: Fri, 8 Aug 2025 12:56:32 +0300 Subject: [PATCH 20/20] last comment refining --- docs/reference/cli/pixi/exec.md | 1 - src/cli/exec.rs | 10 ++++------ 2 files changed, 4 insertions(+), 7 deletions(-) diff --git a/docs/reference/cli/pixi/exec.md b/docs/reference/cli/pixi/exec.md index fc66c6162b..8cae4feb2a 100644 --- a/docs/reference/cli/pixi/exec.md +++ b/docs/reference/cli/pixi/exec.md @@ -35,7 +35,6 @@ pixi exec [OPTIONS] [COMMAND]... : Before executing the command, list packages in the environment Specify `--list=some_regex` to filter the shown packages - `--no-modify-ps1` : Disable modification of the PS1 prompt to indicate the temporary environment -
**default**: `false` ## Config Options - `--auth-file ` diff --git a/src/cli/exec.rs b/src/cli/exec.rs index c337e5f6af..1a85666b6e 100644 --- a/src/cli/exec.rs +++ b/src/cli/exec.rs @@ -109,10 +109,7 @@ pub async fn execute(args: Args) -> miette::Result<()> { .collect(); if !package_names.is_empty() { - let env_name = format!( - "temp:{}", - package_names.into_iter().format(",") - ); + let env_name = format!("temp:{}", package_names.into_iter().format(",")); activation_env.insert("PIXI_ENVIRONMENT_NAME".into(), env_name.clone()); @@ -139,8 +136,9 @@ pub async fn execute(args: Args) -> miette::Result<()> { let command_args_vec: Vec<_> = command_iter.collect(); cmd.args(&command_args_vec); - // On Windows, when using cmd.exe or cmd, we need to set environment variables in a special way - // because cmd.exe has its own environment variable expansion rules + // On Windows, when using cmd.exe or cmd, we need to pass the full environment + // because cmd.exe requires access to all environment variables (including prompt variables) + // to properly display the modified prompt if cfg!(windows) && (command.to_lowercase().ends_with("cmd.exe") || command == "cmd") { let mut env = std::env::vars().collect::>(); env.extend(activation_env);