diff --git a/libmamba/src/api/configuration.cpp b/libmamba/src/api/configuration.cpp index 8eeb552417..a1d33f87f6 100644 --- a/libmamba/src/api/configuration.cpp +++ b/libmamba/src/api/configuration.cpp @@ -1382,6 +1382,11 @@ namespace mamba .set_single_op_lifetime() .description("Packages specification")); + insert(Configurable("clone_env", std::string("")) + .group("Basic") + .set_single_op_lifetime() + .description("Environment name or path to clone from")); + insert(Configurable("others_pkg_mgrs_specs", std::vector({})) .group("Basic") .set_single_op_lifetime() diff --git a/libmamba/src/api/create.cpp b/libmamba/src/api/create.cpp index 704be4d927..dd22f7b690 100644 --- a/libmamba/src/api/create.cpp +++ b/libmamba/src/api/create.cpp @@ -11,7 +11,10 @@ #include "mamba/api/install.hpp" #include "mamba/core/channel_context.hpp" #include "mamba/core/context.hpp" +#include "mamba/core/environments_manager.hpp" +#include "mamba/core/prefix_data.hpp" #include "mamba/core/util.hpp" +#include "mamba/util/path_manip.hpp" namespace mamba { @@ -30,6 +33,7 @@ namespace mamba config.load(); auto& create_specs = config.at("specs").value>(); + auto& clone_env_name = config.at("clone_env").value(); auto& use_explicit = config.at("explicit_install").value(); auto& json_format = config.at("json").get_cli_config(); auto& env_vars = config.at("spec_file_env_vars").value>(); @@ -37,6 +41,90 @@ namespace mamba auto channel_context = ChannelContext::make_conda_compatible(ctx); + // Handle clone environment logic + std::vector clone_specs; + if (!clone_env_name.empty()) + { + // Validate that no other specs are provided when cloning + if (!create_specs.empty()) + { + const auto message = "Cannot specify packages when cloning an environment. Use --clone only."; + LOG_ERROR << message; + throw mamba_error(message, mamba_error_code::incorrect_usage); + } + + auto& file_specs = config.at("file_specs").value>(); + if (!file_specs.empty()) + { + const auto message = "Cannot specify environment file when cloning an environment. Use --clone only."; + LOG_ERROR << message; + throw mamba_error(message, mamba_error_code::incorrect_usage); + } + + // Determine source environment path + fs::u8path source_prefix; + if (fs::exists(clone_env_name)) + { + // Direct path provided + source_prefix = clone_env_name; + } + else + { + // Environment name provided, look for it + if (clone_env_name == "base") + { + source_prefix = ctx.prefix_params.root_prefix; + } + else + { + // Search in envs directories + bool found = false; + for (const auto& envs_dir : ctx.envs_dirs) + { + auto potential_path = envs_dir / clone_env_name; + if (fs::exists(potential_path) && is_conda_environment(potential_path)) + { + source_prefix = potential_path; + found = true; + break; + } + } + if (!found) + { + const auto message = "Environment '" + clone_env_name + "' not found."; + LOG_ERROR << message; + throw mamba_error(message, mamba_error_code::incorrect_usage); + } + } + } + + // Validate source environment exists and is a conda environment + if (!fs::exists(source_prefix) || !is_conda_environment(source_prefix)) + { + const auto message = "Source environment '" + source_prefix.string() + "' is not a valid conda environment."; + LOG_ERROR << message; + throw mamba_error(message, mamba_error_code::incorrect_usage); + } + + // Get package list from source environment + auto maybe_source_prefix_data = PrefixData::create(source_prefix, channel_context); + if (!maybe_source_prefix_data) + { + const auto message = "Could not load prefix data from source environment: " + maybe_source_prefix_data.error().what(); + LOG_ERROR << message; + throw mamba_error(message, mamba_error_code::incorrect_usage); + } + auto records = maybe_source_prefix_data.value().sorted_records(); + + // Convert records to specs for installation + for (const auto& record : records) + { + clone_specs.push_back(record.name + "=" + record.version + "=" + record.build_string); + } + + LOG_INFO << "Cloning environment '" << source_prefix.string() << "' with " << clone_specs.size() << " packages."; + } + bool remove_prefix_on_failure = false; bool create_env = true; @@ -82,7 +170,7 @@ namespace mamba throw mamba_error(message, mamba_error_code::incorrect_usage); } } - if (create_specs.empty()) + if (create_specs.empty() && clone_specs.empty()) { detail::create_empty_target(ctx, ctx.prefix_params.target_prefix, env_vars, no_env); } @@ -98,7 +186,7 @@ namespace mamba } else { - if (create_specs.empty() && json_format) + if (create_specs.empty() && clone_specs.empty() && json_format) { // Just print the JSON nlohmann::json output; @@ -124,14 +212,17 @@ namespace mamba remove_prefix_on_failure ); } - else if (!create_specs.empty()) + else if (!create_specs.empty() || !clone_specs.empty()) { + // Use clone_specs if available, otherwise use create_specs + const auto& specs_to_install = !clone_specs.empty() ? clone_specs : create_specs; + if (use_explicit) { install_explicit_specs( ctx, channel_context, - create_specs, + specs_to_install, create_env, remove_prefix_on_failure ); @@ -142,7 +233,7 @@ namespace mamba ctx, channel_context, config, - create_specs, + specs_to_install, create_env, remove_prefix_on_failure ); diff --git a/micromamba/src/common_options.cpp b/micromamba/src/common_options.cpp index 7f74dc9500..c63df47cbe 100644 --- a/micromamba/src/common_options.cpp +++ b/micromamba/src/common_options.cpp @@ -366,6 +366,11 @@ init_install_options(CLI::App* subcom, Configuration& config) ->type_size(1) ->allow_extra_args(false); + auto& clone_env = config.at("clone_env"); + subcom + ->add_option("--clone", clone_env.get_cli_config(), clone_env.description()) + ->option_text("ENV_NAME_OR_PATH"); + auto& no_pin = config.at("no_pin"); subcom->add_flag("--no-pin,!--pin", no_pin.get_cli_config(), no_pin.description());