From d36e0f6ecd57b3c90d2f0253c02445df0cdeec1d Mon Sep 17 00:00:00 2001 From: faze-geek Date: Mon, 20 Jul 2026 00:59:02 +0530 Subject: [PATCH 1/2] Add Deactivate Command to Mamba helper flag --- micromamba/src/activate.cpp | 61 +++++++++++++++++++--------------- micromamba/src/umamba.cpp | 3 ++ micromamba/src/umamba.hpp | 3 ++ micromamba/tests/test_shell.py | 22 ++++++++++++ 4 files changed, 63 insertions(+), 26 deletions(-) diff --git a/micromamba/src/activate.cpp b/micromamba/src/activate.cpp index b9f9fcac92..c2a5a29724 100644 --- a/micromamba/src/activate.cpp +++ b/micromamba/src/activate.cpp @@ -46,6 +46,35 @@ namespace } return ""; } + // When 'activate' / 'deactivate' is run as subprocess, it cannot modify the parent shell. + // In that case, print instructions to initialize the shell and throw an exception to stop execution. + // If the shell is initialized, the 'activate' / 'deactivate' command are intercepted by the shell. + // The shell calls 'shell activate' / 'shell deactivate' instead, in that case the code below is never executed. + [[noreturn]] void notify_shell_not_initialized() + { + const std::string guessed_shell = guess_shell(); + + const std::string message = fmt::format( + "\n'{exe}' is running as a subprocess and can't modify the parent shell.\n" + "Thus you must initialize your shell before using activate and deactivate.\n" + "\n" + "{0}\n" + "To automatically initialize all future ({1}) shells, run:\n" + " $ {exe} shell init --shell {1} --root-prefix=~/.local/share/mamba\n" + "If your shell was already initialized, reinitialize your shell with:\n" + " $ {exe} shell reinit --shell {1}\n" + "Otherwise, this may be an issue. In the meantime you can run commands. See:\n" + " $ {exe} run --help\n" + "\n" + "Supported shells are {{bash, zsh, csh, posix, xonsh, cmd.exe, powershell, fish, nu}}.\n", + get_shell_hook(guessed_shell), + guessed_shell, + fmt::arg("exe", get_self_exe_path().stem().string()) + ); + + std::cout << message; + throw std::runtime_error("Shell not initialized"); + } } void @@ -61,31 +90,11 @@ set_activate_command(CLI::App* subcom) "Activate the specified environment without first deactivating the current one" ); - subcom->callback( - [&]() - { - const std::string guessed_shell = guess_shell(); - - const std::string message = fmt::format( - "\n'{exe}' is running as a subprocess and can't modify the parent shell.\n" - "Thus you must initialize your shell before using activate and deactivate.\n" - "\n" - "{0}\n" - "To automatically initialize all future ({1}) shells, run:\n" - " $ {exe} shell init --shell {1} --root-prefix=~/.local/share/mamba\n" - "If your shell was already initialized, reinitialize your shell with:\n" - " $ {exe} shell reinit --shell {1}\n" - "Otherwise, this may be an issue. In the meantime you can run commands. See:\n" - " $ {exe} run --help\n" - "\n" - "Supported shells are {{bash, zsh, csh, posix, xonsh, cmd.exe, powershell, fish, nu}}.\n", - get_shell_hook(guessed_shell), - guessed_shell, - fmt::arg("exe", get_self_exe_path().stem().string()) - ); + subcom->callback([&]() { notify_shell_not_initialized(); }); +} - std::cout << message; - throw std::runtime_error("Shell not initialized"); - } - ); +void +set_deactivate_command(CLI::App* subcom) +{ + subcom->callback([]() { notify_shell_not_initialized(); }); } diff --git a/micromamba/src/umamba.cpp b/micromamba/src/umamba.cpp index 97693b211f..6a9102f10a 100644 --- a/micromamba/src/umamba.cpp +++ b/micromamba/src/umamba.cpp @@ -103,6 +103,9 @@ set_umamba_command(CLI::App* com, mamba::Configuration& config) CLI::App* activate_subcom = com->add_subcommand("activate", "Activate an environment"); set_activate_command(activate_subcom); + CLI::App* deactivate_subcom = com->add_subcommand("deactivate", "Deactivate the active environment"); + set_deactivate_command(deactivate_subcom); + CLI::App* run_subcom = com->add_subcommand("run", "Run an executable in an environment"); set_run_command(run_subcom, config); diff --git a/micromamba/src/umamba.hpp b/micromamba/src/umamba.hpp index c19631c165..bc2b7eec5d 100644 --- a/micromamba/src/umamba.hpp +++ b/micromamba/src/umamba.hpp @@ -75,6 +75,9 @@ set_env_command(CLI::App* subcom, mamba::Configuration& config); void set_activate_command(CLI::App* subcom); +void +set_deactivate_command(CLI::App* subcom); + void set_run_command(CLI::App* subcom, mamba::Configuration& config); diff --git a/micromamba/tests/test_shell.py b/micromamba/tests/test_shell.py index 0e96004cf1..aeaef302bd 100644 --- a/micromamba/tests/test_shell.py +++ b/micromamba/tests/test_shell.py @@ -193,6 +193,28 @@ def test_activate_target_prefix_checks(tmp_home, tmp_root_prefix): assert not res["use_default_prefix_fallback"] assert not res["use_root_prefix_fallback"] +@pytest.mark.parametrize( + "command, description", + [ + ("activate", "Activate an environment"), + ("deactivate", "Deactivate the active environment"), + ], +) +def test_top_level_activate_deactivate_not_initialized( + tmp_home, tmp_root_prefix, command, description +): + """Test that top-level activate/deactivate commands are not initialized.""" + umamba = helpers.get_umamba() + + # Commands must be listed in top-level help. + top_help = helpers.subprocess_run(umamba, "--help").decode() + assert description in top_help + + # Commands run directly with no shell function aborts with error message. + with pytest.raises(subprocess.CalledProcessError) as excinfo: + helpers.subprocess_run(umamba, command) + assert "Shell not initialized" in excinfo.value.stderr.decode() + @pytest.mark.parametrize("shell_type", ["bash", "powershell", "cmd.exe"]) @pytest.mark.parametrize("prefix_selector", [None, "prefix"]) From 9e6da9db0ab2f97afc06fdcb89c331de18666b98 Mon Sep 17 00:00:00 2001 From: faze-geek Date: Mon, 20 Jul 2026 12:19:58 +0530 Subject: [PATCH 2/2] Format changes for pre-commit --- micromamba/src/activate.cpp | 8 +++++--- micromamba/tests/test_shell.py | 1 + 2 files changed, 6 insertions(+), 3 deletions(-) diff --git a/micromamba/src/activate.cpp b/micromamba/src/activate.cpp index c2a5a29724..8666b0ce97 100644 --- a/micromamba/src/activate.cpp +++ b/micromamba/src/activate.cpp @@ -46,10 +46,12 @@ namespace } return ""; } + // When 'activate' / 'deactivate' is run as subprocess, it cannot modify the parent shell. - // In that case, print instructions to initialize the shell and throw an exception to stop execution. - // If the shell is initialized, the 'activate' / 'deactivate' command are intercepted by the shell. - // The shell calls 'shell activate' / 'shell deactivate' instead, in that case the code below is never executed. + // In that case, print instructions to initialize the shell and throw an exception to stop + // execution. If the shell is initialized, the 'activate' / 'deactivate' command are intercepted + // by the shell. The shell calls 'shell activate' / 'shell deactivate' instead, in that case the + // code below is never executed. [[noreturn]] void notify_shell_not_initialized() { const std::string guessed_shell = guess_shell(); diff --git a/micromamba/tests/test_shell.py b/micromamba/tests/test_shell.py index aeaef302bd..399ff95904 100644 --- a/micromamba/tests/test_shell.py +++ b/micromamba/tests/test_shell.py @@ -193,6 +193,7 @@ def test_activate_target_prefix_checks(tmp_home, tmp_root_prefix): assert not res["use_default_prefix_fallback"] assert not res["use_root_prefix_fallback"] + @pytest.mark.parametrize( "command, description", [