diff --git a/Taskfile.dist.yml b/Taskfile.dist.yml index 6904afddf5..f930c978f2 100644 --- a/Taskfile.dist.yml +++ b/Taskfile.dist.yml @@ -55,7 +55,7 @@ tasks: Mamba. Many tasks are automatically run inside this environment. The environment is located at "{{.DEV_ENV_DIR}}" and can also be activated with micromamba to benefit from the executables and LSP tools. - cmds: [{task: '_create-env', vars: {prefix: '{{.DEV_ENV_DIR}}'}}] + cmds: [{task: '_create-env', vars: {prefix: '{{.PWD}}/{{.DEV_ENV_DIR}}'}}] create-test-env: desc: Create a local test environment with as a copy of the dev environment. diff --git a/libmamba/include/mamba/core/channel.hpp b/libmamba/include/mamba/core/channel.hpp index 85aa09bf99..c92b538f31 100644 --- a/libmamba/include/mamba/core/channel.hpp +++ b/libmamba/include/mamba/core/channel.hpp @@ -7,9 +7,9 @@ #ifndef MAMBA_CORE_CHANNEL_HPP #define MAMBA_CORE_CHANNEL_HPP -#include #include #include +#include #include #include "mamba/specs/conda_url.hpp" @@ -26,27 +26,43 @@ namespace mamba namespace specs { class ChannelSpec; + class AuthenticationDataBase; } - std::vector get_known_platforms(); - // Note: Channels can only be created using ChannelContext. class Channel { public: - Channel(const Channel&) = delete; - Channel& operator=(const Channel&) = delete; - Channel(Channel&&) noexcept = default; - Channel& operator=(Channel&&) noexcept = default; + struct ResolveParams + { + using platform_list = util::flat_set; + using channel_list = std::vector; + using channel_map = std::map; + using multichannel_map = std::unordered_map; + + const platform_list& platforms; + const specs::CondaURL& channel_alias; + const channel_map& custom_channels; + const specs::AuthenticationDataBase& auth_db; + + // TODO add CWD and home + }; + + using platform_list = util::flat_set; - ~Channel(); + [[nodiscard]] static auto resolve(specs::ChannelSpec spec, ResolveParams params) -> Channel; - const std::string& location() const; - const std::string& name() const; - const std::string& canonical_name() const; - const util::flat_set& platforms() const; - const specs::CondaURL& url() const; + Channel(specs::CondaURL url, std::string display_name, util::flat_set platforms = {}); + + [[nodiscard]] auto url() const -> const specs::CondaURL&; + void set_url(specs::CondaURL url); + + [[nodiscard]] auto platforms() const -> const platform_list&; + void set_platforms(platform_list platforms); + + [[nodiscard]] auto display_name() const -> const std::string&; + void set_display_name(std::string display_name); std::string base_url() const; std::string platform_url(std::string_view platform, bool with_credential = true) const; @@ -57,38 +73,33 @@ namespace mamba private: - Channel( - specs::CondaURL url, - std::string location, - std::string name, - std::string canonical_name, - util::flat_set platforms = {} - ); - specs::CondaURL m_url; - std::string m_location; - std::string m_name; - std::string m_canonical_name; + std::string m_display_name; util::flat_set m_platforms; - - // Note: as long as Channel is not a regular value-type and we want each - // instance only possible to create through ChannelContext, we need - // to have Channel's constructor only available to ChannelContext, - // therefore enabling it's use through this `friend` statement. - // However, all this should be removed as soon as Channel is changed to - // be a regular value-type (regular as in the regular concept). - friend class ChannelContext; }; - using ChannelCache = std::map; + /** Tuple-like equality of all observable members */ + auto operator==(const Channel& a, const Channel& b) -> bool; + auto operator!=(const Channel& a, const Channel& b) -> bool; +} + +template <> +struct std::hash +{ + auto operator()(const mamba::Channel& c) const -> std::size_t; +}; + + +namespace mamba +{ class ChannelContext { public: - using channel_list = std::vector; using channel_map = std::map; - using multichannel_map = std::map>; + using channel_list = std::vector; + using multichannel_map = std::unordered_map; ChannelContext(Context& context); ~ChannelContext(); @@ -99,7 +110,7 @@ namespace mamba ChannelContext& operator=(ChannelContext&&) = delete; const Channel& make_channel(const std::string& value); - std::vector get_channels(const std::vector& channel_names); + auto get_channels(const std::vector& channel_names) -> channel_list; const specs::CondaURL& get_channel_alias() const; const channel_map& get_custom_channels() const; @@ -112,6 +123,8 @@ namespace mamba private: + using ChannelCache = std::map; + Context& m_context; ChannelCache m_channel_cache; specs::CondaURL m_channel_alias; @@ -119,22 +132,6 @@ namespace mamba multichannel_map m_custom_multichannels; void init_custom_channels(); - - Channel make_simple_channel( - const specs::CondaURL& channel_alias, - const std::string& channel_url, - const std::string& channel_name, - const std::string& channel_canonical_name - ); - - Channel from_any_path(specs::ChannelSpec&& spec); - Channel from_package_path(specs::ChannelSpec&& spec); - Channel from_path(specs::ChannelSpec&& spec); - Channel from_any_url(specs::ChannelSpec&& spec); - Channel from_package_url(specs::ChannelSpec&& spec); - Channel from_url(specs::ChannelSpec&& spec); - Channel from_name(specs::ChannelSpec&& spec); - Channel from_value(const std::string& value); }; } // namespace mamba diff --git a/libmamba/include/mamba/specs/channel_spec.hpp b/libmamba/include/mamba/specs/channel_spec.hpp index a9ab747d03..f73ed5913c 100644 --- a/libmamba/include/mamba/specs/channel_spec.hpp +++ b/libmamba/include/mamba/specs/channel_spec.hpp @@ -7,6 +7,7 @@ #ifndef MAMBA_SPECS_CHANNEL_SPEC_HPP #define MAMBA_SPECS_CHANNEL_SPEC_HPP +#include #include #include @@ -63,10 +64,22 @@ namespace mamba::specs * Example "conda-forge", "locals", "my-channel/my-label". */ Name, + /** + * An unknown channel source. + * + * It is currently unclear why it is needed. + */ + Unknown, }; - static constexpr std::string_view default_name = "defaults"; static constexpr std::string_view platform_separators = "|,;"; + static constexpr std::string_view unknown_channel = ""; + static constexpr std::array invalid_channels_lower = { + "", + "none:///", + "none", + ":///", + }; using dynamic_platform_set = util::flat_set; @@ -87,9 +100,9 @@ namespace mamba::specs private: - std::string m_location = std::string(default_name); + std::string m_location = std::string(unknown_channel); dynamic_platform_set m_platform_filters = {}; - Type m_type = {}; + Type m_type = Type::Unknown; }; } #endif diff --git a/libmamba/include/mamba/util/string.hpp b/libmamba/include/mamba/util/string.hpp index 7ebf41950b..1abefa71dc 100644 --- a/libmamba/include/mamba/util/string.hpp +++ b/libmamba/include/mamba/util/string.hpp @@ -12,9 +12,11 @@ #include #include #include +#include #include #include #include +#include #include #include #include @@ -192,6 +194,16 @@ namespace mamba::util template std::array strip_if_parts(std::wstring_view input, UnaryFunc should_strip); + [[nodiscard]] auto split_once(std::string_view str, char sep) + -> std::tuple>; + [[nodiscard]] auto split_once(std::string_view str, std::string_view sep) + -> std::tuple>; + + [[nodiscard]] auto rsplit_once(std::string_view str, char sep) + -> std::tuple, std::string_view>; + [[nodiscard]] auto rsplit_once(std::string_view str, std::string_view sep) + -> std::tuple, std::string_view>; + std::vector split(std::string_view input, std::string_view sep, std::size_t max_split = SIZE_MAX); std::vector @@ -202,6 +214,18 @@ namespace mamba::util std::vector rsplit(std::wstring_view input, std::wstring_view sep, std::size_t max_split = SIZE_MAX); + /** + * Concatenate string while removing the suffix of the first that may be prefix of second. + * + * Comparison are done as if comparing elements in a split given by @p sep. + * For instance "private/channel" and "channel/label/foo" with separator "/" + * would return "private/channel/label/foo", but "private/chan" and "channel/label/foo" + * would return the "private/chan/channel/label/foo". + */ + std::string concat_dedup_splits(std::string_view str1, std::string_view str2, char sep); + std::string + concat_dedup_splits(std::string_view str1, std::string_view str2, std::string_view sep); + void replace_all(std::string& data, std::string_view search, std::string_view replace); void replace_all(std::wstring& data, std::wstring_view search, std::wstring_view replace); @@ -643,14 +667,6 @@ namespace mamba::util return hex_string(buffer, buffer.size()); } - /** - * Return the common parts of two strings by blocks located between the given sep, - * and considering that these common parts would be located at the end of str1 (search from - * left to right). - * str1 is considered smaller than (or equal to) str2. - * cf. Channels use case. - */ - std::string get_common_parts(std::string_view str1, std::string_view str2, std::string_view sep); } #endif diff --git a/libmamba/include/mamba/util/tuple_hash.hpp b/libmamba/include/mamba/util/tuple_hash.hpp index bde581004a..872d9f23d1 100644 --- a/libmamba/include/mamba/util/tuple_hash.hpp +++ b/libmamba/include/mamba/util/tuple_hash.hpp @@ -9,26 +9,41 @@ #include #include +#include namespace mamba::util { - constexpr void hash_combine(std::size_t& seed, std::size_t other) + constexpr auto hash_combine(std::size_t seed, std::size_t other) -> std::size_t { const auto boost_magic_num = 0x9e3779b9; seed ^= other + boost_magic_num + (seed << 6) + (seed >> 2); + return seed; } template > - constexpr void hash_combine_val(std::size_t& seed, const T& val, const Hasher& hasher = {}) + constexpr auto hash_combine_val(std::size_t seed, const T& val, const Hasher& hasher = {}) + -> std::size_t { - hash_combine(seed, hasher(val)); + return hash_combine(seed, hasher(val)); + } + + template ())>>> + auto hash_combine_val_range(std::size_t seed, Iter first, Iter last, const Hasher& hasher = {}) + -> std::size_t + { + for (; first != last; ++first) + { + seed = hash_combine_val(seed, hasher(*first)); + } + return seed; } template constexpr auto hash_vals(const T&... vals) -> std::size_t { std::size_t seed = 0; - (hash_combine_val(seed, vals), ...); + auto combine = [&seed](const auto& val) { seed = hash_combine_val(seed, val); }; + (combine(vals), ...); return seed; } @@ -46,5 +61,11 @@ namespace mamba::util return hash_tuple(t); } }; + + template + constexpr auto hash_range(const Range& rng) -> std::size_t + { + return hash_combine_val_range(0, rng.begin(), rng.end()); + } } #endif diff --git a/libmamba/src/api/channel_loader.cpp b/libmamba/src/api/channel_loader.cpp index 2027ed6993..a2ec0ad8da 100644 --- a/libmamba/src/api/channel_loader.cpp +++ b/libmamba/src/api/channel_loader.cpp @@ -57,7 +57,7 @@ namespace mamba std::vector> priorities; int max_prio = static_cast(channel_urls.size()); - std::string prev_channel_name; + auto prev_channel_url = specs::CondaURL(); Console::instance().init_progress_bar_manager(ProgressBarMode::multi); @@ -65,11 +65,11 @@ namespace mamba for (auto channel : pool.channel_context().get_channels(channel_urls)) { - for (auto& [platform, url] : channel->platform_urls(true)) + for (auto& [platform, url] : channel.platform_urls(true)) { auto sdires = MSubdirData::create( pool.channel_context(), - *channel, + channel, platform, url, package_caches, @@ -89,10 +89,10 @@ namespace mamba else { // Consider 'flexible' and 'strict' the same way - if (channel->canonical_name() != prev_channel_name) + if (channel.url() != prev_channel_url) { max_prio--; - prev_channel_name = channel->canonical_name(); + prev_channel_url = channel.url(); } priorities.push_back(std::make_pair(max_prio, 0)); } diff --git a/libmamba/src/api/configuration.cpp b/libmamba/src/api/configuration.cpp index 17d0f18fa3..0485eac820 100644 --- a/libmamba/src/api/configuration.cpp +++ b/libmamba/src/api/configuration.cpp @@ -879,6 +879,43 @@ namespace mamba return paths; } + void custom_channels_hook(std::map& custom_channels) + { + // Hard coded Anaconda channels names. + // This will not redefine them if the user has already defined these keys. + custom_channels.emplace("pkgs/main", "https://repo.anaconda.com/pkgs/main"); + custom_channels.emplace("pkgs/r", "https://repo.anaconda.com/pkgs/r"); + custom_channels.emplace("pkgs/pro", "https://repo.anaconda.com/pkgs/pro"); + if (util::on_win) + { + custom_channels.emplace("pkgs/msys2", "https://repo.anaconda.com/pkgs/msys2"); + } + } + + void custom_multichannels_hook( + const Context& context, + std::map>& custom_multichannels + ) + { + custom_multichannels.emplace("defaults", context.default_channels); + + auto local_channels = std::vector(); + local_channels.reserve(3); + for (auto p : { + context.prefix_params.target_prefix / "conda-bld", + context.prefix_params.root_prefix / "conda-bld", + env::home_directory() / "conda-bld", + }) + { + if (fs::exists(p)) + { + local_channels.push_back(std::move(p)); + } + } + + custom_multichannels.emplace("local", std::move(local_channels)); + } + void pkgs_dirs_hook(std::vector& dirs) { for (auto& d : dirs) @@ -999,7 +1036,14 @@ namespace mamba out << YAML::BeginSeq; for (std::size_t n = 0; n < value.size(); ++n) { - print_node(out, value[n], source[n], show_source); + if (source.IsSequence() && (source.size() == value.size())) + { + print_node(out, value[n], source[n], show_source); + } + else + { + print_node(out, value[n], source, show_source); + } } out << YAML::EndSeq; } @@ -1264,14 +1308,28 @@ namespace mamba .set_rc_configurable() .set_env_var_names() .description("Custom channels") - .long_description("A dictionary with name: url to use for custom channels.")); + .long_description( // + "A dictionary with name: url to use for custom channels.\n" + "If not defined, the Conda special names " + R"("pkgs/main", "pkgs/r", "pkgs/pro", and "pkgs/msys2" (Windows only) )" + "will be added." + ) + .set_post_merge_hook(detail::custom_channels_hook)); insert(Configurable("custom_multichannels", &m_context.custom_multichannels) .group("Channels") .set_rc_configurable() .description("Custom multichannels") - .long_description( - "A dictionary with name: list of names/urls to use for custom multichannels." + .long_description( // + "A dictionary where keys are multi channels names, and values are a list " + "of correspinding names / urls / file paths to use.\n" + R"(If not defined, the Conda special mutli channels "defaults" is added )" + R"(with values from the "default_channels" option, and "local" is added )" + R"(with "~/conda-bld" and target and root prefix "conda-bld" subfolders/)" + ) + .needs({ "default_channels", "target_prefix", "root_prefix" }) + .set_post_merge_hook>>( + [this](auto& val) { detail::custom_multichannels_hook(m_context, val); } )); insert(Configurable("override_channels_enabled", &m_context.override_channels_enabled) diff --git a/libmamba/src/api/info.cpp b/libmamba/src/api/info.cpp index b17297c3c8..ccefd62a20 100644 --- a/libmamba/src/api/info.cpp +++ b/libmamba/src/api/info.cpp @@ -164,7 +164,7 @@ namespace mamba std::vector channel_urls; for (auto channel : channel_context.get_channels(channels)) { - for (auto url : channel->urls(true)) + for (auto url : channel.urls(true)) { channel_urls.push_back(url); } diff --git a/libmamba/src/api/list.cpp b/libmamba/src/api/list.cpp index 9076a863e7..6afa7f04c5 100644 --- a/libmamba/src/api/list.cpp +++ b/libmamba/src/api/list.cpp @@ -77,7 +77,7 @@ namespace mamba obj["base_url"] = channel.base_url(); obj["build_number"] = pkg_info.build_number; obj["build_string"] = pkg_info.build_string; - obj["channel"] = channel.canonical_name(); + obj["channel"] = channel.display_name(); obj["dist_name"] = pkg_info.str(); obj["name"] = pkg_info.name; obj["platform"] = pkg_info.subdir; @@ -112,7 +112,7 @@ namespace mamba else { const Channel& channel = channel_context.make_channel(package.second.url); - formatted_pkgs.channel = channel.canonical_name(); + formatted_pkgs.channel = channel.display_name(); } packages.push_back(formatted_pkgs); } diff --git a/libmamba/src/core/channel.cpp b/libmamba/src/core/channel.cpp index f4bcd45c06..83397b2289 100644 --- a/libmamba/src/core/channel.cpp +++ b/libmamba/src/core/channel.cpp @@ -5,91 +5,62 @@ // The full license is in the file LICENSE, distributed with this software. #include -#include +#include +#include #include #include "mamba/core/channel.hpp" #include "mamba/core/context.hpp" -#include "mamba/core/environment.hpp" -#include "mamba/core/package_cache.hpp" -#include "mamba/core/validate.hpp" #include "mamba/specs/channel_spec.hpp" #include "mamba/specs/conda_url.hpp" #include "mamba/util/path_manip.hpp" #include "mamba/util/string.hpp" +#include "mamba/util/tuple_hash.hpp" #include "mamba/util/url.hpp" #include "mamba/util/url_manip.hpp" namespace mamba { - namespace - { - const std::map DEFAULT_CUSTOM_CHANNELS = { - { "pkgs/pro", "https://repo.anaconda.com" } - }; - const char UNKNOWN_CHANNEL[] = ""; - - const std::set INVALID_CHANNELS = { "", - "None:///", - "None", - "", - ":///" }; - - const char LOCAL_CHANNELS_NAME[] = "local"; - const char DEFAULT_CHANNELS_NAME[] = "defaults"; - } - - std::vector get_known_platforms() - { - auto plats = specs::known_platform_names(); - return { plats.begin(), plats.end() }; - } - /************************** * Channel implementation * **************************/ - Channel::Channel( - specs::CondaURL url, - std::string location, - std::string name, - std::string canonical_name, - util::flat_set platforms - ) + Channel::Channel(specs::CondaURL url, std::string display_name, util::flat_set platforms) : m_url(std::move(url)) - , m_location(std::move(location)) - , m_name(std::move(name)) - , m_canonical_name(std::move(canonical_name)) + , m_display_name(std::move(display_name)) , m_platforms(std::move(platforms)) { } - Channel::~Channel() = default; - - const specs::CondaURL& Channel::url() const + auto Channel::url() const -> const specs::CondaURL& { return m_url; } - const std::string& Channel::location() const + void Channel::set_url(specs::CondaURL url) { - return m_location; + m_url = std::move(url); } - const std::string& Channel::name() const + auto Channel::platforms() const -> const platform_list& { - return m_name; + return m_platforms; } - const util::flat_set& Channel::platforms() const + void Channel::set_platforms(platform_list platforms) { - return m_platforms; + m_platforms = std::move(platforms); + } + + auto Channel::display_name() const -> const std::string& + { + return m_display_name; } - const std::string& Channel::canonical_name() const + void Channel::set_display_name(std::string display_name) { - return m_canonical_name; + m_display_name = std::move(display_name); } std::string Channel::base_url() const @@ -143,78 +114,41 @@ namespace mamba return (url() / platform).str(cred); } - /********************************* - * ChannelContext implementation * - *********************************/ - - Channel ChannelContext::make_simple_channel( - const specs::CondaURL& channel_alias, - const std::string& channel_url, - const std::string& channel_name, - const std::string& channel_canonical_name - ) + namespace { - if (!util::url_has_scheme(channel_url)) + auto attrs(const Channel& chan) { - const auto& alias = get_channel_alias(); - auto url = alias; - auto name = std::string(util::strip(channel_name.empty() ? channel_url : channel_name, '/') - ); - url.append_path(channel_url); - return Channel( - /* url= */ std::move(url), - /* location= */ alias.pretty_str(specs::CondaURL::StripScheme::yes, '/', specs::CondaURL::Credentials::Remove), - /* name= */ std::move(name), - /* canonical_name= */ channel_canonical_name - ); + return std::tie(chan.url(), chan.platforms(), chan.display_name()); } + } - auto url = specs::CondaURL::parse(channel_url); - std::string location = url.pretty_str( - specs::CondaURL::StripScheme::yes, - '/', - specs::CondaURL::Credentials::Remove - ); - std::string name(channel_name); + /** Tuple-like equality of all observable members */ + auto operator==(const Channel& a, const Channel& b) -> bool + { + return attrs(a) == attrs(b); + } - if (channel_name.empty()) - { - auto ca_location = channel_alias.pretty_str( - specs::CondaURL::StripScheme::yes, - '/', - specs::CondaURL::Credentials::Remove - ); + auto operator!=(const Channel& a, const Channel& b) -> bool + { + return !(a == b); + } +} - if (util::starts_with(location, ca_location)) - { - name = std::string(util::strip(util::remove_prefix(location, ca_location), '/')); - location = std::move(ca_location); - } - else if (url.scheme() == "file") - { - const auto pos = location.rfind('/'); - name = location.substr(pos + 1); - location = location.substr(0, pos); - } - else - { - location = url.authority(specs::CondaURL::Credentials::Remove); - name = url.path_without_token(); - } - } - else - { - url.append_path(channel_name); - } +auto +std::hash::operator()(const mamba::Channel& chan) const -> std::size_t +{ + return mamba::util::hash_combine( + mamba::util::hash_vals(chan.url(), chan.display_name()), + mamba::util::hash_range(chan.platforms()) + ); +} - name = util::strip(name.empty() ? channel_url : name, '/'); - return Channel( - /* url = */ std::move(url), - /* location= */ std::move(location), - /* name= */ std::move(name), - /* canonical_name= */ channel_canonical_name - ); - } +namespace mamba +{ + + /********************************* + * ChannelContext implementation * + *********************************/ namespace { @@ -281,264 +215,158 @@ namespace mamba } } - auto rsplit_once(std::string_view str, char sep) + auto + make_platforms(util::flat_set filters, const util::flat_set& defaults) { - auto [head, tail] = util::rstrip_if_parts(str, [sep](char c) { return c != sep; }); - if (head.empty()) - { - return std::array{ head, tail }; - } - return std::array{ head.substr(0, head.size() - 1), tail }; + return filters.empty() ? defaults : filters; } - auto - make_platforms(util::flat_set filters, const std::vector& defaults) + auto resolve_path_name(const specs::CondaURL& uri, Channel::ResolveParams params) + -> std::string { - if (filters.empty()) + for (const auto& [display_name, chan] : params.custom_channels) { - for (const auto& plat : defaults) + if (url_match(chan.url(), uri)) { - filters.insert(plat); + return std::string(display_name); } } - return filters; - }; - } - - Channel ChannelContext::from_any_path(specs::ChannelSpec&& spec) - { - auto uri = specs::CondaURL::parse(util::path_or_url_to_url(spec.location())); - auto path = uri.pretty_path(); - auto [parent, current] = rsplit_once(path, '/'); - for (const auto& [canonical_name, chan] : get_custom_channels()) - { - if (url_match(chan.url(), uri)) + if (const auto& ca = params.channel_alias; url_match(ca, uri)) { - return Channel( - /* url= */ std::move(uri), - /* location= */ chan.url().pretty_str(specs::CondaURL::StripScheme::yes), - /* name= */ std::string(util::rstrip(parent, '/')), - /* canonical_name= */ std::string(canonical_name) - ); + return std::string(util::strip(util::remove_prefix(uri.path(), ca.path()), '/')); } + + return uri.pretty_str(); } - if (const auto& ca = get_channel_alias(); url_match(ca, uri)) + auto resolve_path(specs::ChannelSpec&& spec, Channel::ResolveParams params) -> Channel { - auto name = util::strip(util::remove_prefix(uri.path(), ca.path()), '/'); - return Channel( - /* url= */ std::move(uri), - /* location= */ ca.pretty_str(specs::CondaURL::StripScheme::yes), - /* name= */ std::string(name), - /* canonical_name= */ std::string(name) - ); - } + auto uri = specs::CondaURL::parse(util::path_or_url_to_url(spec.location())); + auto display_name = resolve_path_name(uri, params); + auto platforms = Channel::ResolveParams::platform_list{}; + if (spec.type() == specs::ChannelSpec::Type::Path) + { + platforms = make_platforms(spec.clear_platform_filters(), params.platforms); + } - auto canonical_name = uri.pretty_str(); - return Channel( - /* url= */ std::move(uri), - /* location= */ std::string(util::rstrip(parent, '/')), - /* name= */ std::string(util::rstrip(current, '/')), - /* canonical_name= */ std::move(canonical_name) - ); - } + return Channel(std::move(uri), std::move(display_name), std::move(platforms)); + } - Channel ChannelContext::from_package_path(specs::ChannelSpec&& spec) - { - assert(spec.type() == specs::ChannelSpec::Type::PackagePath); - return from_any_path(std::move(spec)); - } + auto resolve_url_name(const specs::CondaURL& url, Channel::ResolveParams params) -> std::string + { + using StripScheme = typename specs::CondaURL::StripScheme; + using Credentials = typename specs::CondaURL::Credentials; - Channel ChannelContext::from_path(specs::ChannelSpec&& spec) - { - assert(spec.type() == specs::ChannelSpec::Type::Path); - auto platforms = make_platforms(spec.clear_platform_filters(), m_context.platforms()); - auto chan = from_any_path(std::move(spec)); - chan.m_platforms = std::move(platforms); - return chan; - } + std::string url_str = url.pretty_str(StripScheme::yes, '/', Credentials::Remove); - Channel ChannelContext::from_any_url(specs::ChannelSpec&& spec) - { - assert(util::url_has_scheme(spec.location())); - auto url = specs::CondaURL::parse(spec.location()); - assert(url.scheme() != "file"); + for (const auto& [display_name, chan] : params.custom_channels) + { + if (url_match(chan.url(), url)) + { + return std::string(display_name); + } + } - using StripScheme = typename specs::CondaURL::StripScheme; - using Credentials = typename specs::CondaURL::Credentials; + if (const auto& ca = params.channel_alias; url_match(ca, url)) + { + auto ca_str = ca.pretty_str(StripScheme::yes, '/', Credentials::Remove); + return std::string(util::strip(util::remove_prefix(url_str, ca_str), '/')); + } - std::string default_location = url.pretty_str(StripScheme::yes, '/', Credentials::Remove); + return url.pretty_str(StripScheme::no, '/', Credentials::Remove); + } - for (const auto& [canonical_name, chan] : get_custom_channels()) + auto resolve_url(specs::ChannelSpec&& spec, Channel::ResolveParams params) -> Channel { - if (url_match(chan.url(), url)) + assert(util::url_has_scheme(spec.location())); + assert(util::url_get_scheme(spec.location()) != "file"); + + auto url = specs::CondaURL::parse(spec.location()); + auto display_name = resolve_url_name(url, params); + set_fallback_credential_from_db(url, params.auth_db); + auto platforms = Channel::ResolveParams::platform_list{}; + if (spec.type() == specs::ChannelSpec::Type::URL) { - std::string location = chan.location(); - // TODO cannot change all the locations at once since they are used in from_name - // std::string location = chan.url().pretty_str(StripScheme::yes, '/', - // Credentials::Remove); - std::string name = std::string( - util::strip(util::remove_prefix(default_location, location), '/') - ); - return Channel( - /* url= */ std::move(url), - /* location= */ std::move(location), - /* name= */ std::move(name), - /* canonical_name= */ std::string(canonical_name) - ); + platforms = make_platforms(spec.clear_platform_filters(), params.platforms); } - } - if (const auto& ca = get_channel_alias(); url_match(ca, url)) - { - auto location = ca.pretty_str(StripScheme::yes, '/', Credentials::Remove); - // Overridding url scheme since chan_url could have been defaulted - auto name = std::string(util::strip(util::remove_prefix(default_location, location), '/')); - return Channel( - /*..url= */ std::move(url), - /* location= */ std::move(location), - /* name= */ name, - /* canonical_name= */ name - ); + return Channel(std::move(url), std::move(display_name), std::move(platforms)); } - auto name = std::string(util::strip(url.path_without_token(), '/')); - auto location = url.authority(Credentials::Remove); - auto canonical_name = url.pretty_str(StripScheme::no, '/', Credentials::Remove); - return Channel( - /* url= */ std::move(url), - /* location= */ std::move(location), - /* name= */ std::move(name), - /* canonical_name= */ std::move(canonical_name) - ); - } - - Channel ChannelContext::from_package_url(specs::ChannelSpec&& spec) - { - auto chan = from_any_url(std ::move(spec)); - set_fallback_credential_from_db(chan.m_url, m_context.authentication_info()); - return chan; - } - - Channel ChannelContext::from_url(specs::ChannelSpec&& spec) - { - auto platforms = make_platforms(spec.clear_platform_filters(), m_context.platforms()); - auto chan = from_any_url(std::move(spec)); - chan.m_platforms = std::move(platforms); - set_fallback_credential_from_db(chan.m_url, m_context.authentication_info()); - return chan; - } - - Channel ChannelContext::from_name(specs::ChannelSpec&& spec) - { - std::string name = spec.clear_location(); - const auto& custom_channels = get_custom_channels(); - const auto it_end = custom_channels.end(); - auto it = custom_channels.find(name); + auto resolve_name(specs::ChannelSpec&& spec, Channel::ResolveParams params) -> Channel { - std::string considered_name = name; - while (it == it_end) + std::string name = spec.clear_location(); + const auto it_end = params.custom_channels.end(); + auto it = it_end; { - if (const auto pos = considered_name.rfind("/"); pos != std::string::npos) - { - considered_name = considered_name.substr(0, pos); - it = custom_channels.find(considered_name); - } - else + auto considered_name = std::optional(name); + while ((it == it_end)) { - break; + if (!considered_name.has_value()) + { + break; + } + it = params.custom_channels.find(std::string(considered_name.value())); + considered_name = std::get<0>(util::rsplit_once(considered_name.value(), '/')); } } - } - if (it != it_end) - { - auto url = it->second.url(); - // we can have a channel like - // testchannel: https://server.com/private/testchannel - // where `name == private/testchannel` and we need to join the remaining label part - // of the channel (e.g. -c testchannel/mylabel/xyz) - // needs to result in `name = private/testchannel/mylabel/xyz` - std::string combined_name = it->second.name(); - if (combined_name != name) + if (it != it_end) { - // Find common string between `name` and `combined_name` - auto common_str = util::get_common_parts(combined_name, name, "/"); - // Combine names properly - if (common_str.empty()) - { - url.append_path(name); - combined_name += "/" + name; - } - else - { - // NOTE We assume that the `common_str`, if not empty, is necessarily at the - // beginning of `name` and at the end of `combined_name` (I don't know about - // other use cases for now) - combined_name += name.substr(common_str.size()); - url.append_path(name.substr(common_str.size())); - } + auto url = it->second.url(); + // we can have a channel like + // testchannel: https://server.com/private/testchannel + // where `name == private/testchannel` and we need to join the remaining label part + // of the channel (e.g. -c testchannel/mylabel/xyz) + // needs to result in `name = private/testchannel/mylabel/xyz` + std::string combined_name = util::concat_dedup_splits( + util::rstrip(url.path(), '/'), + util::lstrip(name, '/'), + '/' + ); + url.set_path(combined_name); + + set_fallback_credential_from_db(url, params.auth_db); + return Channel( + /* url= */ std::move(url), + /* display_name= */ std::move(name), + /* platforms= */ make_platforms(spec.clear_platform_filters(), params.platforms) + ); } - set_fallback_credential_from_db(url, m_context.authentication_info()); + auto url = params.channel_alias; + url.append_path(name); + set_fallback_credential_from_db(url, params.auth_db); return Channel( /* url= */ std::move(url), - /* location= */ it->second.location(), - /* name= */ std::move(combined_name), - /* canonical_name= */ std::move(name), - /* platforms= */ make_platforms(spec.clear_platform_filters(), m_context.platforms()) + /* display_name= */ name, + /* platforms= */ make_platforms(spec.clear_platform_filters(), params.platforms) ); } - - const auto& alias = get_channel_alias(); - auto url = alias; - url.append_path(name); - set_fallback_credential_from_db(url, m_context.authentication_info()); - return Channel( - /* url= */ std::move(url), - /* location= */ alias.pretty_str(specs::CondaURL::StripScheme::yes, '/', specs::CondaURL::Credentials::Remove), - /* name= */ name, - /* canonical_name= */ name, - /* platforms= */ make_platforms(spec.clear_platform_filters(), m_context.platforms()) - ); } - Channel ChannelContext::from_value(const std::string& in_value) + auto Channel::resolve(specs::ChannelSpec spec, ResolveParams params) -> Channel { - if (INVALID_CHANNELS.count(in_value) > 0) - { - return Channel( - /* url= */ specs::CondaURL{}, - /* location= */ "", - /* name= */ UNKNOWN_CHANNEL, - /* canonical_name= */ UNKNOWN_CHANNEL - ); - } - - auto spec = specs::ChannelSpec::parse(in_value); - switch (spec.type()) { case specs::ChannelSpec::Type::PackagePath: - { - return from_package_path(std::move(spec)); - } case specs::ChannelSpec::Type::Path: { - return from_path(std::move(spec)); + return resolve_path(std::move(spec), params); } case specs::ChannelSpec::Type::PackageURL: - { - return from_package_url(std::move(spec)); - } case specs::ChannelSpec::Type::URL: { - return from_url(std::move(spec)); + return resolve_url(std::move(spec), params); } case specs::ChannelSpec::Type::Name: { - return from_name(std::move(spec)); + return resolve_name(std::move(spec), params); + } + case specs::ChannelSpec::Type::Unknown: + { + return Channel(specs::CondaURL{}, spec.clear_location()); } } throw std::invalid_argument("Invalid ChannelSpec::Type"); @@ -551,45 +379,53 @@ namespace mamba return it->second; } - auto [it, inserted] = m_channel_cache.emplace(value, from_value(value)); + auto spec = specs::ChannelSpec::parse(value); + const auto platforms = [](const auto& plats) { + return Channel::ResolveParams::platform_list(plats.cbegin(), plats.cend()); + }(m_context.platforms()); + auto params = Channel::ResolveParams{ + /* .platforms */ platforms, + /* .channel_alias */ m_channel_alias, + /* .custom_channels */ m_custom_channels, + /* .auth_db */ m_context.authentication_info(), + }; + + auto [it, inserted] = m_channel_cache.emplace(value, Channel::resolve(std::move(spec), params)); assert(inserted); return it->second; } - std::vector - ChannelContext::get_channels(const std::vector& channel_names) + auto ChannelContext::get_channels(const std::vector& channel_names) -> channel_list { - std::set added; - std::vector result; + auto added = std::unordered_set(); + auto result = channel_list(); for (auto name : channel_names) { - std::string platform_spec; - auto platform_spec_ind = name.find("["); - if (platform_spec_ind != std::string::npos) - { - platform_spec = name.substr(platform_spec_ind); - name = name.substr(0, platform_spec_ind); - } + auto spec = specs::ChannelSpec::parse(name); - auto add_channel = [&](const std::string& lname) - { - auto* channel = &make_channel(lname + platform_spec); - if (added.insert(channel).second) - { - result.push_back(channel); - } - }; - auto multi_iter = get_custom_multichannels().find(name); - if (multi_iter != get_custom_multichannels().end()) + const auto& multi_chan = get_custom_multichannels(); + if (auto iter = multi_chan.find(spec.location()); iter != multi_chan.end()) { - for (const auto& n : multi_iter->second) + for (const auto& chan : iter->second) { - add_channel(n); + auto channel = chan; + if (!spec.platform_filters().empty()) + { + channel.set_platforms(spec.platform_filters()); + } + if (added.insert(channel).second) + { + result.push_back(std::move(channel)); + } } } else { - add_channel(name); + auto channel = make_channel(name); + if (added.insert(channel).second) + { + result.push_back(std::move(channel)); + } } } return result; @@ -621,89 +457,22 @@ namespace mamba void ChannelContext::init_custom_channels() { - /****************** - * MULTI CHANNELS * - ******************/ - - // Default channels - auto& default_channels = m_context.default_channels; - std::vector default_names(default_channels.size()); - auto default_name_iter = default_names.begin(); - for (auto& url : default_channels) - { - auto channel = make_simple_channel(m_channel_alias, url, "", DEFAULT_CHANNELS_NAME); - std::string name = channel.name(); - auto res = m_custom_channels.emplace(std::move(name), std::move(channel)); - *default_name_iter++ = res.first->first; - } - m_custom_multichannels.emplace(DEFAULT_CHANNELS_NAME, std::move(default_names)); - - // Local channels - std::vector local_channels = { m_context.prefix_params.target_prefix - / "conda-bld", - m_context.prefix_params.root_prefix / "conda-bld", - env::home_directory() / "conda-bld" }; - - bool at_least_one_local_dir = false; - std::vector local_names; - local_names.reserve(local_channels.size()); - for (const auto& p : local_channels) - { - if (fs::is_directory(p)) - { - at_least_one_local_dir = true; - std::string url = util::path_or_url_to_url(p); - auto channel = make_simple_channel(m_channel_alias, url, "", LOCAL_CHANNELS_NAME); - std::string name = channel.name(); - auto res = m_custom_channels.emplace(std::move(name), std::move(channel)); - local_names.push_back(res.first->first); - } - } - - // Throw if `-c local` is given but none of the specified `local_channels` are found - if (!at_least_one_local_dir - && std::find(m_context.channels.begin(), m_context.channels.end(), LOCAL_CHANNELS_NAME) - != m_context.channels.end()) + for (const auto& [name, location] : m_context.custom_channels) { - throw std::runtime_error( - "No 'conda-bld' directory found in target prefix, root prefix or home directories!" - ); - } - - m_custom_multichannels.emplace(LOCAL_CHANNELS_NAME, std::move(local_names)); - - const auto& context_custom_channels = m_context.custom_channels; - for (const auto& [name, location] : context_custom_channels) - { - auto channel = from_value(location); - channel.m_canonical_name = name; + auto channel = make_channel(location); + channel.set_display_name(name); m_custom_channels.emplace(name, std::move(channel)); } for (const auto& [multi_name, location_list] : m_context.custom_multichannels) { - std::vector names = {}; - names.reserve(location_list.size()); + auto channels = channel_list(); + channels.reserve(location_list.size()); for (auto& location : location_list) { - auto channel = from_value(location); - // No cannonical name give to mulit_channels - names.push_back(location); + channels.push_back(make_channel(location)); } - m_custom_multichannels.emplace(multi_name, std::move(names)); - } - - /******************* - * SIMPLE CHANNELS * - *******************/ - - // Default local channel - for (auto& ch : DEFAULT_CUSTOM_CHANNELS) - { - m_custom_channels.emplace( - ch.first, - make_simple_channel(m_channel_alias, ch.second, ch.first, ch.first) - ); + m_custom_multichannels.emplace(multi_name, std::move(channels)); } } } // namespace mamba diff --git a/libmamba/src/core/match_spec.cpp b/libmamba/src/core/match_spec.cpp index f13ff67fbb..d62368c4c6 100644 --- a/libmamba/src/core/match_spec.cpp +++ b/libmamba/src/core/match_spec.cpp @@ -13,6 +13,7 @@ #include "mamba/core/output.hpp" #include "mamba/core/util.hpp" #include "mamba/specs/archive.hpp" +#include "mamba/specs/platform.hpp" #include "mamba/util/string.hpp" #include "mamba/util/url_manip.hpp" @@ -101,7 +102,7 @@ namespace mamba version = dist[1]; build_string = dist[2]; - channel = parsed_channel.canonical_name(); + channel = parsed_channel.display_name(); // TODO how to handle this with multiple platforms? if (const auto& plats = parsed_channel.platforms(); !plats.empty()) { @@ -188,6 +189,12 @@ namespace mamba throw std::runtime_error("Parsing of channel / namespace / subdir failed."); } + auto get_known_platforms = []() -> std::vector + { + auto plats = specs::known_platform_names(); + return { plats.begin(), plats.end() }; + }; + std::string cleaned_url; std::string platform; util::split_platform( diff --git a/libmamba/src/core/pool.cpp b/libmamba/src/core/pool.cpp index c3b485bcf4..84c5f95c7b 100644 --- a/libmamba/src/core/pool.cpp +++ b/libmamba/src/core/pool.cpp @@ -149,7 +149,7 @@ namespace mamba } auto& custom_multichannels = channel_context.context().custom_multichannels; - auto x = custom_multichannels.find(needle.name()); + auto x = custom_multichannels.find(needle.display_name()); if (x != custom_multichannels.end()) { for (auto el : (x->second)) @@ -209,7 +209,7 @@ namespace mamba ) -> solv::DependencyId { // Poor man's ms repr to match waht the user provided - std::string const repr = fmt::format("{}::{}", ms.channel, ms.conda_build_form()); + const std::string repr = fmt::format("{}::{}", ms.channel, ms.conda_build_form()); // Already added, return that id if (const auto maybe_id = pool.find_string(repr)) @@ -218,25 +218,22 @@ namespace mamba } // conda_build_form does **NOT** contain the channel info - solv::DependencyId const match = pool_conda_matchspec( + const solv::DependencyId match = pool_conda_matchspec( pool.raw(), ms.conda_build_form().c_str() ); - auto multi_channels = channel_context.get_custom_multichannels(); - std::vector channels; - auto multi_channels_it = multi_channels.find(ms.channel); - if (multi_channels_it != multi_channels.end()) + const auto& multi_chan = channel_context.get_custom_multichannels(); + auto channels = std::vector(); + if (auto iter = multi_chan.find(ms.channel); iter != multi_chan.end()) { - for (auto& c : multi_channels_it->second) - { - channels.push_back(&channel_context.make_channel(c)); - } + channels.insert(channels.end(), iter->second.cbegin(), iter->second.cend()); } else { - channels.push_back(&channel_context.make_channel(ms.channel)); + channels.push_back(channel_context.make_channel(ms.channel)); } + solv::ObjQueue selected_pkgs = {}; pool.for_each_whatprovides( match, @@ -247,9 +244,9 @@ namespace mamba auto repo = solv::ObjRepoView(*s.raw()->repo); // TODO make_channel should disapear avoiding conflict here auto const url = std::string(repo.url()); - for (auto& c : channels) + for (auto const& c : channels) { - if (channel_match(channel_context, channel_context.make_channel(url), *c)) + if (channel_match(channel_context, channel_context.make_channel(url), c)) { if (subdir_match(url, ms.spec)) { @@ -260,7 +257,7 @@ namespace mamba } ); - solv::StringId const repr_id = pool.add_string(repr); + const solv::StringId repr_id = pool.add_string(repr); // FRAGILE This get deleted when calling ``pool_createwhatprovides`` so care // must be taken to do it before // TODO investigate namespace providers diff --git a/libmamba/src/core/solver.cpp b/libmamba/src/core/solver.cpp index c40ea36033..05a19ff45b 100644 --- a/libmamba/src/core/solver.cpp +++ b/libmamba/src/core/solver.cpp @@ -108,7 +108,7 @@ namespace mamba MatchSpec modified_spec(ms); const Channel& chan = m_pool.channel_context().make_channel(std::string(solvable->channel())); - modified_spec.channel = chan.canonical_name(); + modified_spec.channel = chan.display_name(); modified_spec.version = solvable->version(); modified_spec.build_string = solvable->build_string(); diff --git a/libmamba/src/core/subdirdata.cpp b/libmamba/src/core/subdirdata.cpp index 5aef9e1f79..95ea1d19cb 100644 --- a/libmamba/src/core/subdirdata.cpp +++ b/libmamba/src/core/subdirdata.cpp @@ -547,7 +547,7 @@ namespace mamba , m_expired_cache_path("") , m_writable_pkgs_dir(caches.first_writable_path()) , m_repodata_url(util::concat(url, "/", repodata_fn)) - , m_name(util::join_url(channel.canonical_name(), platform)) + , m_name(util::join_url(channel.display_name(), platform)) , m_json_fn(cache_fn_url(m_repodata_url)) , m_solv_fn(m_json_fn.substr(0, m_json_fn.size() - 4) + "solv") , m_is_noarch(platform == "noarch") diff --git a/libmamba/src/core/transaction.cpp b/libmamba/src/core/transaction.cpp index 6bbc69849f..b8c71e1e37 100644 --- a/libmamba/src/core/transaction.cpp +++ b/libmamba/src/core/transaction.cpp @@ -1232,7 +1232,7 @@ namespace mamba else { const Channel& chan = m_pool.channel_context().make_channel(str); - chan_name = chan.canonical_name(); + chan_name = chan.display_name(); } } else diff --git a/libmamba/src/specs/channel_spec.cpp b/libmamba/src/specs/channel_spec.cpp index c9539dfc25..93f2c0179a 100644 --- a/libmamba/src/specs/channel_spec.cpp +++ b/libmamba/src/specs/channel_spec.cpp @@ -4,7 +4,9 @@ // // The full license is in the file LICENSE, distributed with this software. +#include #include +#include #include #include #include @@ -108,14 +110,24 @@ namespace mamba::specs out = util::rstrip(out, '/'); return out; } + + auto is_unknown_channel(std::string_view str) -> bool + { + auto it = std::find( + ChannelSpec::invalid_channels_lower.cbegin(), + ChannelSpec::invalid_channels_lower.cend(), + util::to_lower(str) + ); + return str.empty() || (it != ChannelSpec::invalid_channels_lower.cend()); + } } auto ChannelSpec::parse(std::string_view str) -> ChannelSpec { str = util::strip(str); - if (str.empty()) + if (is_unknown_channel(str)) { - return {}; + return { std::string(unknown_channel), {}, Type::Unknown }; } auto [location, filters] = split_location_platform(str); @@ -148,9 +160,17 @@ namespace mamba::specs , m_platform_filters(std::move(filters)) , m_type(type) { + if (m_type == Type::Unknown) + { + m_location = unknown_channel; + m_platform_filters = {}; + } if (m_location.empty()) { - m_location = std::string(default_name); + throw std::invalid_argument( // + "Cannot channel with empty location, " + "use unknown type instead." + ); } } diff --git a/libmamba/src/util/string.cpp b/libmamba/src/util/string.cpp index 661de95ee3..e36f9bcd9a 100644 --- a/libmamba/src/util/string.cpp +++ b/libmamba/src/util/string.cpp @@ -4,6 +4,7 @@ // // The full license is in the file LICENSE, distributed with this software. +#include #include #include #include @@ -556,6 +557,65 @@ namespace mamba::util return strip_parts_impl(input, chars); } + /******************************************** + * Implementation of split_once functions * + ********************************************/ + + namespace + { + template + auto split_once_impl(std::basic_string_view str, CharOrStrView sep) + -> std::tuple> + { + static constexpr auto npos = std::basic_string_view::npos; + if (const auto pos = str.find(sep); pos != npos) + { + return { str.substr(0, pos), str.substr(pos + detail::length(sep)) }; + } + return { str, std::nullopt }; + } + } + + auto split_once(std::string_view str, char sep) + -> std::tuple> + { + return split_once_impl(str, sep); + } + + auto split_once(std::string_view str, std::string_view sep) + -> std::tuple> + { + return split_once_impl(str, sep); + } + + namespace + { + template + auto rsplit_once_impl(std::basic_string_view str, CharOrStrView sep) + -> std::tuple, std::string_view> + { + static constexpr auto npos = std::basic_string_view::npos; + if (const auto pos = str.rfind(sep); pos != npos) + { + return { str.substr(0, pos), str.substr(pos + detail::length(sep)) }; + } + return { std::nullopt, str }; + } + } + + auto rsplit_once(std::string_view str, char sep) + -> std::tuple, std::string_view> + { + return rsplit_once_impl(str, sep); + } + + auto rsplit_once(std::string_view str, std::string_view sep) + -> std::tuple, std::string_view> + { + return rsplit_once_impl(str, sep); + } + + /*************************************** * Implementation of split functions * ***************************************/ @@ -675,6 +735,106 @@ namespace mamba::util return rsplit(input, sep, max_split); } + /************************************* + * Implementation of ending_splits * + *************************************/ + + namespace + { + template + auto starts_with_split( + std::basic_string_view str, + std::basic_string_view prefix, + CharOrStrView sep + ) -> bool + { + auto end = prefix.size(); + const auto sep_size = detail::length(sep); + const auto str_size = str.size(); + return + // The substring is found + starts_with(str, prefix) + && ( + // Either it ends at the end + (end == str_size) + // Or it is found before a separator + || ((end <= str_size) && ends_with(str.substr(0, end + sep_size), sep)) + ); + } + + template + auto remove_suffix_splits( + std::basic_string_view str1, + std::basic_string_view str2, + CharOrStrView sep + ) -> std::basic_string_view + { + static constexpr auto npos = std::basic_string_view::npos; + + assert(!str1.empty()); + assert(!str2.empty()); + const auto sep_size = detail::length(sep); + assert(sep_size > 0); + + auto get_common_candidate = [&](auto split) + { return str1.substr((split == npos) ? 0 : split + sep_size); }; + + auto split1 = str1.rfind(sep); + + // In the case we did not find a match, we try a bigger common part + while (!starts_with_split(str2, get_common_candidate(split1), sep)) + { + if ((split1 == npos) || (split1 < sep_size)) + { + // No further possibility to find a match, nothing to remove + return str1; + } + // Add the next split element + split1 = str1.rfind(sep, split1 - sep_size); + } + + return str1.substr(0, (split1 == npos) ? 0 : split1); + } + + template + auto concat_dedup_splits_impl( + std::basic_string_view str1, + std::basic_string_view str2, + CharOrStrView sep + ) -> std::basic_string + { + if (str1.empty()) + { + return std::string(str2); + } + if (str2.empty()) + { + return std::string(str1); + } + if (detail::length(sep) < 1) + { + throw std::invalid_argument("Cannot split on empty separator"); + } + auto str1_no_suffix = remove_suffix_splits(str1, str2, sep); + if (str1_no_suffix.empty()) + { + return concat(str1_no_suffix, str2); + } + return concat(str1_no_suffix, sep, str2); + } + } + + std::string concat_dedup_splits(std::string_view str1, std::string_view str2, char sep) + { + return concat_dedup_splits_impl(str1, str2, sep); + } + + std::string + concat_dedup_splits(std::string_view str1, std::string_view str2, std::string_view sep) + { + return concat_dedup_splits_impl(str1, str2, sep); + } + /***************************************** * Implementation of replace functions * *****************************************/ @@ -735,47 +895,4 @@ namespace mamba::util } } - - /******************************************************** - * Implementation of Channels use case util function * - *******************************************************/ - - std::string get_common_parts(std::string_view str1, std::string_view str2, std::string_view sep) - { - std::string common_str{ str1 }; - while ((str2.find(common_str) == std::string::npos)) - { - if (common_str.find(sep) != std::string::npos) - { - common_str = common_str.substr(common_str.find(sep) + 1); - } - else - { - return ""; - } - } - - // Case of non empty common_str - // Check that subparts of common_str are not substrings of elements between the sep - auto vec1 = split(common_str, sep); - auto vec2 = split(str2, sep); - std::vector res_vec; - for (std::size_t idx = 0; idx < vec1.size(); ++idx) - { - auto it = std::find(vec2.begin(), vec2.end(), vec1.at(idx)); - if (it != vec2.end()) - { - res_vec.emplace_back(vec1.at(idx)); - } - else - { - if (idx != 0) - { - return join(sep, res_vec); - } - } - } - - return join(sep, res_vec); - } } diff --git a/libmamba/tests/src/core/test_channel.cpp b/libmamba/tests/src/core/test_channel.cpp index 775394a93f..df85b4e443 100644 --- a/libmamba/tests/src/core/test_channel.cpp +++ b/libmamba/tests/src/core/test_channel.cpp @@ -38,7 +38,16 @@ namespace mamba { // ChannelContext builds its custom channels with // make_simple_channel - ChannelContext channel_context{ mambatests::context() }; + auto& ctx = mambatests::context(); + + // Hard coded Anaconda channels names set in configuration after refactor + // Should be moved to test_config + // FIXME: this has side effect on all tests + ctx.custom_channels.emplace("pkgs/main", "https://repo.anaconda.com/pkgs/main"); + ctx.custom_channels.emplace("pkgs/r", "https://repo.anaconda.com/pkgs/r"); + ctx.custom_channels.emplace("pkgs/pro", "https://repo.anaconda.com/pkgs/pro"); + + ChannelContext channel_context{ ctx }; const auto& ch = channel_context.get_channel_alias(); CHECK_EQ(ch.str(), "https://conda.anaconda.org/"); @@ -47,17 +56,17 @@ namespace mamba auto it = custom.find("pkgs/main"); REQUIRE_NE(it, custom.end()); CHECK_EQ(it->second.url(), CondaURL::parse("https://repo.anaconda.com/pkgs/main")); - CHECK_EQ(it->second.canonical_name(), "defaults"); + CHECK_EQ(it->second.display_name(), "pkgs/main"); it = custom.find("pkgs/pro"); REQUIRE_NE(it, custom.end()); CHECK_EQ(it->second.url(), CondaURL::parse("https://repo.anaconda.com/pkgs/pro")); - CHECK_EQ(it->second.canonical_name(), "pkgs/pro"); + CHECK_EQ(it->second.display_name(), "pkgs/pro"); it = custom.find("pkgs/r"); REQUIRE_NE(it, custom.end()); CHECK_EQ(it->second.url(), CondaURL::parse("https://repo.anaconda.com/pkgs/r")); - CHECK_EQ(it->second.canonical_name(), "defaults"); + CHECK_EQ(it->second.display_name(), "pkgs/r"); } TEST_CASE("channel_alias") @@ -77,12 +86,12 @@ namespace mamba auto it = custom.find("pkgs/main"); REQUIRE_NE(it, custom.end()); CHECK_EQ(it->second.url(), CondaURL::parse("https://repo.anaconda.com/pkgs/main")); - CHECK_EQ(it->second.canonical_name(), "defaults"); + CHECK_EQ(it->second.display_name(), "pkgs/main"); std::string value = "conda-forge"; const Channel& c = channel_context.make_channel(value); CHECK_EQ(c.url(), CondaURL::parse("https://mydomain.com/channels/conda-forge")); - CHECK_EQ(c.canonical_name(), "conda-forge"); + CHECK_EQ(c.display_name(), "conda-forge"); CHECK_EQ(c.platforms(), PlatformSet({ platform, "noarch" })); ctx.channel_alias = "https://conda.anaconda.org"; @@ -126,7 +135,7 @@ namespace mamba std::string value = "test_channel"; const Channel& c = channel_context.make_channel(value); CHECK_EQ(c.url(), CondaURL::parse("file:///tmp/test_channel")); - CHECK_EQ(c.canonical_name(), "test_channel"); + CHECK_EQ(c.display_name(), "test_channel"); CHECK_EQ(c.platforms(), PlatformSet({ platform, "noarch" })); const UrlSet exp_urls({ std::string("file:///tmp/test_channel/") + platform, @@ -139,7 +148,7 @@ namespace mamba std::string value = "some_channel"; const Channel& c = channel_context.make_channel(value); CHECK_EQ(c.url(), CondaURL::parse("https://conda.mydomain.xyz/some_channel")); - CHECK_EQ(c.canonical_name(), "some_channel"); + CHECK_EQ(c.display_name(), "some_channel"); CHECK_EQ(c.platforms(), PlatformSet({ platform, "noarch" })); const UrlSet exp_urls({ std::string("https://conda.mydomain.xyz/some_channel/") + platform, @@ -173,14 +182,14 @@ namespace mamba auto x = channel_context.get_channels({ "xtest" }); CHECK_EQ(x.size(), 3); - auto* c1 = x[0]; + auto c1 = x[0]; const UrlSet exp_urls({ std::string("https://mydomain.com/conda-forge/") + platform, "https://mydomain.com/conda-forge/noarch", }); - CHECK_EQ(c1->urls(), exp_urls); + CHECK_EQ(c1.urls(), exp_urls); const UrlSet exp_urlsy3({ std::string("https://otherdomain.com/snakepit/") + platform, @@ -188,9 +197,9 @@ namespace mamba }); auto y = channel_context.get_channels({ "ytest" }); - auto* y3 = y[2]; + auto y3 = y[2]; - CHECK_EQ(y3->urls(), exp_urlsy3); + CHECK_EQ(y3.urls(), exp_urlsy3); ctx.channel_alias = "https://conda.anaconda.org"; ctx.custom_multichannels.clear(); @@ -217,30 +226,30 @@ namespace mamba auto x = channel_context.get_channels({ "everything" }); CHECK_EQ(x.size(), 3); - auto* c1 = x[0]; - auto* c2 = x[1]; - auto* c3 = x[2]; + auto c1 = x[0]; + auto c2 = x[1]; + auto c3 = x[2]; const UrlSet exp_urls({ std::string("https://condaforge.org/channels/conda-forge/") + platform, "https://condaforge.org/channels/conda-forge/noarch", }); - CHECK_EQ(c1->urls(), exp_urls); + CHECK_EQ(c1.urls(), exp_urls); const UrlSet exp_urls2({ std::string("https://mydomain.com/bioconda/") + platform, "https://mydomain.com/bioconda/noarch", }); - CHECK_EQ(c2->urls(), exp_urls2); + CHECK_EQ(c2.urls(), exp_urls2); const UrlSet exp_urls3({ std::string("https://mydomain.xyz/xyzchannel/xyz/") + platform, "https://mydomain.xyz/xyzchannel/xyz/noarch", }); - CHECK_EQ(c3->urls(), exp_urls3); + CHECK_EQ(c3.urls(), exp_urls3); ctx.channel_alias = "https://conda.anaconda.org"; ctx.custom_multichannels.clear(); @@ -250,26 +259,31 @@ namespace mamba TEST_CASE("default_channels") { auto& ctx = mambatests::context(); + // Hard coded Anaconda multi channel names set in configuration after refactor + // Should be moved to test_config + // FIXME: this has side effect on all tests + ctx.custom_multichannels["defaults"] = ctx.default_channels; + ChannelContext channel_context{ ctx }; auto x = channel_context.get_channels({ "defaults" }); #if !defined(_WIN32) - const Channel* c1 = x[0]; - const Channel* c2 = x[1]; + const Channel c1 = x[0]; + const Channel c2 = x[1]; - CHECK_EQ(c1->url(), CondaURL::parse("https://repo.anaconda.com/pkgs/main")); + CHECK_EQ(c1.url(), CondaURL::parse("https://repo.anaconda.com/pkgs/main")); const UrlSet exp_urls({ std::string("https://repo.anaconda.com/pkgs/main/") + platform, "https://repo.anaconda.com/pkgs/main/noarch", }); - CHECK_EQ(c1->urls(), exp_urls); + CHECK_EQ(c1.urls(), exp_urls); - CHECK_EQ(c2->url(), CondaURL::parse("https://repo.anaconda.com/pkgs/r")); + CHECK_EQ(c2.url(), CondaURL::parse("https://repo.anaconda.com/pkgs/r")); const UrlSet exp_urls2({ std::string("https://repo.anaconda.com/pkgs/r/") + platform, "https://repo.anaconda.com/pkgs/r/noarch", }); - CHECK_EQ(c2->urls(), exp_urls2); + CHECK_EQ(c2.urls(), exp_urls2); #endif ctx.custom_channels.clear(); @@ -282,25 +296,28 @@ namespace mamba "https://mamba.com/test/channel", "https://mamba.com/stable/channel", }; + // Hard coded Anaconda multi channel names set by configuration after refactor + // FIXME: this has side effect on all tests + ctx.custom_multichannels["defaults"] = ctx.default_channels; ChannelContext channel_context{ ctx }; auto x = channel_context.get_channels({ "defaults" }); - const Channel* c1 = x[0]; - const Channel* c2 = x[1]; + const Channel c1 = x[0]; + const Channel c2 = x[1]; - CHECK_EQ(c1->url(), CondaURL::parse("https://mamba.com/test/channel")); + CHECK_EQ(c1.url(), CondaURL::parse("https://mamba.com/test/channel")); const UrlSet exp_urls({ std::string("https://mamba.com/test/channel/") + platform, "https://mamba.com/test/channel/noarch", }); - CHECK_EQ(c1->urls(), exp_urls); + CHECK_EQ(c1.urls(), exp_urls); - CHECK_EQ(c2->url(), CondaURL::parse("https://mamba.com/stable/channel")); + CHECK_EQ(c2.url(), CondaURL::parse("https://mamba.com/stable/channel")); const UrlSet exp_urls2({ std::string("https://mamba.com/stable/channel/") + platform, "https://mamba.com/stable/channel/noarch", }); - CHECK_EQ(c2->urls(), exp_urls2); + CHECK_EQ(c2.urls(), exp_urls2); ctx.custom_channels.clear(); } @@ -309,30 +326,20 @@ namespace mamba { auto& ctx = mambatests::context(); - // Create conda-bld directory to enable testing - auto conda_bld_dir = env::home_directory() / "conda-bld"; - bool to_be_removed = fs::create_directories(conda_bld_dir); - + // Hard coded Anaconda multi channel names set in configuration after refactor + // Should be moved to test_config + // FIXME: this has side effect on all tests + ctx.custom_multichannels["local"] = std::vector{ + ctx.prefix_params.target_prefix / "conda-bld", + ctx.prefix_params.root_prefix / "conda-bld", + env::home_directory() / "conda-bld", + }; ChannelContext channel_context{ ctx }; - const auto& custom = channel_context.get_custom_channels(); - - CHECK_EQ(custom.size(), 4); - - auto it = custom.find("conda-bld"); - REQUIRE_NE(it, custom.end()); - CHECK_EQ(it->second.url(), CondaURL::parse(util::path_to_url(conda_bld_dir.string()))); - CHECK_EQ(it->second.canonical_name(), "local"); + CHECK_EQ(channel_context.get_custom_multichannels().at("local").size(), 3); auto local_channels = channel_context.get_channels({ "local" }); - CHECK_EQ(local_channels.size(), 1); - - // Cleaning - ctx.custom_channels.clear(); - if (to_be_removed) - { - fs::remove_all(conda_bld_dir); - } + CHECK_EQ(local_channels.size(), 3); } TEST_CASE("custom_channels_with_labels") @@ -348,7 +355,7 @@ namespace mamba std::string value = "test_channel"; const Channel& c = channel_context.make_channel(value); CHECK_EQ(c.url(), CondaURL::parse("https://server.com/private/channels/test_channel")); - CHECK_EQ(c.canonical_name(), "test_channel"); + CHECK_EQ(c.display_name(), "test_channel"); CHECK_EQ(c.platforms(), PlatformSet({ platform, "noarch" })); const UrlSet exp_urls({ std::string("https://server.com/private/channels/test_channel/") + platform, @@ -364,7 +371,7 @@ namespace mamba c.url(), CondaURL::parse("https://server.com/private/channels/test_channel/mylabel/xyz") ); - CHECK_EQ(c.canonical_name(), "test_channel/mylabel/xyz"); + CHECK_EQ(c.display_name(), "test_channel/mylabel/xyz"); CHECK_EQ(c.platforms(), PlatformSet({ platform, "noarch" })); const UrlSet exp_urls({ std::string("https://server.com/private/channels/test_channel/mylabel/xyz/") @@ -382,7 +389,7 @@ namespace mamba c.url(), CondaURL::parse("https://server.com/random/channels/random/test_channel/pkg") ); - CHECK_EQ(c.canonical_name(), "random/test_channel/pkg"); + CHECK_EQ(c.display_name(), "random/test_channel/pkg"); CHECK_EQ(c.platforms(), PlatformSet({ platform, "noarch" })); const UrlSet exp_urls({ std::string("https://server.com/random/channels/random/test_channel/pkg/") @@ -405,7 +412,7 @@ namespace mamba ChannelContext channel_context{ mambatests::context() }; const Channel& c = channel_context.make_channel(value); CHECK_EQ(c.url(), CondaURL::parse("https://repo.mamba.pm/conda-forge")); - CHECK_EQ(c.canonical_name(), "https://repo.mamba.pm/conda-forge"); + CHECK_EQ(c.display_name(), "https://repo.mamba.pm/conda-forge"); CHECK_EQ(c.platforms(), PlatformSet({ platform, "noarch" })); } @@ -415,32 +422,32 @@ namespace mamba ChannelContext channel_context{ mambatests::context() }; const Channel& c = channel_context.make_channel(value); CHECK_EQ(c.url(), CondaURL::parse("https://conda.anaconda.org/conda-forge")); - CHECK_EQ(c.canonical_name(), "conda-forge"); + CHECK_EQ(c.display_name(), "conda-forge"); CHECK_EQ(c.platforms(), PlatformSet({ platform, "noarch" })); std::string value2 = "https://repo.anaconda.com/pkgs/main[" + platform + "]"; const Channel& c2 = channel_context.make_channel(value2); CHECK_EQ(c2.url(), CondaURL::parse("https://repo.anaconda.com/pkgs/main")); - CHECK_EQ(c2.canonical_name(), "https://repo.anaconda.com/pkgs/main"); + CHECK_EQ(c2.display_name(), "https://repo.anaconda.com/pkgs/main"); CHECK_EQ(c2.platforms(), PlatformSet({ platform })); std::string value3 = "https://conda.anaconda.org/conda-forge[" + platform + "]"; const Channel& c3 = channel_context.make_channel(value3); CHECK_EQ(c3.url(), c.url()); - CHECK_EQ(c3.canonical_name(), c.canonical_name()); + CHECK_EQ(c3.display_name(), c.display_name()); CHECK_EQ(c3.platforms(), PlatformSet({ platform })); std::string value4 = "/home/mamba/test/channel_b"; const Channel& c4 = channel_context.make_channel(value4); CHECK_EQ(c4.url(), CondaURL::parse(util::path_to_url(value4))); - CHECK_EQ(c4.canonical_name(), c4.url().pretty_str()); + CHECK_EQ(c4.display_name(), c4.url().pretty_str()); CHECK_EQ(c4.platforms(), PlatformSet({ platform, "noarch" })); std::string path5 = "/home/mamba/test/channel_b"; std::string value5 = util::concat(path5, '[', platform, ']'); const Channel& c5 = channel_context.make_channel(value5); CHECK_EQ(c5.url(), CondaURL::parse(util::path_to_url(path5))); - CHECK_EQ(c5.canonical_name(), c5.url().pretty_str()); + CHECK_EQ(c5.display_name(), c5.url().pretty_str()); CHECK_EQ(c5.platforms(), PlatformSet({ platform })); std::string value6a = "http://localhost:8000/conda-forge[noarch]"; diff --git a/libmamba/tests/src/core/test_satisfiability_error.cpp b/libmamba/tests/src/core/test_satisfiability_error.cpp index 4d3864d05b..602e79c77f 100644 --- a/libmamba/tests/src/core/test_satisfiability_error.cpp +++ b/libmamba/tests/src/core/test_satisfiability_error.cpp @@ -330,12 +330,12 @@ namespace auto load_channels(MPool& pool, MultiPackageCache& cache, std::vector&& channels) { auto sub_dirs = std::vector(); - for (const auto* chan : pool.channel_context().get_channels(channels)) + for (const auto& chan : pool.channel_context().get_channels(channels)) { - for (auto& [platform, url] : chan->platform_urls(true)) + for (auto& [platform, url] : chan.platform_urls(true)) { auto sub_dir = expected_value_or_throw( - MSubdirData::create(pool.channel_context(), *chan, platform, url, cache) + MSubdirData::create(pool.channel_context(), chan, platform, url, cache) ); sub_dirs.push_back(std::move(sub_dir)); } diff --git a/libmamba/tests/src/doctest-printer/optional.hpp b/libmamba/tests/src/doctest-printer/optional.hpp new file mode 100644 index 0000000000..4c70a2f844 --- /dev/null +++ b/libmamba/tests/src/doctest-printer/optional.hpp @@ -0,0 +1,26 @@ +// Copyright (c) 2023, 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 + +#include +#include + +namespace doctest +{ + template + struct StringMaker> + { + static auto convert(const std::optional& value) -> String + { + if (value.has_value()) + { + return { fmt::format("Some({})", value.value()).c_str() }; + } + return "None"; + } + }; +} diff --git a/libmamba/tests/src/specs/test_channel_spec.cpp b/libmamba/tests/src/specs/test_channel_spec.cpp index 144fb23274..3fe6a26e25 100644 --- a/libmamba/tests/src/specs/test_channel_spec.cpp +++ b/libmamba/tests/src/specs/test_channel_spec.cpp @@ -6,22 +6,50 @@ #include -#include "mamba/fs/filesystem.hpp" #include "mamba/specs/channel_spec.hpp" #include "mamba/util/build.hpp" -#include "mamba/util/path_manip.hpp" -#include "mamba/util/string.hpp" using namespace mamba; using namespace mamba::specs; TEST_SUITE("specs::channel_spec") { + TEST_CASE("Constructor") + { + SUBCASE("Default") + { + const auto spec = ChannelSpec(); + CHECK_EQ(spec.type(), ChannelSpec::Type::Unknown); + CHECK_EQ(spec.location(), ""); + CHECK(spec.platform_filters().empty()); + } + + SUBCASE("Unknown") + { + const auto spec = ChannelSpec("hello", { "linux-78" }, ChannelSpec::Type::Unknown); + CHECK_EQ(spec.type(), ChannelSpec::Type::Unknown); + CHECK_EQ(spec.location(), ""); + CHECK(spec.platform_filters().empty()); + } + } + TEST_CASE("Parsing") { using Type = typename ChannelSpec::Type; using PlatformSet = typename util::flat_set; + SUBCASE("Invalid channels") + { + for (std::string_view str : { "", "", ":///", "none" }) + { + CAPTURE(str); + const auto spec = ChannelSpec::parse(str); + CHECK_EQ(spec.type(), Type::Unknown); + CHECK_EQ(spec.location(), ""); + CHECK_EQ(spec.platform_filters(), PlatformSet{}); + } + } + SUBCASE("https://repo.anaconda.com/conda-forge") { const auto spec = ChannelSpec::parse("https://repo.anaconda.com/conda-forge"); diff --git a/libmamba/tests/src/util/test_string.cpp b/libmamba/tests/src/util/test_string.cpp index 59aa18d2b7..d9661d34dd 100644 --- a/libmamba/tests/src/util/test_string.cpp +++ b/libmamba/tests/src/util/test_string.cpp @@ -4,7 +4,6 @@ // // The full license is in the file LICENSE, distributed with this software. -#include #include #include #include @@ -14,7 +13,12 @@ #include "mamba/fs/filesystem.hpp" #include "mamba/util/string.hpp" -namespace mamba::util +#include "doctest-printer/array.hpp" +#include "doctest-printer/optional.hpp" + +using namespace mamba::util; + +namespace { TEST_SUITE("util::string") { @@ -364,6 +368,40 @@ namespace mamba::util } } + TEST_CASE("split_once") + { + using Out = std::tuple>; + + CHECK_EQ(split_once("", '/'), Out{ "", std::nullopt }); + CHECK_EQ(split_once("/", '/'), Out{ "", "" }); + CHECK_EQ(split_once("hello/world", '/'), Out{ "hello", "world" }); + CHECK_EQ(split_once("hello/my/world", '/'), Out{ "hello", "my/world" }); + CHECK_EQ(split_once("/hello/world", '/'), Out{ "", "hello/world" }); + + CHECK_EQ(split_once("", "/"), Out{ "", std::nullopt }); + CHECK_EQ(split_once("hello/world", "/"), Out{ "hello", "world" }); + CHECK_EQ(split_once("hello//world", "//"), Out{ "hello", "world" }); + CHECK_EQ(split_once("hello/my//world", "/"), Out{ "hello", "my//world" }); + CHECK_EQ(split_once("hello/my//world", "//"), Out{ "hello/my", "world" }); + } + + TEST_CASE("rsplit_once") + { + using Out = std::tuple, std::string_view>; + + CHECK_EQ(rsplit_once("", '/'), Out{ std::nullopt, "" }); + CHECK_EQ(rsplit_once("/", '/'), Out{ "", "" }); + CHECK_EQ(rsplit_once("hello/world", '/'), Out{ "hello", "world" }); + CHECK_EQ(rsplit_once("hello/my/world", '/'), Out{ "hello/my", "world" }); + CHECK_EQ(rsplit_once("hello/world/", '/'), Out{ "hello/world", "" }); + + CHECK_EQ(rsplit_once("", "/"), Out{ std::nullopt, "" }); + CHECK_EQ(rsplit_once("hello/world", "/"), Out{ "hello", "world" }); + CHECK_EQ(rsplit_once("hello//world", "//"), Out{ "hello", "world" }); + CHECK_EQ(rsplit_once("hello//my/world", "/"), Out{ "hello//my", "world" }); + CHECK_EQ(rsplit_once("hello//my/world", "//"), Out{ "hello", "my/world" }); + } + TEST_CASE("split") { std::string a = "hello.again.it's.me.mario"; @@ -406,7 +444,7 @@ namespace mamba::util CHECK_EQ(joined, "a-bc-d"); } { - std::vector to_join = { "/a", "bc", "d" }; + std::vector to_join = { "/a", "bc", "d" }; auto joined = join("/", to_join); static_assert(std::is_same::value); CHECK_EQ(joined, "/a/bc/d"); @@ -464,37 +502,71 @@ namespace mamba::util CHECK_EQ(concat("aa", std::string("bb"), std::string_view("cc"), 'd'), "aabbccd"); } - TEST_CASE("get_common_parts") - { - CHECK_EQ(get_common_parts("", "", "/"), ""); - CHECK_EQ(get_common_parts("", "test", "/"), ""); - CHECK_EQ(get_common_parts("test", "test", "/"), "test"); - CHECK_EQ(get_common_parts("test/chan", "test/chan", "/"), "test/chan"); - CHECK_EQ(get_common_parts("st/ch", "test/chan", "/"), ""); - CHECK_EQ(get_common_parts("st/chan", "test/chan", "/"), "chan"); - CHECK_EQ(get_common_parts("st/chan/abc", "test/chan/abc", "/"), "chan/abc"); - CHECK_EQ(get_common_parts("test/ch", "test/chan", "/"), "test"); - CHECK_EQ(get_common_parts("test/an/abc", "test/chan/abc", "/"), "abc"); - CHECK_EQ(get_common_parts("test/chan/label", "label/abcd/xyz", "/"), "label"); - CHECK_EQ(get_common_parts("test/chan/label", "chan/label/abcd", "/"), "chan/label"); - CHECK_EQ(get_common_parts("test/chan/label", "abcd/chan/label", "/"), "chan/label"); - CHECK_EQ(get_common_parts("test", "abcd", "/"), ""); - CHECK_EQ(get_common_parts("test", "abcd/xyz", "/"), ""); - CHECK_EQ(get_common_parts("test/xyz", "abcd/xyz", "/"), "xyz"); - CHECK_EQ(get_common_parts("test/xyz", "abcd/gef", "/"), ""); - CHECK_EQ(get_common_parts("abcd/test", "abcd/xyz", "/"), ""); - - CHECK_EQ(get_common_parts("", "", "."), ""); - CHECK_EQ(get_common_parts("", "test", "."), ""); - CHECK_EQ(get_common_parts("test", "test", "."), "test"); - CHECK_EQ(get_common_parts("test.chan", "test.chan", "."), "test.chan"); - CHECK_EQ(get_common_parts("test.chan.label", "chan.label.abcd", "."), "chan.label"); - CHECK_EQ(get_common_parts("test/chan/label", "chan/label/abcd", "."), ""); - CHECK_EQ(get_common_parts("st/ch", "test/chan", "."), ""); - CHECK_EQ(get_common_parts("st.ch", "test.chan", "."), ""); - - CHECK_EQ(get_common_parts("test..chan", "test..chan", ".."), "test..chan"); + TEST_CASE("concat_dedup_splits") + { + for (std::string_view sep : { "/", "//", "/////", "./", "./." }) + { + CAPTURE(sep); + + CHECK_EQ(concat_dedup_splits("", "", sep), ""); + + CHECK_EQ( + concat_dedup_splits(fmt::format("test{}chan", sep), "", sep), + fmt::format("test{}chan", sep) + ); + CHECK_EQ( + concat_dedup_splits("", fmt::format("test{}chan", sep), sep), + fmt::format("test{}chan", sep) + ); + CHECK_EQ( + concat_dedup_splits("test", fmt::format("test{}chan", sep), sep), + fmt::format("test{}chan", sep) + ); + CHECK_EQ(concat_dedup_splits("test", "chan", sep), fmt::format("test{}chan", sep)); + CHECK_EQ( + concat_dedup_splits(fmt::format("test{}chan", sep), "chan", sep), + fmt::format("test{}chan", sep) + ); + CHECK_EQ( + concat_dedup_splits(fmt::format("test{}chan", sep), fmt::format("chan{}foo", sep), sep), + fmt::format("test{}chan{}foo", sep, sep) + ); + CHECK_EQ( + concat_dedup_splits( + fmt::format("test{}chan-foo", sep), + fmt::format("foo{}bar", sep), + sep + ), + fmt::format("test{}chan-foo{}foo{}bar", sep, sep, sep, sep) + ); + CHECK_EQ( + concat_dedup_splits( + fmt::format("ab{}test{}chan", sep, sep), + fmt::format("chan{}foo{}ab", sep, sep), + sep + ), + fmt::format("ab{}test{}chan{}foo{}ab", sep, sep, sep, sep) + ); + CHECK_EQ( + concat_dedup_splits( + fmt::format("{}test{}chan", sep, sep), + fmt::format("chan{}foo{}", sep, sep), + sep + ), + fmt::format("{}test{}chan{}foo{}", sep, sep, sep, sep) + ); + CHECK_EQ( + concat_dedup_splits( + fmt::format("test{}chan", sep), + fmt::format("chan{}test", sep), + sep + ), + fmt::format("test{}chan{}test", sep, sep) + ); + } + + CHECK_EQ(concat_dedup_splits("test/chan", "chan/foo", "//"), "test/chan//chan/foo"); + CHECK_EQ(concat_dedup_splits("test/chan", "chan/foo", '/'), "test/chan/foo"); } } - } // namespace mamba diff --git a/libmamba/tests/src/util/test_tuple_hash.cpp b/libmamba/tests/src/util/test_tuple_hash.cpp index d871151825..677b9696d4 100644 --- a/libmamba/tests/src/util/test_tuple_hash.cpp +++ b/libmamba/tests/src/util/test_tuple_hash.cpp @@ -28,4 +28,26 @@ TEST_SUITE("util::tuple_hash") const auto t3 = std::tuple{ std::string("hello"), 33 }; CHECK_NE(hash_tuple(t1), hash_tuple(t3)); } + + TEST_CASE("hash_combine_val_range") + { + const auto hello = std::string("hello"); + // Hash colision are hard to predict, but this is so trivial it is likely a bug if it fails. + CHECK_NE(hash_combine_val_range(0, hello.cbegin(), hello.cend()), 0); + CHECK_NE(hash_combine_val_range(0, hello.crbegin(), hello.crend()), 0); + CHECK_NE( + hash_combine_val_range(0, hello.cbegin(), hello.cend()), + hash_combine_val_range(0, hello.crbegin(), hello.crend()) + ); + } + + TEST_CASE("hash_range") + { + const auto hello = std::string("hello"); + const auto world = std::string("world"); + // Hash colision are hard to predict, but this is so trivial it is likely a bug if it fails. + CHECK_NE(hash_range(hello), 0); + CHECK_NE(hash_range(world), 0); + CHECK_NE(hash_range(hello), hash_range(world)); + } } diff --git a/libmambapy/src/libmambapy/bindings/legacy.cpp b/libmambapy/src/libmambapy/bindings/legacy.cpp index 43b4ec8b6b..60a7fb8f09 100644 --- a/libmambapy/src/libmambapy/bindings/legacy.cpp +++ b/libmambapy/src/libmambapy/bindings/legacy.cpp @@ -1154,10 +1154,8 @@ bind_submodule_impl(pybind11::module_ m) ); } )) - .def_property_readonly("location", &Channel::location) - .def_property_readonly("name", &Channel::name) .def_property_readonly("platforms", &Channel::platforms) - .def_property_readonly("canonical_name", &Channel::canonical_name) + .def_property_readonly("canonical_name", &Channel::display_name) .def("urls", &Channel::urls, py::arg("with_credentials") = true) .def("platform_urls", &Channel::platform_urls, py::arg("with_credentials") = true) .def("platform_url", &Channel::platform_url, py::arg("platform"), py::arg("with_credentials") = true) @@ -1165,7 +1163,7 @@ bind_submodule_impl(pybind11::module_ m) "__repr__", [](const Channel& c) { - auto s = c.name(); + auto s = c.display_name(); s += "["; bool first = true; for (const auto& platform : c.platforms()) diff --git a/micromamba/src/env.cpp b/micromamba/src/env.cpp index 89ff39210c..9703d9488d 100644 --- a/micromamba/src/env.cpp +++ b/micromamba/src/env.cpp @@ -187,7 +187,7 @@ set_env_command(CLI::App* com, Configuration& config) dependencies << "- "; if (channel_subdir) { - dependencies << channel.canonical_name() << "/" << v.subdir << "::"; + dependencies << channel.display_name() << "/" << v.subdir << "::"; } dependencies << v.name << "=" << v.version; if (!no_build) @@ -201,7 +201,7 @@ set_env_command(CLI::App* com, Configuration& config) dependencies << "\n"; } - channels.insert(channel.canonical_name()); + channels.insert(channel.display_name()); } for (const auto& c : channels) diff --git a/micromamba/tests/test_config.py b/micromamba/tests/test_config.py index 32a2bcdf1f..2c0f8d9e67 100644 --- a/micromamba/tests/test_config.py +++ b/micromamba/tests/test_config.py @@ -226,10 +226,8 @@ def test_list_with_sources(self, rc_file, source_flag): def test_list_map_with_sources(self, rc_file, source_flag): home_folder = os.path.expanduser("~") src = f" # '{str(rc_file).replace(home_folder, '~')}'" - assert ( - config("list", "--no-env", "--rc-file", rc_file, source_flag).splitlines() - == f"custom_channels:\n key1: value1{src}\n".splitlines() - ) + out = config("list", "--no-env", "--rc-file", rc_file, source_flag) + assert f"key1: value1{src}" in out @pytest.mark.parametrize("desc_flag", ["--descriptions", "-d"]) @pytest.mark.parametrize("rc_file_args", ({"channels": ["channel1", "channel2"]},)) @@ -850,6 +848,7 @@ def test_envsubst_windows_problem(self, monkeypatch, rc_file): out["channel_alias"] == "https://xxxxxxxxxxxxxxxxxxxx.com/t/kkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkk/get" ) - assert out["custom_channels"] == { - "yyyyyyyyyyyy": "https://uuuuuuuuu:pppppppppppppppppppp@xxxxxxxxxxxxxxx.com" - } + assert ( + out["custom_channels"]["yyyyyyyyyyyy"] + == "https://uuuuuuuuu:pppppppppppppppppppp@xxxxxxxxxxxxxxx.com" + )