| @@ -6,4 +6,5 @@ Uses: Niebler/range-v3 | |||
| Uses: nlohmann/json | |||
| Uses: neo/buffer | |||
| Uses: neo/sqlite3 | |||
| Uses: semver/semver | |||
| Uses: semver/semver | |||
| Uses: pubgrub/pubgrub | |||
| @@ -1,5 +1,7 @@ | |||
| #include "./catalog.hpp" | |||
| #include <dds/solve/solve.hpp> | |||
| #include <neo/sqlite3/exec.hpp> | |||
| #include <neo/sqlite3/iter_tuples.hpp> | |||
| #include <neo/sqlite3/single.hpp> | |||
| @@ -157,11 +159,16 @@ void catalog::store(const package_info& pkg) { | |||
| )"_sql); | |||
| for (const auto& dep : pkg.deps) { | |||
| new_dep_st.reset(); | |||
| if (dep.versions.num_intervals() != 1) { | |||
| throw std::runtime_error( | |||
| "Package dependency may only contain a single version interval"); | |||
| } | |||
| auto iv_1 = *dep.versions.iter_intervals().begin(); | |||
| sqlite3::exec(new_dep_st, | |||
| std::forward_as_tuple(db_pkg_id, | |||
| dep.name, | |||
| dep.version_range.low().to_string(), | |||
| dep.version_range.high().to_string())); | |||
| iv_1.low.to_string(), | |||
| iv_1.high.to_string())); | |||
| } | |||
| } | |||
| @@ -244,9 +251,7 @@ std::vector<dependency> catalog::dependencies_of(const package_id& pkg) const no | |||
| std::forward_as_tuple(pkg.name, pkg.version.to_string())) // | |||
| | ranges::views::transform([](auto&& pair) { | |||
| auto& [name, low, high] = pair; | |||
| return dependency{name, | |||
| semver::range(semver::version::parse(low), | |||
| semver::version::parse(high))}; | |||
| return dependency{name, {semver::version::parse(low), semver::version::parse(high)}}; | |||
| }) // | |||
| | ranges::to_vector; | |||
| } | |||
| @@ -299,9 +304,10 @@ void catalog::import_json_str(std::string_view content) { | |||
| pkg_name, | |||
| version_, | |||
| dep_name)); | |||
| auto range = semver::range::parse(std::string(dep_version)); | |||
| info.deps.push_back({ | |||
| std::string(dep_name), | |||
| semver::range::parse(std::string(dep_version)), | |||
| {range.low(), range.high()}, | |||
| }); | |||
| } | |||
| @@ -325,3 +331,9 @@ void catalog::import_json_str(std::string_view content) { | |||
| } | |||
| } | |||
| } | |||
| std::vector<package_id> catalog::solve_requirements(const std::vector<dependency>& deps) const { | |||
| return dds::solve(deps, | |||
| [&](std::string_view pkg_name) { return this->by_name(pkg_name); }, | |||
| [&](const package_id& pkg) { return this->dependencies_of(pkg); }); | |||
| } | |||
| @@ -1,9 +1,9 @@ | |||
| #pragma once | |||
| #include <dds/catalog/git.hpp> | |||
| #include <dds/deps.hpp> | |||
| #include <dds/package_id.hpp> | |||
| #include <dds/util/fs.hpp> | |||
| #include <dds/catalog/git.hpp> | |||
| #include <neo/sqlite3/database.hpp> | |||
| #include <neo/sqlite3/statement.hpp> | |||
| @@ -38,7 +38,7 @@ public: | |||
| static catalog open(const std::string& db_path); | |||
| static catalog open(path_ref db_path) { return open(db_path.string()); } | |||
| void store(const package_info& info); | |||
| void store(const package_info& info); | |||
| std::optional<package_info> get(const package_id& id) const noexcept; | |||
| std::vector<package_id> by_name(std::string_view sv) const noexcept; | |||
| @@ -49,6 +49,8 @@ public: | |||
| auto content = dds::slurp_file(json_path); | |||
| import_json_str(content); | |||
| } | |||
| std::vector<package_id> solve_requirements(const std::vector<dependency>& deps) const; | |||
| }; | |||
| } // namespace dds | |||
| @@ -34,8 +34,8 @@ TEST_CASE_METHOD(catalog_test_case, "Package requirements") { | |||
| db.store(dds::package_info{ | |||
| dds::package_id{"foo", semver::version::parse("1.2.3")}, | |||
| { | |||
| {"bar", semver::range::parse("=1.2.5")}, | |||
| {"baz", semver::range::parse("^5.3.2")}, | |||
| {"bar", {semver::version::parse("1.2.3"), semver::version::parse("1.4.0")}}, | |||
| {"baz", {semver::version::parse("5.3.0"), semver::version::parse("6.0.0")}}, | |||
| }, | |||
| dds::git_remote_listing{"http://example.com", "master", std::nullopt}, | |||
| }); | |||
| @@ -72,5 +72,43 @@ TEST_CASE_METHOD(catalog_test_case, "Parse JSON repo") { | |||
| auto deps = db.dependencies_of(pkgs[0]); | |||
| REQUIRE(deps.size() == 1); | |||
| CHECK(deps[0].name == "bar"); | |||
| CHECK(deps[0].version_range == semver::range::parse("~4.2.1")); | |||
| CHECK(deps[0].versions | |||
| == dds::version_range_set{semver::version::parse("4.2.1"), | |||
| semver::version::parse("4.3.0")}); | |||
| } | |||
| TEST_CASE_METHOD(catalog_test_case, "Simple solve") { | |||
| db.import_json_str(R"({ | |||
| "version": 1, | |||
| "packages": { | |||
| "foo": { | |||
| "1.2.3": { | |||
| "depends": { | |||
| "bar": "~4.2.1" | |||
| }, | |||
| "git": { | |||
| "url": "http://example.com", | |||
| "ref": "master" | |||
| } | |||
| } | |||
| }, | |||
| "bar": { | |||
| "4.2.3": { | |||
| "depends": {}, | |||
| "git": { | |||
| "url": "http://example.com", | |||
| "ref": "master" | |||
| } | |||
| } | |||
| } | |||
| } | |||
| })"); | |||
| auto sln = db.solve_requirements({{"foo", | |||
| dds::version_range_set{semver::version::parse("1.0.0"), | |||
| semver::version::parse("2.0.0")}}}); | |||
| REQUIRE(sln.size() == 2); | |||
| CHECK(sln[0].name == "foo"); | |||
| CHECK(sln[0].version == semver::version::parse("1.2.3")); | |||
| CHECK(sln[1].name == "bar"); | |||
| CHECK(sln[1].version == semver::version::parse("4.2.3")); | |||
| } | |||
| @@ -541,7 +541,7 @@ struct cli_deps { | |||
| int run() { | |||
| const auto man = parent.load_package_manifest(); | |||
| for (const auto& dep : man.dependencies) { | |||
| std::cout << dep.name << " " << dep.version_range.to_string() << '\n'; | |||
| std::cout << dep.name << " " << dep.versions << '\n'; | |||
| } | |||
| return 0; | |||
| } | |||
| @@ -557,34 +557,30 @@ struct cli_deps { | |||
| repo_path_flag repo_where{cmd}; | |||
| catalog_path_flag catalog_path{cmd}; | |||
| int run() { | |||
| auto man = parent.load_package_manifest(); | |||
| auto catalog = catalog_path.open(); | |||
| bool failed = false; | |||
| auto man = parent.load_package_manifest(); | |||
| auto catalog = catalog_path.open(); | |||
| bool failed = false; | |||
| auto solved_deps = catalog.solve_requirements(man.dependencies); | |||
| dds::repository::with_repository( // | |||
| repo_where.Get(), | |||
| dds::repo_flags::write_lock | dds::repo_flags::create_if_absent, | |||
| [&](dds::repository repo) { | |||
| for (auto& dep : man.dependencies) { | |||
| assert(false && "Not ready yet"); | |||
| // auto exists = !!repo.find(dep.name, dep.version_range); | |||
| // if (!exists) { | |||
| // spdlog::info("Pull remote: {}@{}", dep.name, | |||
| // dep.version_range.to_string()); auto opt_pkg = | |||
| // catalog.get(dds::package_id{dep.name, dep.version_range}); if | |||
| // (opt_pkg) { | |||
| // auto tsd = dds::get_package_sdist(*opt_pkg); | |||
| // repo.add_sdist(tsd.sdist, dds::if_exists::ignore); | |||
| // } else { | |||
| // spdlog::error("No remote listing for {}@{}", | |||
| // dep.name, | |||
| // dep.version_range.to_string()); | |||
| // failed = true; | |||
| // } | |||
| // } else { | |||
| // spdlog::info("Okay: {} {}", dep.name, dep.version_range.to_string()); | |||
| // } | |||
| for (const dds::package_id& pk : solved_deps) { | |||
| auto exists = !!repo.find(pk); | |||
| if (!exists) { | |||
| spdlog::info("Pull remote: {}", pk.to_string()); | |||
| auto opt_pkg = catalog.get(pk); | |||
| if (opt_pkg) { | |||
| auto tsd = dds::get_package_sdist(*opt_pkg); | |||
| repo.add_sdist(tsd.sdist, dds::if_exists::ignore); | |||
| } else { | |||
| spdlog::error("No remote listing for {}", pk.to_string()); | |||
| failed = true; | |||
| } | |||
| } else { | |||
| spdlog::info("Okay: {}", pk.to_string()); | |||
| } | |||
| } | |||
| }); | |||
| if (failed) { | |||
| @@ -624,9 +620,7 @@ struct cli_deps { | |||
| repo_where.Get(), | |||
| dds::repo_flags::read, | |||
| [&](dds::repository repo) { | |||
| return find_dependencies(repo, | |||
| man.dependencies.begin(), | |||
| man.dependencies.end()); | |||
| return repo.solve(man.dependencies); | |||
| }); | |||
| auto tc = tc_filepath.get_toolchain(); | |||
| @@ -10,6 +10,7 @@ | |||
| #include <range/v3/range/conversion.hpp> | |||
| #include <range/v3/view/transform.hpp> | |||
| #include <spdlog/spdlog.h> | |||
| #include <spdlog/fmt/ostr.h> | |||
| #include <cctype> | |||
| #include <map> | |||
| @@ -31,7 +32,7 @@ dependency dependency::parse_depends_string(std::string_view str) { | |||
| try { | |||
| auto rng = semver::range::parse_restricted(version_str); | |||
| return dependency{std::string(name), rng}; | |||
| return dependency{std::string(name), {rng.low(), rng.high()}}; | |||
| } catch (const semver::invalid_range&) { | |||
| throw std::runtime_error(fmt::format( | |||
| "Invalid version range string '{}' in dependency declaration '{}' (Should be a " | |||
| @@ -41,38 +42,6 @@ dependency dependency::parse_depends_string(std::string_view str) { | |||
| } | |||
| } | |||
| std::vector<sdist> dds::find_dependencies(const repository& repo, const dependency& dep) { | |||
| std::vector<sdist> acc; | |||
| detail::do_find_deps(repo, dep, acc); | |||
| return acc; | |||
| } | |||
| void detail::do_find_deps(const repository& repo, const dependency& dep, std::vector<sdist>& sd) { | |||
| auto sdist_opt = repo.find(dep.name, dep.version_range.low()); | |||
| if (!sdist_opt) { | |||
| throw std::runtime_error( | |||
| fmt::format("Unable to find dependency to satisfy requirement: {} {}", | |||
| dep.name, | |||
| dep.version_range.to_string())); | |||
| } | |||
| const sdist& new_sd = *sdist_opt; | |||
| for (const auto& inner_dep : new_sd.manifest.dependencies) { | |||
| do_find_deps(repo, inner_dep, sd); | |||
| } | |||
| auto insert_point = std::partition_point(sd.begin(), sd.end(), [&](const sdist& cand) { | |||
| return cand.path < new_sd.path; | |||
| }); | |||
| if (insert_point != sd.end() | |||
| && insert_point->manifest.pk_id.name == new_sd.manifest.pk_id.name) { | |||
| if (insert_point->manifest.pk_id.version != new_sd.manifest.pk_id.version) { | |||
| assert(false && "Version conflict resolution not implemented yet"); | |||
| std::terminate(); | |||
| } | |||
| return; | |||
| } | |||
| sd.insert(insert_point, std::move(new_sd)); | |||
| } | |||
| using sdist_index_type = std::map<std::string, std::reference_wrapper<const sdist>>; | |||
| using sdist_names = std::set<std::string>; | |||
| @@ -2,6 +2,7 @@ | |||
| #include <dds/build/plan/full.hpp> | |||
| #include <pubgrub/interval.hpp> | |||
| #include <semver/range.hpp> | |||
| #include <semver/version.hpp> | |||
| @@ -19,30 +20,15 @@ enum class version_strength { | |||
| major, | |||
| }; | |||
| using version_range_set = pubgrub::interval_set<semver::version>; | |||
| struct dependency { | |||
| std::string name; | |||
| semver::range version_range; | |||
| std::string name; | |||
| version_range_set versions; | |||
| static dependency parse_depends_string(std::string_view str); | |||
| }; | |||
| namespace detail { | |||
| void do_find_deps(const repository&, const dependency& dep, std::vector<sdist>& acc); | |||
| } // namespace detail | |||
| std::vector<sdist> find_dependencies(const repository& repo, const dependency& dep); | |||
| template <typename Iter, typename Snt> | |||
| inline std::vector<sdist> find_dependencies(const repository& repo, Iter it, Snt stop) { | |||
| std::vector<sdist> acc; | |||
| while (it != stop) { | |||
| detail::do_find_deps(repo, *it++, acc); | |||
| } | |||
| return acc; | |||
| } | |||
| build_plan create_deps_build_plan(const std::vector<sdist>& deps, build_env_ref env); | |||
| void write_libman_index(path_ref where, const build_plan& plan, const build_env& env); | |||
| @@ -1,11 +1,13 @@ | |||
| #include "./repo.hpp" | |||
| #include <dds/sdist.hpp> | |||
| #include <dds/solve/solve.hpp> | |||
| #include <dds/util/paths.hpp> | |||
| #include <dds/util/string.hpp> | |||
| #include <spdlog/spdlog.h> | |||
| #include <range/v3/action/sort.hpp> | |||
| #include <range/v3/range/conversion.hpp> | |||
| #include <range/v3/view/filter.hpp> | |||
| #include <range/v3/view/transform.hpp> | |||
| @@ -96,10 +98,37 @@ void repository::add_sdist(const sdist& sd, if_exists ife_action) { | |||
| spdlog::info("Source distribution '{}' successfully exported", sd.manifest.pk_id.to_string()); | |||
| } | |||
| const sdist* repository::find(std::string_view name, semver::version ver) const noexcept { | |||
| auto found = _sdists.find(std::tie(name, ver)); | |||
| const sdist* repository::find(const package_id& pkg) const noexcept { | |||
| auto found = _sdists.find(pkg); | |||
| if (found == _sdists.end()) { | |||
| return nullptr; | |||
| } | |||
| return &*found; | |||
| } | |||
| std::vector<sdist> repository::solve(const std::vector<dependency>& deps) const { | |||
| auto ids = dds::solve(deps, | |||
| [&](std::string_view name) -> std::vector<package_id> { | |||
| auto items = ranges::views::all(_sdists) // | |||
| | ranges::views::filter([&](const sdist& sd) { | |||
| return sd.manifest.pk_id.name == name; | |||
| }) | |||
| | ranges::views::transform( | |||
| [](const sdist& sd) { return sd.manifest.pk_id; }) | |||
| | ranges::to_vector; | |||
| ranges::sort(items, std::less<>{}); | |||
| return items; | |||
| }, | |||
| [&](const package_id& pkg_id) { | |||
| auto found = find(pkg_id); | |||
| assert(found); | |||
| return found->manifest.dependencies; | |||
| }); | |||
| return ids // | |||
| | ranges::views::transform([&](const package_id& pk_id) { | |||
| auto found = find(pk_id); | |||
| assert(found); | |||
| return *found; | |||
| }) // | |||
| | ranges::to_vector; | |||
| } | |||
| @@ -80,7 +80,7 @@ public: | |||
| void add_sdist(const sdist&, if_exists = if_exists::throw_exc); | |||
| const sdist* find(std::string_view name, semver::version ver) const noexcept; | |||
| const sdist* find(const package_id& pk) const noexcept; | |||
| auto iter_sdists() const noexcept { | |||
| class ret { | |||
| @@ -95,6 +95,8 @@ public: | |||
| } r{_sdists}; | |||
| return r; | |||
| } | |||
| std::vector<sdist> solve(const std::vector<dependency>& deps) const; | |||
| }; | |||
| } // namespace dds | |||
| @@ -30,15 +30,11 @@ inline constexpr struct sdist_compare_t { | |||
| bool operator()(const sdist& lhs, const sdist& rhs) const { | |||
| return lhs.manifest.pk_id < rhs.manifest.pk_id; | |||
| } | |||
| template <typename Name, typename Version> | |||
| bool operator()(const sdist& lhs, const std::tuple<Name, Version>& rhs) const { | |||
| auto&& [name, ver] = rhs; | |||
| return lhs.manifest.pk_id < package_id{name, ver}; | |||
| bool operator()(const sdist& lhs, const package_id& rhs) const { | |||
| return lhs.manifest.pk_id < rhs; | |||
| } | |||
| template <typename Name, typename Version> | |||
| bool operator()(const std::tuple<Name, Version>& lhs, const sdist& rhs) const { | |||
| auto&& [name, ver] = lhs; | |||
| return package_id{name, ver} < rhs.manifest.pk_id; | |||
| bool operator()(const package_id& lhs, const sdist& rhs) const { | |||
| return lhs < rhs.manifest.pk_id; | |||
| } | |||
| using is_transparent = int; | |||
| } sdist_compare; | |||
| @@ -0,0 +1,102 @@ | |||
| #include "./solve.hpp" | |||
| #include <pubgrub/solve.hpp> | |||
| #include <range/v3/range/conversion.hpp> | |||
| #include <range/v3/view/transform.hpp> | |||
| using namespace dds; | |||
| namespace { | |||
| struct req_type { | |||
| dependency dep; | |||
| using req_ref = const req_type&; | |||
| bool implied_by(req_ref other) const noexcept { | |||
| return dep.versions.contains(other.dep.versions); | |||
| } | |||
| bool excludes(req_ref other) const noexcept { | |||
| return dep.versions.disjoint(other.dep.versions); | |||
| } | |||
| std::optional<req_type> intersection(req_ref other) const noexcept { | |||
| auto range = dep.versions.intersection(other.dep.versions); | |||
| if (range.empty()) { | |||
| return std::nullopt; | |||
| } | |||
| return req_type{dependency{dep.name, std::move(range)}}; | |||
| } | |||
| std::optional<req_type> union_(req_ref other) const noexcept { | |||
| auto range = dep.versions.union_(other.dep.versions); | |||
| if (range.empty()) { | |||
| return std::nullopt; | |||
| } | |||
| return req_type{dependency{dep.name, std::move(range)}}; | |||
| } | |||
| std::optional<req_type> difference(req_ref other) const noexcept { | |||
| auto range = dep.versions.difference(other.dep.versions); | |||
| if (range.empty()) { | |||
| return std::nullopt; | |||
| } | |||
| return req_type{dependency{dep.name, std::move(range)}}; | |||
| } | |||
| auto key() const noexcept { return dep.name; } | |||
| friend bool operator==(req_ref lhs, req_ref rhs) noexcept { | |||
| return lhs.dep.name == rhs.dep.name && lhs.dep.versions == rhs.dep.versions; | |||
| } | |||
| }; | |||
| auto as_pkg_id(const req_type& req) { | |||
| const version_range_set& versions = req.dep.versions; | |||
| assert(versions.num_intervals() == 1); | |||
| return package_id{req.dep.name, (*versions.iter_intervals().begin()).low}; | |||
| } | |||
| struct solver_provider { | |||
| pkg_id_provider_fn& pkgs_for_name; | |||
| deps_provider_fn& deps_for_pkg; | |||
| mutable std::map<std::string, std::vector<package_id>> pkgs_by_name = {}; | |||
| std::optional<req_type> best_candidate(const req_type& req) const { | |||
| auto found = pkgs_by_name.find(req.dep.name); | |||
| if (found == pkgs_by_name.end()) { | |||
| found = pkgs_by_name.emplace(req.dep.name, pkgs_for_name(req.dep.name)).first; | |||
| } | |||
| auto& vec = found->second; | |||
| auto cand = std::find_if(vec.cbegin(), vec.cend(), [&](const package_id& pk) { | |||
| return req.dep.versions.contains(pk.version); | |||
| }); | |||
| if (cand == vec.cend()) { | |||
| return std::nullopt; | |||
| } | |||
| return req_type{dependency{cand->name, {cand->version, cand->version.next_after()}}}; | |||
| } | |||
| std::vector<req_type> requirements_of(const req_type& req) const { | |||
| auto pk_id = as_pkg_id(req); | |||
| auto deps = deps_for_pkg(pk_id); | |||
| return deps // | |||
| | ranges::views::transform([](const dependency& dep) { return req_type{dep}; }) // | |||
| | ranges::to_vector; | |||
| } | |||
| }; | |||
| } // namespace | |||
| std::vector<package_id> dds::solve(const std::vector<dependency>& deps, | |||
| pkg_id_provider_fn pkgs_prov, | |||
| deps_provider_fn deps_prov) { | |||
| auto wrap_req | |||
| = deps | ranges::v3::views::transform([](const dependency& dep) { return req_type{dep}; }); | |||
| auto solution = pubgrub::solve(wrap_req, solver_provider{pkgs_prov, deps_prov}); | |||
| return solution | ranges::views::transform(as_pkg_id) | ranges::to_vector; | |||
| } | |||
| @@ -0,0 +1,16 @@ | |||
| #pragma once | |||
| #include <dds/deps.hpp> | |||
| #include <dds/package_id.hpp> | |||
| #include <functional> | |||
| namespace dds { | |||
| using pkg_id_provider_fn = std::function<std::vector<package_id>(std::string_view)>; | |||
| using deps_provider_fn = std::function<std::vector<dependency>(const package_id& pk)>; | |||
| std::vector<package_id> | |||
| solve(const std::vector<dependency>& deps, pkg_id_provider_fn, deps_provider_fn); | |||
| } // namespace dds | |||