diff --git a/libmamba/src/api/configuration.cpp b/libmamba/src/api/configuration.cpp index f1e2ae3251..ead2fdea14 100644 --- a/libmamba/src/api/configuration.cpp +++ b/libmamba/src/api/configuration.cpp @@ -754,7 +754,7 @@ namespace mamba } else { - throw std::move(result).value(); + throw std::move(result).error(); } LOG_TRACE << "Using default root prefix for micromamba: " << root_prefix; diff --git a/libmamba/src/core/shell_init.cpp b/libmamba/src/core/shell_init.cpp index 4f3e4b0d6d..ef4206b30e 100644 --- a/libmamba/src/core/shell_init.cpp +++ b/libmamba/src/core/shell_init.cpp @@ -800,10 +800,20 @@ namespace mamba { const ShellInitPathsWindowsCmd paths{ root_prefix }; + // The default root prefix is validated on the next invocation, so it must + // be a valid conda prefix (with `pkgs`, `conda-meta` or `envs` folder) from the start. + // It used to contain only the hook scripts -> every following run aborted silently. + std::error_code ec; + fs::create_directories(root_prefix / "conda-meta", ec); + if (ec) + { + LOG_ERROR << "Failed to create directory '" << (root_prefix / "conda-meta").string() + << "' : " << ec.message(); + } for (const auto& directory : paths.every_generated_directories_paths()) { // Maybe the prefix isn't writable. No big deal, just keep going. - std::error_code maybe_error [[maybe_unused]]; + std::error_code maybe_error; fs::create_directories(directory, maybe_error); if (maybe_error) { diff --git a/micromamba/src/main.cpp b/micromamba/src/main.cpp index a1559c9589..4c6a171727 100644 --- a/micromamba/src/main.cpp +++ b/micromamba/src/main.cpp @@ -16,6 +16,7 @@ #endif #include +#include #include @@ -83,158 +84,198 @@ decide_log_handler(const ContextOptions& options) -> mamba::logging::AnyLogHandl return mamba::logging::spdlogimpl::LogHandler_spdlog{}; } +void +report_error(int argc, char** argv, const std::string& message) +{ + const auto options = decide_preconfig_context_options(argc, argv); + if (options.output_params and options.output_params->json) + { + nlohmann::json output{ + { "success", false }, + { "log_history", + nlohmann::json::array( + { { { "message", message }, { "level", "critical" }, { "source", "libmamba" } } } + ) } + }; + std::cout << output.dump(4) << std::endl; + } + else if (not options.output_params or not options.output_params->quiet) + { + std::cerr << message << std::endl; + } +} + int main(int argc, char** argv) { - mamba::MainExecutor scoped_threads; - const auto pre_config_options = decide_preconfig_context_options(argc, argv); - mamba::Context ctx{ pre_config_options, decide_log_handler(pre_config_options) }; - mamba::Console console{ ctx }; - mamba::Configuration config{ ctx }; + try + { + mamba::MainExecutor scoped_threads; + const auto pre_config_options = decide_preconfig_context_options(argc, argv); + mamba::Context ctx{ pre_config_options, decide_log_handler(pre_config_options) }; + mamba::Console console{ ctx }; + mamba::Configuration config{ ctx }; - init_console(); - mamba::on_scope_exit _console_reset{ [] { reset_console(); } }; + init_console(); + mamba::on_scope_exit _console_reset{ [] { reset_console(); } }; - ctx.command_params.is_mamba_exe = true; + ctx.command_params.is_mamba_exe = true; - CLI::App app{ "Version: " + version() + "\n" }; - set_umamba_command(&app, config); + CLI::App app{ "Version: " + version() + "\n" }; + set_umamba_command(&app, config); - char** utf8argv; + char** utf8argv; #ifdef _WIN32 - wchar_t** wargv; - wargv = CommandLineToArgvW(GetCommandLineW(), &argc); + wchar_t** wargv; + wargv = CommandLineToArgvW(GetCommandLineW(), &argc); - std::vector utf8Args; - std::vector utf8CharArgs; - for (int i = 0; i < argc; i++) - { - utf8Args.push_back(util::windows_encoding_to_utf8(wargv[i])); - } - for (int i = 0; i < argc; ++i) - { - utf8CharArgs.push_back(utf8Args[i].data()); - } - utf8argv = utf8CharArgs.data(); + std::vector utf8Args; + std::vector utf8CharArgs; + for (int i = 0; i < argc; i++) + { + utf8Args.push_back(util::windows_encoding_to_utf8(wargv[i])); + } + for (int i = 0; i < argc; ++i) + { + utf8CharArgs.push_back(utf8Args[i].data()); + } + utf8argv = utf8CharArgs.data(); #else - utf8argv = argv; + utf8argv = argv; #endif - if (argc >= 2 && strcmp(argv[1], "completer") == 0) - { - get_completions(&app, config, argc, utf8argv); - return 0; - } + if (argc >= 2 && strcmp(argv[1], "completer") == 0) + { + get_completions(&app, config, argc, utf8argv); + return 0; + } - std::stringstream full_command; - for (int i = 0; i < argc; ++i) - { - full_command << utf8argv[i]; - if (i < argc - 1) + std::stringstream full_command; + for (int i = 0; i < argc; ++i) { - full_command << " "; + full_command << utf8argv[i]; + if (i < argc - 1) + { + full_command << " "; + } } - } - ctx.command_params.current_command = full_command.str(); + ctx.command_params.current_command = full_command.str(); - std::optional error_to_report; - auto handle_exception = [&](auto& e, const auto&... additional_messages) - { - using namespace std::literals; - error_to_report.emplace(e.what()); - (error_to_report->append(additional_messages), ...); - set_sig_interrupted(); - }; + std::optional error_to_report; + auto handle_exception = [&](auto& e, const auto&... additional_messages) + { + using namespace std::literals; + error_to_report.emplace(e.what()); + (error_to_report->append(additional_messages), ...); + set_sig_interrupted(); + }; - int return_value = EXIT_SUCCESS; + int return_value = EXIT_SUCCESS; - try - { - // Note: do not use CLI11_PARSE macro as its error handling - // would bypass ours. - app.parse(argc, utf8argv); - if (app.get_subcommands().size() == 0) + try { - config.load(); - Console::instance().print(app.help()); + // Note: do not use CLI11_PARSE macro as its error handling + // would bypass ours. + app.parse(argc, utf8argv); + if (app.get_subcommands().size() == 0) + { + config.load(); + Console::instance().print(app.help()); + } + if (app.got_subcommand("config") + && app.get_subcommand("config")->get_subcommands().size() == 0) + { + config.load(); + Console::instance().print(app.get_subcommand("config")->help()); + } } - if (app.got_subcommand("config") - && app.get_subcommand("config")->get_subcommands().size() == 0) + catch (const mamba::mamba_error& e) { - config.load(); - Console::instance().print(app.get_subcommand("config")->help()); - } - } - catch (const mamba::mamba_error& e) - { - // We treat interruptions (ctrl-c) specially by not logging a critical error. - const bool is_interruption = [&] - { - if (e.error_code() == mamba::mamba_error_code::aggregated) + // We treat interruptions (ctrl-c) specially by not logging a critical error. + const bool is_interruption = [&] { - // Prefer dynamic_cast: a plain mamba_error can incorrectly carry - // error_code::aggregated after object slicing (mamba-org/mamba#4352). - if (const auto* aggregated_error = dynamic_cast( - &e - )) + if (e.error_code() == mamba::mamba_error_code::aggregated) + { + // Prefer dynamic_cast: a plain mamba_error can incorrectly carry + // error_code::aggregated after object slicing (mamba-org/mamba#4352). + if (const auto* aggregated_error = dynamic_cast( + &e + )) + { + return aggregated_error->has_only_error( + mamba::mamba_error_code::user_interrupted + ); + } + return false; + } + else { - return aggregated_error->has_only_error(mamba::mamba_error_code::user_interrupted); + return e.error_code() == mamba::mamba_error_code::user_interrupted; } - return false; + }(); + + if (is_interruption) + { + LOG_WARNING << e.what(); + return 0; } else { - return e.error_code() == mamba::mamba_error_code::user_interrupted; + handle_exception(e); } - }(); - - if (is_interruption) - { - LOG_WARNING << e.what(); - return 0; } - else + catch (const CLI::Error& e) { - handle_exception(e); + using namespace std::literals; + // We only preserve CLI11 output behavior when errors from CLI11 + // occurs because of `--help` or `--version` is used. Otherwise we follow the + // logic that `--json` outputs everything as JSON. + static constexpr std::array non_error_request_names = { "CallForHelp"sv, + "CallForAllHelp"sv, + "CallForVersion"sv }; + const bool is_non_error_request = std::ranges::find(non_error_request_names, e.get_name()) + != non_error_request_names.end(); + + if (ctx.output_params.json and not is_non_error_request) + { + // we want the output to end up in the json log history + std::stringstream output; + return_value = app.exit(e, output, output); + LOG_WARNING << output.str(); + } + else + { + // we don't want any json output even if requested, CLI11 will handle this + console.cancel_json_print(); + return_value = app.exit(e); + } } - } - catch (const CLI::Error& e) - { - using namespace std::literals; - // We only preserve CLI11 output behavior when errors from CLI11 - // occurs because of `--help` or `--version` is used. Otherwise we follow the - // logic that `--json` outputs everything as JSON. - static constexpr std::array non_error_request_names = { "CallForHelp"sv, - "CallForAllHelp"sv, - "CallForVersion"sv }; - const bool is_non_error_request = std::ranges::find(non_error_request_names, e.get_name()) - != non_error_request_names.end(); - - if (ctx.output_params.json and not is_non_error_request) + catch (const std::exception& e) { - // we want the output to end up in the json log history - std::stringstream output; - return_value = app.exit(e, output, output); - LOG_WARNING << output.str(); + handle_exception(e); } - else + + if (error_to_report) { - // we don't want any json output even if requested, CLI11 will handle this - console.cancel_json_print(); - return_value = app.exit(e); + LOG_CRITICAL << error_to_report.value(); + return_value = EXIT_FAILURE; } + + return return_value; } + + // Handle errors printing - specifically for `--json` and `--quiet` options + // as `Console` is unreachable here catch (const std::exception& e) { - handle_exception(e); + report_error(argc, argv, e.what()); + return EXIT_FAILURE; } - if (error_to_report) + catch (...) { - LOG_CRITICAL << error_to_report.value(); - return_value = EXIT_FAILURE; + report_error(argc, argv, "Unhandled non-standard exception"); + return EXIT_FAILURE; } - - return return_value; } diff --git a/micromamba/tests/test_shell.py b/micromamba/tests/test_shell.py index 399ff95904..c8ab7a5e52 100644 --- a/micromamba/tests/test_shell.py +++ b/micromamba/tests/test_shell.py @@ -3,6 +3,7 @@ import platform import shutil import subprocess +import sys from pathlib import Path, PureWindowsPath import pytest @@ -21,6 +22,42 @@ def skip_if_shell_incompat(shell_type): pytest.skip("Incompatible shell/OS") +@pytest.mark.skipif(sys.platform != "win32", reason="Windows only") +def test_hook_cmd_exe(tmp_home, tmp_root_prefix, tmp_path): + res = helpers.shell("hook", "-s", "cmd.exe") + + assert res == "" + assert (tmp_root_prefix / "condabin" / "mamba_hook.bat").is_file() + assert (tmp_root_prefix / "Scripts" / "activate.bat").is_file() + assert (tmp_root_prefix / "conda-meta").is_dir() + + # Remove any preconfigured settings + data = tmp_path / "data" + env = {k: v for k, v in os.environ.items() if not k.startswith(("MAMBA_", "XDG_", "CONDA_"))} + env["XDG_DATA_HOME"] = str(data) + env["XDG_CONFIG_HOME"] = str(tmp_path / "config") + default_prefix = data / "mamba" + + mamba_exe = helpers.get_umamba() + + res = subprocess.run([mamba_exe], env=env, capture_output=True, text=True) + assert res.returncode == 0 + + hook = subprocess.run( + [mamba_exe, "shell", "hook", "-s", "cmd.exe"], env=env, capture_output=True, text=True + ) + assert hook.returncode == 0 + + assert (default_prefix / "condabin").is_dir() + assert (default_prefix / "Scripts").is_dir() + assert (default_prefix / "conda-meta").is_dir() + + # Running `mamba_exe` again does not fail silently + res = subprocess.run([mamba_exe], env=env, capture_output=True, text=True) + assert res.returncode == 0 + assert not res.stderr, f"stderr was not empty: {res.stderr}" + + @pytest.mark.parametrize( "shell_type", ["bash", "posix", "powershell", "cmd.exe", "xonsh", "zsh", "fish", "tcsh", "nu"],