| @@ -0,0 +1,84 @@ | |||
| #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 | |||
| @@ -21,6 +21,7 @@ command pkg_import; | |||
| command pkg_ls; | |||
| command pkg_repo_add; | |||
| command pkg_repo_update; | |||
| command repoman_add; | |||
| command repoman_import; | |||
| command repoman_init; | |||
| command repoman_ls; | |||
| @@ -66,6 +67,8 @@ int dispatch_main(const options& opts) noexcept { | |||
| switch (opts.repoman.subcommand) { | |||
| case repoman_subcommand::import: | |||
| return cmd::repoman_import(opts); | |||
| case repoman_subcommand::add: | |||
| return cmd::repoman_add(opts); | |||
| case repoman_subcommand::init: | |||
| return cmd::repoman_init(opts); | |||
| case repoman_subcommand::remove: | |||
| @@ -55,25 +55,5 @@ auto handlers = std::tuple( // | |||
| } // namespace | |||
| 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); | |||
| } | |||
| @@ -344,15 +344,19 @@ struct setup { | |||
| .name = "init", | |||
| .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({ | |||
| .name = "ls", | |||
| .help = "List the contents of a package repository directory", | |||
| }); | |||
| 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({ | |||
| .name = "remove", | |||
| .help = "Remove packages from a package repository", | |||
| @@ -382,6 +386,27 @@ struct setup { | |||
| }); | |||
| } | |||
| 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) { | |||
| repoman_remove_cmd.add_argument(repoman_repo_dir_arg.dup()); | |||
| repoman_remove_cmd.add_argument({ | |||
| @@ -65,6 +65,7 @@ enum class repoman_subcommand { | |||
| _none_, | |||
| init, | |||
| import, | |||
| add, | |||
| remove, | |||
| ls, | |||
| }; | |||
| @@ -224,6 +225,13 @@ struct options { | |||
| std::vector<fs::path> files; | |||
| } 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' | |||
| struct { | |||
| /// Package IDs of packages to remove | |||
| @@ -1,6 +1,7 @@ | |||
| #include <dds/pkg/id.hpp> | |||
| #include <dds/error/errors.hpp> | |||
| #include <dds/util/result.hpp> | |||
| #include <fmt/core.h> | |||
| @@ -8,7 +9,8 @@ | |||
| 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('@'); | |||
| if (at_pos == s.npos) { | |||
| throw_user_error<errc::invalid_pkg_id>("Invalid package ID '{}'", s); | |||
| @@ -17,7 +19,12 @@ pkg_id pkg_id::parse(std::string_view s) { | |||
| auto name = s.substr(0, at_pos); | |||
| 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) | |||
| @@ -8,6 +8,10 @@ | |||
| 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. | |||
| * | |||
| @@ -1,5 +1,6 @@ | |||
| #include "./repoman.hpp" | |||
| #include <dds/pkg/info.hpp> | |||
| #include <dds/sdist/package.hpp> | |||
| #include <dds/util/log.hpp> | |||
| #include <dds/util/result.hpp> | |||
| @@ -16,6 +17,8 @@ | |||
| #include <neo/utility.hpp> | |||
| #include <nlohmann/json.hpp> | |||
| #include <fstream> | |||
| using namespace dds; | |||
| namespace nsql = neo::sqlite3; | |||
| @@ -148,35 +151,12 @@ void repo_manager::import_targz(path_ref tgz_file) { | |||
| neo::sqlite3::transaction_guard tr{_db}; | |||
| 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"; | |||
| fs::create_directories(dest_path.parent_path()); | |||
| @@ -220,3 +200,41 @@ void repo_manager::delete_package(pkg_id pkg_id) { | |||
| 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; | |||
| } | |||
| @@ -10,6 +10,8 @@ | |||
| namespace dds { | |||
| struct pkg_info; | |||
| struct e_init_repo { | |||
| fs::path path; | |||
| }; | |||
| @@ -55,6 +57,7 @@ public: | |||
| void import_targz(path_ref tgz_path); | |||
| void delete_package(pkg_id id); | |||
| void add_pkg(const pkg_info& info, std::string_view url); | |||
| auto all_packages() const noexcept { | |||
| using namespace neo::sqlite3::literals; | |||
| @@ -1,6 +1,8 @@ | |||
| #include <dds/repoman/repoman.hpp> | |||
| #include <dds/pkg/info.hpp> | |||
| #include <dds/temp.hpp> | |||
| #include <neo/sqlite3/error.hpp> | |||
| #include <catch2/catch.hpp> | |||
| @@ -12,11 +14,14 @@ const auto THIS_DIR = THIS_FILE.parent_path(); | |||
| const auto REPO_ROOT = (THIS_DIR / "../../../").lexically_normal(); | |||
| 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 | |||
| 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"; | |||
| repo.import_targz(neo_url_tgz); | |||
| CHECK(dds::fs::is_directory(repo.pkg_dir() / "neo-url/")); | |||
| @@ -28,3 +33,16 @@ TEST_CASE("Open and import into a repository") { | |||
| 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)); | |||
| } | |||
| 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")); | |||
| } | |||
| @@ -1,7 +1,12 @@ | |||
| #include "./result.hpp" | |||
| #include <dds/util/log.hpp> | |||
| #include <fmt/ostream.h> | |||
| #include <neo/sqlite3/error.hpp> | |||
| #include <fstream> | |||
| void dds::capture_exception() { | |||
| try { | |||
| throw; | |||
| @@ -15,3 +20,12 @@ void dds::capture_exception() { | |||
| // Re-throw as a bare 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); | |||
| } | |||
| } | |||
| @@ -9,6 +9,7 @@ | |||
| #include <exception> | |||
| #include <filesystem> | |||
| #include <string> | |||
| #include <string_view> | |||
| namespace dds { | |||
| @@ -68,6 +69,8 @@ struct e_parse_error { | |||
| #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 | |||
| * in-flight error if the current scope is exitted via exception or a bad result<> | |||
| @@ -0,0 +1,33 @@ | |||
| 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']) | |||