| #include "../options.hpp" | |||||
| #include <dds/error/errors.hpp> | |||||
| #include <dds/pkg/get/get.hpp> | |||||
| #include <dds/pkg/info.hpp> | |||||
| #include <dds/repoman/repoman.hpp> | |||||
| #include <dds/util/http/pool.hpp> | |||||
| #include <dds/util/result.hpp> | |||||
| #include <boost/leaf/handle_exception.hpp> | |||||
| #include <fmt/ostream.h> | |||||
| #include <neo/sqlite3/error.hpp> | |||||
| namespace dds::cli::cmd { | |||||
| static int _repoman_add(const options& opts) { | |||||
| auto pkg_id = dds::pkg_id::parse(opts.repoman.add.pkg_id_str); | |||||
| auto listing = parse_remote_url(opts.repoman.add.url_str); | |||||
| dds::pkg_info add_info{ | |||||
| .ident = pkg_id, | |||||
| .deps = {}, | |||||
| .description = opts.repoman.add.description, | |||||
| .remote = listing, | |||||
| }; | |||||
| auto temp_sdist = get_package_sdist(add_info); | |||||
| add_info.deps = temp_sdist.sdist.manifest.dependencies; | |||||
| auto repo = repo_manager::open(opts.repoman.repo_dir); | |||||
| repo.add_pkg(add_info, opts.repoman.add.url_str); | |||||
| return 0; | |||||
| } | |||||
| int repoman_add(const options& opts) { | |||||
| return boost::leaf::try_catch( // | |||||
| [&] { | |||||
| try { | |||||
| return _repoman_add(opts); | |||||
| } catch (...) { | |||||
| dds::capture_exception(); | |||||
| } | |||||
| }, | |||||
| [](user_error<errc::invalid_pkg_id>, | |||||
| semver::invalid_version err, | |||||
| dds::e_invalid_pkg_id_str idstr) -> int { | |||||
| dds_log(error, | |||||
| "Package ID string '{}' is invalid, because '{}' is not a valid semantic " | |||||
| "version string", | |||||
| idstr.value, | |||||
| err.string()); | |||||
| write_error_marker("invalid-pkg-id-str-version"); | |||||
| throw; | |||||
| }, | |||||
| [](user_error<errc::invalid_pkg_id>, dds::e_invalid_pkg_id_str idstr) -> int { | |||||
| dds_log(error, "Invalid package ID string '{}'", idstr.value); | |||||
| write_error_marker("invalid-pkg-id-str"); | |||||
| throw; | |||||
| }, | |||||
| [](dds::e_sqlite3_error_exc, | |||||
| boost::leaf::match<neo::sqlite3::errc, neo::sqlite3::errc::constraint_unique>, | |||||
| dds::pkg_id pkid) { | |||||
| dds_log(error, "Package {} is already present in the repository", pkid.to_string()); | |||||
| write_error_marker("dup-pkg-add"); | |||||
| return 1; | |||||
| }, | |||||
| [](http_status_error, http_response_info resp, neo::url url) { | |||||
| dds_log(error, | |||||
| "Error resulted from HTTP request [{}]: {} {}", | |||||
| url.to_string(), | |||||
| resp.status, | |||||
| resp.status_message); | |||||
| return 1; | |||||
| }, | |||||
| [](dds::e_sqlite3_error_exc e, dds::e_repo_import_targz tgz) { | |||||
| dds_log(error, "Database error while importing tar file {}: {}", tgz.path, e.message); | |||||
| return 1; | |||||
| }, | |||||
| [](dds::e_system_error_exc e, dds::e_open_repo_db db) { | |||||
| dds_log(error, "Error while opening repository database {}: {}", db.path, e.message); | |||||
| return 1; | |||||
| }); | |||||
| } | |||||
| } // namespace dds::cli::cmd | 
| command pkg_ls; | command pkg_ls; | ||||
| command pkg_repo_add; | command pkg_repo_add; | ||||
| command pkg_repo_update; | command pkg_repo_update; | ||||
| command repoman_add; | |||||
| command repoman_import; | command repoman_import; | ||||
| command repoman_init; | command repoman_init; | ||||
| command repoman_ls; | command repoman_ls; | ||||
| switch (opts.repoman.subcommand) { | switch (opts.repoman.subcommand) { | ||||
| case repoman_subcommand::import: | case repoman_subcommand::import: | ||||
| return cmd::repoman_import(opts); | return cmd::repoman_import(opts); | ||||
| case repoman_subcommand::add: | |||||
| return cmd::repoman_add(opts); | |||||
| case repoman_subcommand::init: | case repoman_subcommand::init: | ||||
| return cmd::repoman_init(opts); | return cmd::repoman_init(opts); | ||||
| case repoman_subcommand::remove: | case repoman_subcommand::remove: | 
| } // namespace | } // namespace | ||||
| int dds::handle_cli_errors(std::function<int()> fn) noexcept { | int dds::handle_cli_errors(std::function<int()> fn) noexcept { | ||||
| return boost::leaf::try_catch( | |||||
| [&] { | |||||
| boost::leaf::context<dds::e_error_marker> marker_ctx; | |||||
| marker_ctx.activate(); | |||||
| neo_defer { | |||||
| marker_ctx.deactivate(); | |||||
| marker_ctx.handle_error<void>( | |||||
| boost::leaf::current_error(), | |||||
| [](dds::e_error_marker mark) { | |||||
| dds_log(trace, "[error marker {}]", mark.value); | |||||
| auto efile_path = std::getenv("DDS_WRITE_ERROR_MARKER"); | |||||
| if (efile_path) { | |||||
| std::ofstream outfile{efile_path, std::ios::binary}; | |||||
| fmt::print(outfile, "{}", mark.value); | |||||
| } | |||||
| }, | |||||
| [] {}); | |||||
| }; | |||||
| return fn(); | |||||
| }, | |||||
| handlers); | |||||
| return boost::leaf::try_catch(fn, handlers); | |||||
| } | } | 
| .name = "init", | .name = "init", | ||||
| .help = "Initialize a directory as a new repository", | .help = "Initialize a directory as a new repository", | ||||
| })); | })); | ||||
| setup_repoman_import_cmd(grp.add_parser({ | |||||
| .name = "import", | |||||
| .help = "Import a source distribution into the repository", | |||||
| })); | |||||
| auto& ls_cmd = grp.add_parser({ | auto& ls_cmd = grp.add_parser({ | ||||
| .name = "ls", | .name = "ls", | ||||
| .help = "List the contents of a package repository directory", | .help = "List the contents of a package repository directory", | ||||
| }); | }); | ||||
| ls_cmd.add_argument(repoman_repo_dir_arg.dup()); | ls_cmd.add_argument(repoman_repo_dir_arg.dup()); | ||||
| setup_repoman_add_cmd(grp.add_parser({ | |||||
| .name = "add", | |||||
| .help = "Add a package listing to the repository by URL", | |||||
| })); | |||||
| setup_repoman_import_cmd(grp.add_parser({ | |||||
| .name = "import", | |||||
| .help = "Import a source distribution into the repository", | |||||
| })); | |||||
| setup_repoman_remove_cmd(grp.add_parser({ | setup_repoman_remove_cmd(grp.add_parser({ | ||||
| .name = "remove", | .name = "remove", | ||||
| .help = "Remove packages from a package repository", | .help = "Remove packages from a package repository", | ||||
| }); | }); | ||||
| } | } | ||||
| void setup_repoman_add_cmd(argument_parser& repoman_add_cmd) { | |||||
| repoman_add_cmd.add_argument(repoman_repo_dir_arg.dup()); | |||||
| repoman_add_cmd.add_argument({ | |||||
| .help = "The package ID of the package to add", | |||||
| .valname = "<pkg-id>", | |||||
| .required = true, | |||||
| .action = put_into(opts.repoman.add.pkg_id_str), | |||||
| }); | |||||
| repoman_add_cmd.add_argument({ | |||||
| .help = "URL to add to the repository", | |||||
| .valname = "<url>", | |||||
| .required = true, | |||||
| .action = put_into(opts.repoman.add.url_str), | |||||
| }); | |||||
| repoman_add_cmd.add_argument({ | |||||
| .long_spellings = {"description"}, | |||||
| .short_spellings = {"d"}, | |||||
| .action = put_into(opts.repoman.add.description), | |||||
| }); | |||||
| } | |||||
| void setup_repoman_remove_cmd(argument_parser& repoman_remove_cmd) { | void setup_repoman_remove_cmd(argument_parser& repoman_remove_cmd) { | ||||
| repoman_remove_cmd.add_argument(repoman_repo_dir_arg.dup()); | repoman_remove_cmd.add_argument(repoman_repo_dir_arg.dup()); | ||||
| repoman_remove_cmd.add_argument({ | repoman_remove_cmd.add_argument({ | 
| _none_, | _none_, | ||||
| init, | init, | ||||
| import, | import, | ||||
| add, | |||||
| remove, | remove, | ||||
| ls, | ls, | ||||
| }; | }; | ||||
| std::vector<fs::path> files; | std::vector<fs::path> files; | ||||
| } import; | } import; | ||||
| /// Options for 'dds repoman add' | |||||
| struct { | |||||
| std::string pkg_id_str; | |||||
| std::string url_str; | |||||
| std::string description; | |||||
| } add; | |||||
| /// Options for 'dds repoman remove' | /// Options for 'dds repoman remove' | ||||
| struct { | struct { | ||||
| /// Package IDs of packages to remove | /// Package IDs of packages to remove | 
| #include <dds/pkg/id.hpp> | #include <dds/pkg/id.hpp> | ||||
| #include <dds/error/errors.hpp> | #include <dds/error/errors.hpp> | ||||
| #include <dds/util/result.hpp> | |||||
| #include <fmt/core.h> | #include <fmt/core.h> | ||||
| using namespace dds; | using namespace dds; | ||||
| pkg_id pkg_id::parse(std::string_view s) { | |||||
| pkg_id pkg_id::parse(const std::string_view s) { | |||||
| DDS_E_SCOPE(e_invalid_pkg_id_str{std::string(s)}); | |||||
| auto at_pos = s.find('@'); | auto at_pos = s.find('@'); | ||||
| if (at_pos == s.npos) { | if (at_pos == s.npos) { | ||||
| throw_user_error<errc::invalid_pkg_id>("Invalid package ID '{}'", s); | throw_user_error<errc::invalid_pkg_id>("Invalid package ID '{}'", s); | ||||
| auto name = s.substr(0, at_pos); | auto name = s.substr(0, at_pos); | ||||
| auto ver_str = s.substr(at_pos + 1); | auto ver_str = s.substr(at_pos + 1); | ||||
| return {std::string(name), semver::version::parse(ver_str)}; | |||||
| try { | |||||
| return {std::string(name), semver::version::parse(ver_str)}; | |||||
| } catch (const semver::invalid_version& err) { | |||||
| BOOST_LEAF_THROW_EXCEPTION(user_error<errc::invalid_pkg_id>("Package ID string is invalid"), | |||||
| err); | |||||
| } | |||||
| } | } | ||||
| pkg_id::pkg_id(std::string_view n, semver::version v) | pkg_id::pkg_id(std::string_view n, semver::version v) | 
| namespace dds { | namespace dds { | ||||
| struct e_invalid_pkg_id_str { | |||||
| std::string value; | |||||
| }; | |||||
| /** | /** | ||||
| * Represents a unique package ID. We store this as a simple name-version pair. | * Represents a unique package ID. We store this as a simple name-version pair. | ||||
| * | * | 
| #include "./repoman.hpp" | #include "./repoman.hpp" | ||||
| #include <dds/pkg/info.hpp> | |||||
| #include <dds/sdist/package.hpp> | #include <dds/sdist/package.hpp> | ||||
| #include <dds/util/log.hpp> | #include <dds/util/log.hpp> | ||||
| #include <dds/util/result.hpp> | #include <dds/util/result.hpp> | ||||
| #include <neo/utility.hpp> | #include <neo/utility.hpp> | ||||
| #include <nlohmann/json.hpp> | #include <nlohmann/json.hpp> | ||||
| #include <fstream> | |||||
| using namespace dds; | using namespace dds; | ||||
| namespace nsql = neo::sqlite3; | namespace nsql = neo::sqlite3; | ||||
| neo::sqlite3::transaction_guard tr{_db}; | neo::sqlite3::transaction_guard tr{_db}; | ||||
| dds_log(debug, "Recording package {}@{}", man->id.name, man->id.version.to_string()); | dds_log(debug, "Recording package {}@{}", man->id.name, man->id.version.to_string()); | ||||
| nsql::exec( // | |||||
| _stmts(R"( | |||||
| INSERT INTO dds_repo_packages (name, version, description, url) | |||||
| VALUES ( | |||||
| ?1, | |||||
| ?2, | |||||
| 'No description', | |||||
| printf('dds:%s@%s', ?1, ?2) | |||||
| ) | |||||
| )"_sql), | |||||
| man->id.name, | |||||
| man->id.version.to_string()); | |||||
| auto package_id = _db.last_insert_rowid(); | |||||
| auto& insert_dep_st = _stmts(R"( | |||||
| INSERT INTO dds_repo_package_deps(package_id, dep_name, low, high) | |||||
| VALUES (?, ?, ?, ?) | |||||
| )"_sql); | |||||
| for (auto& dep : man->dependencies) { | |||||
| assert(dep.versions.num_intervals() == 1); | |||||
| auto iv_1 = *dep.versions.iter_intervals().begin(); | |||||
| dds_log(trace, " Depends on: {}", dep.to_string()); | |||||
| nsql::exec(insert_dep_st, | |||||
| package_id, | |||||
| dep.name, | |||||
| iv_1.low.to_string(), | |||||
| iv_1.high.to_string()); | |||||
| } | |||||
| dds::pkg_info info{.ident = man->id, | |||||
| .deps = man->dependencies, | |||||
| .description = "[No description]", | |||||
| .remote = {}}; | |||||
| auto rel_url = fmt::format("dds:{}", man->id.to_string()); | |||||
| add_pkg(info, rel_url); | |||||
| auto dest_path = pkg_dir() / man->id.name / man->id.version.to_string() / "sdist.tar.gz"; | auto dest_path = pkg_dir() / man->id.name / man->id.version.to_string() / "sdist.tar.gz"; | ||||
| fs::create_directories(dest_path.parent_path()); | fs::create_directories(dest_path.parent_path()); | ||||
| throw std::system_error(ec, "Failed to delete package name directory"); | throw std::system_error(ec, "Failed to delete package name directory"); | ||||
| } | } | ||||
| } | } | ||||
| void repo_manager::add_pkg(const pkg_info& info, std::string_view url) { | |||||
| dds_log(info, "Directly add an entry for {}", info.ident.to_string()); | |||||
| DDS_E_SCOPE(info.ident); | |||||
| nsql::recursive_transaction_guard tr{_db}; | |||||
| nsql::exec( // | |||||
| _stmts(R"( | |||||
| INSERT INTO dds_repo_packages (name, version, description, url) | |||||
| VALUES (?, ?, ?, ?) | |||||
| )"_sql), | |||||
| info.ident.name, | |||||
| info.ident.version.to_string(), | |||||
| info.description, | |||||
| url); | |||||
| auto package_rowid = _db.last_insert_rowid(); | |||||
| auto& insert_dep_st = _stmts(R"( | |||||
| INSERT INTO dds_repo_package_deps(package_id, dep_name, low, high) | |||||
| VALUES (?, ?, ?, ?) | |||||
| )"_sql); | |||||
| for (auto& dep : info.deps) { | |||||
| assert(dep.versions.num_intervals() == 1); | |||||
| auto iv_1 = *dep.versions.iter_intervals().begin(); | |||||
| dds_log(trace, " Depends on: {}", dep.to_string()); | |||||
| nsql::exec(insert_dep_st, | |||||
| package_rowid, | |||||
| dep.name, | |||||
| iv_1.low.to_string(), | |||||
| iv_1.high.to_string()); | |||||
| } | |||||
| auto dest_dir = pkg_dir() / info.ident.name / info.ident.version.to_string(); | |||||
| auto stamp_path = dest_dir / "url.txt"; | |||||
| fs::create_directories(dest_dir); | |||||
| std::ofstream stamp_file{stamp_path, std::ios::binary}; | |||||
| stamp_file << url; | |||||
| } | 
| namespace dds { | namespace dds { | ||||
| struct pkg_info; | |||||
| struct e_init_repo { | struct e_init_repo { | ||||
| fs::path path; | fs::path path; | ||||
| }; | }; | ||||
| void import_targz(path_ref tgz_path); | void import_targz(path_ref tgz_path); | ||||
| void delete_package(pkg_id id); | void delete_package(pkg_id id); | ||||
| void add_pkg(const pkg_info& info, std::string_view url); | |||||
| auto all_packages() const noexcept { | auto all_packages() const noexcept { | ||||
| using namespace neo::sqlite3::literals; | using namespace neo::sqlite3::literals; | 
| #include <dds/repoman/repoman.hpp> | #include <dds/repoman/repoman.hpp> | ||||
| #include <dds/pkg/info.hpp> | |||||
| #include <dds/temp.hpp> | #include <dds/temp.hpp> | ||||
| #include <neo/sqlite3/error.hpp> | #include <neo/sqlite3/error.hpp> | ||||
| #include <catch2/catch.hpp> | #include <catch2/catch.hpp> | ||||
| const auto REPO_ROOT = (THIS_DIR / "../../../").lexically_normal(); | const auto REPO_ROOT = (THIS_DIR / "../../../").lexically_normal(); | ||||
| const auto DATA_DIR = REPO_ROOT / "data"; | const auto DATA_DIR = REPO_ROOT / "data"; | ||||
| struct tmp_repo { | |||||
| dds::temporary_dir tempdir = dds::temporary_dir::create(); | |||||
| dds::repo_manager repo = dds::repo_manager::create(tempdir.path(), "test-repo"); | |||||
| }; | |||||
| } // namespace | } // namespace | ||||
| TEST_CASE("Open and import into a repository") { | |||||
| auto tdir = dds::temporary_dir::create(); | |||||
| auto repo = dds::repo_manager::create(tdir.path(), "test-repo"); | |||||
| TEST_CASE_METHOD(tmp_repo, "Open and import into a repository") { | |||||
| auto neo_url_tgz = DATA_DIR / "neo-url@0.2.1.tar.gz"; | auto neo_url_tgz = DATA_DIR / "neo-url@0.2.1.tar.gz"; | ||||
| repo.import_targz(neo_url_tgz); | repo.import_targz(neo_url_tgz); | ||||
| CHECK(dds::fs::is_directory(repo.pkg_dir() / "neo-url/")); | CHECK(dds::fs::is_directory(repo.pkg_dir() / "neo-url/")); | ||||
| CHECK_THROWS_AS(repo.delete_package(dds::pkg_id::parse("neo-url@0.2.1")), std::system_error); | CHECK_THROWS_AS(repo.delete_package(dds::pkg_id::parse("neo-url@0.2.1")), std::system_error); | ||||
| CHECK_NOTHROW(repo.import_targz(neo_url_tgz)); | CHECK_NOTHROW(repo.import_targz(neo_url_tgz)); | ||||
| } | } | ||||
| TEST_CASE_METHOD(tmp_repo, "Add a package directly") { | |||||
| dds::pkg_info info{ | |||||
| .ident = dds::pkg_id::parse("foo@1.2.3"), | |||||
| .deps = {}, | |||||
| .description = "Something", | |||||
| .remote = {}, | |||||
| }; | |||||
| repo.add_pkg(info, "http://example.com"); | |||||
| CHECK_THROWS_AS(repo.add_pkg(info, "https://example.com"), | |||||
| neo::sqlite3::constraint_unique_error); | |||||
| repo.delete_package(dds::pkg_id::parse("foo@1.2.3")); | |||||
| } | 
| #include "./result.hpp" | #include "./result.hpp" | ||||
| #include <dds/util/log.hpp> | |||||
| #include <fmt/ostream.h> | |||||
| #include <neo/sqlite3/error.hpp> | #include <neo/sqlite3/error.hpp> | ||||
| #include <fstream> | |||||
| void dds::capture_exception() { | void dds::capture_exception() { | ||||
| try { | try { | ||||
| throw; | throw; | ||||
| // Re-throw as a bare exception. | // Re-throw as a bare exception. | ||||
| throw std::exception(); | throw std::exception(); | ||||
| } | } | ||||
| void dds::write_error_marker(std::string_view error) noexcept { | |||||
| dds_log(trace, "[error marker {}]", error); | |||||
| auto efile_path = std::getenv("DDS_WRITE_ERROR_MARKER"); | |||||
| if (efile_path) { | |||||
| std::ofstream outfile{efile_path, std::ios::binary}; | |||||
| fmt::print(outfile, "{}", error); | |||||
| } | |||||
| } | 
| #include <exception> | #include <exception> | ||||
| #include <filesystem> | #include <filesystem> | ||||
| #include <string> | #include <string> | ||||
| #include <string_view> | |||||
| namespace dds { | namespace dds { | ||||
| #define DDS_ERROR_MARKER(Value) DDS_E_ARG(::dds::e_error_marker{Value}) | #define DDS_ERROR_MARKER(Value) DDS_E_ARG(::dds::e_error_marker{Value}) | ||||
| void write_error_marker(std::string_view error) noexcept; | |||||
| /** | /** | ||||
| * @brief Generate a leaf::on_error object that loads the given expression into the currently | * @brief Generate a leaf::on_error object that loads the given expression into the currently | ||||
| * in-flight error if the current scope is exitted via exception or a bad result<> | * in-flight error if the current scope is exitted via exception or a bad result<> | 
| import pytest | |||||
| from dds_ci import dds | |||||
| from dds_ci.testing.fixtures import DDSWrapper, Project | |||||
| from dds_ci.testing.error import expect_error_marker | |||||
| from pathlib import Path | |||||
| @pytest.fixture() | |||||
| def tmp_repo(tmp_path: Path, dds: DDSWrapper) -> Path: | |||||
| dds.run(['repoman', 'init', tmp_path]) | |||||
| return tmp_path | |||||
| def test_bad_pkg_id(dds: DDSWrapper, tmp_repo: Path) -> None: | |||||
| with expect_error_marker('invalid-pkg-id-str-version'): | |||||
| dds.run(['repoman', 'add', tmp_repo, 'foo@bar', 'http://example.com']) | |||||
| with expect_error_marker('invalid-pkg-id-str'): | |||||
| dds.run(['repoman', 'add', tmp_repo, 'foo', 'http://example.com']) | |||||
| def test_add_simple(dds: DDSWrapper, tmp_repo: Path) -> None: | |||||
| dds.run(['repoman', 'add', tmp_repo, 'neo-fun@0.6.0', 'git+https://github.com/vector-of-bool/neo-fun.git#0.6.0']) | |||||
| with expect_error_marker('dup-pkg-add'): | |||||
| dds.run( | |||||
| ['repoman', 'add', tmp_repo, 'neo-fun@0.6.0', 'git+https://github.com/vector-of-bool/neo-fun.git#0.6.0']) | |||||
| def test_add_github(dds: DDSWrapper, tmp_repo: Path) -> None: | |||||
| dds.run(['repoman', 'add', tmp_repo, 'neo-fun@0.6.0', 'github:vector-of-bool/neo-fun#0.6.0']) | |||||
| with expect_error_marker('dup-pkg-add'): | |||||
| dds.run(['repoman', 'add', tmp_repo, 'neo-fun@0.6.0', 'github:vector-of-bool/neo-fun#0.6.0']) |