Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

fix: PyPI support for env update #3419

Merged
merged 13 commits into from
Sep 5, 2024
2 changes: 2 additions & 0 deletions libmamba/CMakeLists.txt
Original file line number Diff line number Diff line change
Expand Up @@ -257,6 +257,8 @@ set(
${LIBMAMBA_SOURCE_DIR}/api/info.cpp
${LIBMAMBA_SOURCE_DIR}/api/install.cpp
${LIBMAMBA_SOURCE_DIR}/api/list.cpp
${LIBMAMBA_SOURCE_DIR}/api/pip_utils.cpp
jjerphan marked this conversation as resolved.
Show resolved Hide resolved
${LIBMAMBA_SOURCE_DIR}/api/pip_utils.hpp
${LIBMAMBA_SOURCE_DIR}/api/remove.cpp
${LIBMAMBA_SOURCE_DIR}/api/repoquery.cpp
${LIBMAMBA_SOURCE_DIR}/api/shell.cpp
Expand Down
144 changes: 4 additions & 140 deletions libmamba/src/api/install.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -6,13 +6,6 @@

#include <stdexcept>

#include <fmt/color.h>
#include <fmt/format.h>
#include <fmt/ostream.h>
#include <fmt/ranges.h>
#include <reproc++/run.hpp>
#include <reproc/reproc.h>

#include "mamba/api/channel_loader.hpp"
#include "mamba/api/configuration.hpp"
#include "mamba/api/install.hpp"
Expand All @@ -33,139 +26,10 @@
#include "mamba/util/path_manip.hpp"
#include "mamba/util/string.hpp"

#include "pip_utils.hpp"

namespace mamba
{
namespace
{
using command_args = std::vector<std::string>;

tl::expected<command_args, std::runtime_error> get_other_pkg_mgr_install_instructions(
const std::string& name,
const std::string& target_prefix,
const fs::u8path& spec_file
)
{
const auto get_python_path = [&]
{ return util::which_in("python", get_path_dirs(target_prefix)).string(); };

const std::unordered_map<std::string, command_args> other_pkg_mgr_install_instructions{
{ "pip",
{ get_python_path(), "-m", "pip", "install", "-r", spec_file, "--no-input" } },
{ "pip --no-deps",
{ get_python_path(), "-m", "pip", "install", "--no-deps", "-r", spec_file, "--no-input" } }
};

auto found_it = other_pkg_mgr_install_instructions.find(name);
if (found_it != other_pkg_mgr_install_instructions.end())
{
return found_it->second;
}
else
{
return tl::unexpected(std::runtime_error(
fmt::format("no install instruction found for package manager '{}'", name)
));
}
}

}

bool reproc_killed(int status)
{
return status == REPROC_SIGKILL;
}

bool reproc_terminated(int status)
{
return status == REPROC_SIGTERM;
}

void assert_reproc_success(const reproc::options& options, int status, std::error_code ec)
{
bool killed_not_an_err = (options.stop.first.action == reproc::stop::kill)
|| (options.stop.second.action == reproc::stop::kill)
|| (options.stop.third.action == reproc::stop::kill);

bool terminated_not_an_err = (options.stop.first.action == reproc::stop::terminate)
|| (options.stop.second.action == reproc::stop::terminate)
|| (options.stop.third.action == reproc::stop::terminate);

if (ec || (!killed_not_an_err && reproc_killed(status))
|| (!terminated_not_an_err && reproc_terminated(status)))
{
if (ec)
{
LOG_ERROR << "Subprocess call failed: " << ec.message();
}
else if (reproc_killed(status))
{
LOG_ERROR << "Subprocess call failed (killed)";
}
else
{
LOG_ERROR << "Subprocess call failed (terminated)";
}
throw std::runtime_error("Subprocess call failed. Aborting.");
}
}

auto install_for_other_pkgmgr(const Context& ctx, const detail::other_pkg_mgr_spec& other_spec)
{
const auto& pkg_mgr = other_spec.pkg_mgr;
const auto& deps = other_spec.deps;
const auto& cwd = other_spec.cwd;

TemporaryFile specs("mambaf", "", cwd);
{
std::ofstream specs_f = open_ofstream(specs.path());
for (auto& d : deps)
{
specs_f << d.c_str() << '\n';
}
}

command_args install_instructions = [&]
{
const auto maybe_instructions = get_other_pkg_mgr_install_instructions(
pkg_mgr,
ctx.prefix_params.target_prefix.string(),
specs.path()
);
if (maybe_instructions)
{
return maybe_instructions.value();
}
else
{
throw maybe_instructions.error();
}
}();

auto [wrapped_command, tmpfile] = prepare_wrapped_call(
ctx,
ctx.prefix_params.target_prefix,
install_instructions
);

reproc::options options;
options.redirect.parent = true;
options.working_directory = cwd.c_str();

Console::stream() << fmt::format(
ctx.graphics_params.palette.external,
"\nInstalling {} packages: {}",
pkg_mgr,
fmt::join(deps, ", ")
);
fmt::print(LOG_INFO, "Calling: {}", fmt::join(install_instructions, " "));

auto [status, ec] = reproc::run(wrapped_command, options);
assert_reproc_success(options, status, ec);
if (status != 0)
{
throw std::runtime_error("pip failed to install packages");
}
}

const auto& truthy_values(const std::string platform)
{
Expand Down Expand Up @@ -675,7 +539,7 @@ namespace mamba
for (auto other_spec : config.at("others_pkg_mgrs_specs")
.value<std::vector<detail::other_pkg_mgr_spec>>())
{
install_for_other_pkgmgr(ctx, other_spec);
install_for_other_pkgmgr(ctx, other_spec, pip::Update::No);
}
}
else
Expand Down Expand Up @@ -773,7 +637,7 @@ namespace mamba

for (auto other_spec : others)
{
install_for_other_pkgmgr(ctx, other_spec);
install_for_other_pkgmgr(ctx, other_spec, pip::Update::No);
}
}
else
Expand Down
187 changes: 187 additions & 0 deletions libmamba/src/api/pip_utils.cpp
Original file line number Diff line number Diff line change
@@ -0,0 +1,187 @@
// Copyright (c) 2024, QuantStack and Mamba Contributors
//
// Distributed under the terms of the BSD 3-Clause License.
//
// The full license is in the file LICENSE, distributed with this software.

#include <fmt/color.h>
#include <fmt/format.h>
#include <fmt/ostream.h>
#include <fmt/ranges.h>
#include <reproc++/run.hpp>
#include <reproc/reproc.h>

// TODO includes to be removed after moving some functions/structs around
#include "mamba/api/install.hpp" // other_pkg_mgr_spec
#include "mamba/core/activation.hpp" // get_path_dirs
#include "mamba/core/context.hpp"
#include "mamba/core/util.hpp"
#include "mamba/fs/filesystem.hpp"
#include "mamba/util/environment.hpp"

#include "pip_utils.hpp"

namespace mamba
{
namespace
{
tl::expected<command_args, std::runtime_error> get_pip_install_command(
const std::string& name,
const std::string& target_prefix,
const fs::u8path& spec_file,
pip::Update update
)
{
const auto get_python_path = [&]
{ return util::which_in("python", get_path_dirs(target_prefix)).string(); };

command_args cmd = { get_python_path(), "-m", "pip", "install" };
command_args cmd_extension = { "-r", spec_file, "--no-input", "--quiet" };

if (update == pip::Update::Yes)
{
cmd.push_back("-U");
}

if (name == "pip --no-deps")
{
cmd.push_back("--no-deps");
}

cmd.insert(cmd.end(), cmd_extension.begin(), cmd_extension.end());
const std::unordered_map<std::string, command_args> pip_install_command{
{ "pip", cmd },
{ "pip --no-deps", cmd }
};

auto found_it = pip_install_command.find(name);
if (found_it != pip_install_command.end())
{
return found_it->second;
}
else
{
return tl::unexpected(std::runtime_error(fmt::format(
"no {} instruction found for package manager '{}'",
(update == pip::Update::Yes) ? "update" : "install",
name
)));
}
}
}

bool reproc_killed(int status)
{
return status == REPROC_SIGKILL;
}

bool reproc_terminated(int status)
{
return status == REPROC_SIGTERM;
}

void assert_reproc_success(const reproc::options& options, int status, std::error_code ec)
{
bool killed_not_an_err = (options.stop.first.action == reproc::stop::kill)
|| (options.stop.second.action == reproc::stop::kill)
|| (options.stop.third.action == reproc::stop::kill);

bool terminated_not_an_err = (options.stop.first.action == reproc::stop::terminate)
|| (options.stop.second.action == reproc::stop::terminate)
|| (options.stop.third.action == reproc::stop::terminate);

if (ec || (!killed_not_an_err && reproc_killed(status))
|| (!terminated_not_an_err && reproc_terminated(status)))
{
if (ec)
{
LOG_ERROR << "Subprocess call failed: " << ec.message();
}
else if (reproc_killed(status))
{
LOG_ERROR << "Subprocess call failed (killed)";
}
else
{
LOG_ERROR << "Subprocess call failed (terminated)";
}
throw std::runtime_error("Subprocess call failed. Aborting.");
}
}

tl::expected<command_args, std::runtime_error> install_for_other_pkgmgr(
const Context& ctx,
const detail::other_pkg_mgr_spec& other_spec,
pip::Update update
)
{
const auto& pkg_mgr = other_spec.pkg_mgr;
const auto& deps = other_spec.deps;
const auto& cwd = other_spec.cwd;

LOG_WARNING << fmt::format(
"You are using '{}' as an additional package manager.\nBe aware that packages installed with '{}' are managed independently from 'conda-forge' channel.",
pkg_mgr,
pkg_mgr
);

TemporaryFile specs("mambaf", "", cwd);
{
std::ofstream specs_f = open_ofstream(specs.path());
for (auto& d : deps)
{
specs_f << d.c_str() << '\n';
}
}

command_args command = [&]
{
const auto maybe_command = get_pip_install_command(
pkg_mgr,
ctx.prefix_params.target_prefix.string(),
specs.path(),
update
);
if (maybe_command)
{
return maybe_command.value();
}
else
{
throw maybe_command.error();
}
}();

auto [wrapped_command, tmpfile] = prepare_wrapped_call(
ctx,
ctx.prefix_params.target_prefix,
command
);

reproc::options options;
options.redirect.parent = true;
options.working_directory = cwd.c_str();

Console::stream() << fmt::format(
ctx.graphics_params.palette.external,
"\n{} {} packages: {}",
(update == pip::Update::Yes) ? "Updating" : "Installing",
pkg_mgr,
fmt::join(deps, ", ")
);

fmt::print(LOG_INFO, "Calling: {}", fmt::join(command, " "));

auto [status, ec] = reproc::run(wrapped_command, options);
assert_reproc_success(options, status, ec);
if (status != 0)
{
throw std::runtime_error(fmt::format(
"pip failed to {} packages",
(update == pip::Update::Yes) ? "update" : "install"
));
}

return command;
}
}
Loading
Loading