Significantly simplify some code, and removes a lot of hacks. Now, the proper way to get packages is from an HTTP repository. The mkrepo.py uses the content of the old catalog.json to populate a dds repository. This is also used in the test cases to spawn repositories as test fixtures.default_compile_flags
| } | } | ||||
| } create{*this}; | } create{*this}; | ||||
| struct { | |||||
| cli_catalog& parent; | |||||
| args::Command cmd{parent.cat_group, "import", "Import entries into a catalog"}; | |||||
| common_flags _common{cmd}; | |||||
| catalog_path_flag cat_path{cmd}; | |||||
| args::Flag import_stdin{cmd, "stdin", "Import JSON from stdin", {"stdin"}}; | |||||
| args::ValueFlagList<std::string> | |||||
| json_paths{cmd, | |||||
| "json", | |||||
| "Import catalog entries from the given JSON files", | |||||
| {"json", 'j'}}; | |||||
| int run() { | |||||
| auto cat = cat_path.open(); | |||||
| for (const auto& json_fpath : json_paths.Get()) { | |||||
| cat.import_json_file(json_fpath); | |||||
| } | |||||
| if (import_stdin.Get()) { | |||||
| std::ostringstream strm; | |||||
| strm << std::cin.rdbuf(); | |||||
| cat.import_json_str(strm.str()); | |||||
| } | |||||
| return 0; | |||||
| } | |||||
| } import{*this}; | |||||
| struct { | struct { | ||||
| cli_catalog& parent; | cli_catalog& parent; | ||||
| args::Command cmd{parent.cat_group, "get", "Obtain an sdist from a catalog listing"}; | args::Command cmd{parent.cat_group, "get", "Obtain an sdist from a catalog listing"}; | ||||
| int run() { | int run() { | ||||
| if (create.cmd) { | if (create.cmd) { | ||||
| return create.run(); | return create.run(); | ||||
| } else if (import.cmd) { | |||||
| return import.run(); | |||||
| } else if (get.cmd) { | } else if (get.cmd) { | ||||
| return get.run(); | return get.run(); | ||||
| } else if (add.cmd) { | } else if (add.cmd) { |
| remote_id INTEGER | remote_id INTEGER | ||||
| REFERENCES dds_cat_remotes | REFERENCES dds_cat_remotes | ||||
| ON DELETE CASCADE, | ON DELETE CASCADE, | ||||
| repo_transform TEXT NOT NULL DEFAULT '[]', | |||||
| UNIQUE (name, version, remote_id) | UNIQUE (name, version, remote_id) | ||||
| ); | ); | ||||
| name, | name, | ||||
| version, | version, | ||||
| description, | description, | ||||
| remote_url, | |||||
| repo_transform) | |||||
| remote_url) | |||||
| SELECT pkg_id, | SELECT pkg_id, | ||||
| name, | name, | ||||
| version, | version, | ||||
| WHEN lm_name ISNULL THEN '' | WHEN lm_name ISNULL THEN '' | ||||
| ELSE ('?lm=' || lm_namespace || '/' || lm_name) | ELSE ('?lm=' || lm_namespace || '/' || lm_name) | ||||
| END | END | ||||
| ) || '#' || git_ref, | |||||
| repo_transform | |||||
| ) || '#' || git_ref | |||||
| FROM dds_cat_pkgs; | FROM dds_cat_pkgs; | ||||
| CREATE TABLE dds_cat_pkg_deps_new ( | CREATE TABLE dds_cat_pkg_deps_new ( | ||||
| )"); | )"); | ||||
| } | } | ||||
| std::string transforms_to_json(const std::vector<fs_transformation>& trs) { | |||||
| std::string acc = "["; | |||||
| for (auto it = trs.begin(); it != trs.end(); ++it) { | |||||
| acc += it->as_json(); | |||||
| if (std::next(it) != trs.end()) { | |||||
| acc += ", "; | |||||
| } | |||||
| } | |||||
| return acc + "]"; | |||||
| } | |||||
| void store_with_remote(const neo::sqlite3::statement_cache&, | void store_with_remote(const neo::sqlite3::statement_cache&, | ||||
| const package_info& pkg, | const package_info& pkg, | ||||
| std::monostate) { | std::monostate) { | ||||
| name, | name, | ||||
| version, | version, | ||||
| remote_url, | remote_url, | ||||
| description, | |||||
| repo_transform | |||||
| ) VALUES (?1, ?2, ?3, ?4, ?5) | |||||
| description | |||||
| ) VALUES (?1, ?2, ?3, ?4) | |||||
| )"_sql), | )"_sql), | ||||
| pkg.ident.name, | pkg.ident.name, | ||||
| pkg.ident.version.to_string(), | pkg.ident.version.to_string(), | ||||
| http.url, | http.url, | ||||
| pkg.description, | |||||
| transforms_to_json(http.transforms)); | |||||
| pkg.description); | |||||
| } | } | ||||
| void store_with_remote(neo::sqlite3::statement_cache& stmts, | void store_with_remote(neo::sqlite3::statement_cache& stmts, | ||||
| name, | name, | ||||
| version, | version, | ||||
| remote_url, | remote_url, | ||||
| description, | |||||
| repo_transform | |||||
| description | |||||
| ) VALUES ( | ) VALUES ( | ||||
| ?1, | ?1, | ||||
| ?2, | ?2, | ||||
| ?3, | ?3, | ||||
| ?4, | |||||
| ?5 | |||||
| ?4 | |||||
| ) | ) | ||||
| )"_sql), | )"_sql), | ||||
| pkg.ident.name, | pkg.ident.name, | ||||
| pkg.ident.version.to_string(), | pkg.ident.version.to_string(), | ||||
| url, | url, | ||||
| pkg.description, | |||||
| transforms_to_json(git.transforms)); | |||||
| pkg.description); | |||||
| } | } | ||||
| void do_store_pkg(neo::sqlite3::database& db, | void do_store_pkg(neo::sqlite3::database& db, | ||||
| exec(db.prepare("UPDATE dds_cat_meta SET meta=?"), meta.dump()); | exec(db.prepare("UPDATE dds_cat_meta SET meta=?"), meta.dump()); | ||||
| } | } | ||||
| void check_json(bool b, std::string_view what) { | |||||
| if (!b) { | |||||
| throw_user_error<errc::invalid_catalog_json>("Catalog JSON is invalid: {}", what); | |||||
| } | |||||
| } | |||||
| } // namespace | } // namespace | ||||
| catalog catalog::open(const std::string& db_path) { | catalog catalog::open(const std::string& db_path) { | ||||
| name, | name, | ||||
| version, | version, | ||||
| remote_url, | remote_url, | ||||
| description, | |||||
| repo_transform | |||||
| description | |||||
| FROM dds_cat_pkgs | FROM dds_cat_pkgs | ||||
| WHERE name = ?1 AND version = ?2 | WHERE name = ?1 AND version = ?2 | ||||
| ORDER BY pkg_id DESC | ORDER BY pkg_id DESC | ||||
| pk_id.to_string(), | pk_id.to_string(), | ||||
| nsql::error_category().message(int(ec))); | nsql::error_category().message(int(ec))); | ||||
| const auto& [pkg_id, name, version, remote_url, description, repo_transform] | |||||
| = st.row() | |||||
| .unpack<std::int64_t, | |||||
| std::string, | |||||
| std::string, | |||||
| std::string, | |||||
| std::string, | |||||
| std::string>(); | |||||
| const auto& [pkg_id, name, version, remote_url, description] | |||||
| = st.row().unpack<std::int64_t, std::string, std::string, std::string, std::string>(); | |||||
| ec = st.step(std::nothrow); | ec = st.step(std::nothrow); | ||||
| if (ec == nsql::errc::row) { | if (ec == nsql::errc::row) { | ||||
| parse_remote_url(remote_url), | parse_remote_url(remote_url), | ||||
| }; | }; | ||||
| if (!repo_transform.empty()) { | |||||
| // Transforms are stored in the DB as JSON strings. Convert them back to real objects. | |||||
| auto tr_data = json5::parse_data(repo_transform); | |||||
| check_json(tr_data.is_array(), | |||||
| fmt::format("Database record for {} has an invalid 'repo_transform' field [1]", | |||||
| pkg_id)); | |||||
| for (const auto& el : tr_data.as_array()) { | |||||
| check_json( | |||||
| el.is_object(), | |||||
| fmt::format("Database record for {} has an invalid 'repo_transform' field [2]", | |||||
| pkg_id)); | |||||
| auto tr = fs_transformation::from_json(el); | |||||
| std::visit( | |||||
| [&](auto& remote) { | |||||
| if constexpr (neo::alike<decltype(remote), std::monostate>) { | |||||
| // Do nothing | |||||
| } else { | |||||
| remote.transforms.push_back(std::move(tr)); | |||||
| } | |||||
| }, | |||||
| info.remote); | |||||
| } | |||||
| } | |||||
| return info; | return info; | ||||
| } | } | ||||
| }) // | }) // | ||||
| | ranges::to_vector; | | ranges::to_vector; | ||||
| } | } | ||||
| void catalog::import_json_str(std::string_view content) { | |||||
| dds_log(trace, "Importing JSON string into catalog"); | |||||
| auto pkgs = parse_packages_json(content); | |||||
| nsql::transaction_guard tr{_db}; | |||||
| for (const auto& pkg : pkgs) { | |||||
| do_store_pkg(_db, _stmt_cache, pkg); | |||||
| } | |||||
| } |
| std::vector<package_id> by_name(std::string_view sv) const noexcept; | std::vector<package_id> by_name(std::string_view sv) const noexcept; | ||||
| std::vector<dependency> dependencies_of(const package_id& pkg) const noexcept; | std::vector<dependency> dependencies_of(const package_id& pkg) const noexcept; | ||||
| void import_json_str(std::string_view json_str); | |||||
| void import_json_file(path_ref json_path) { | |||||
| auto content = dds::slurp_file(json_path); | |||||
| import_json_str(content); | |||||
| } | |||||
| auto& database() noexcept { return _db; } | auto& database() noexcept { return _db; } | ||||
| auto& database() const noexcept { return _db; } | auto& database() const noexcept { return _db; } | ||||
| }; | }; |
| dds::package_id("foo", semver::version::parse("1.2.3")), | dds::package_id("foo", semver::version::parse("1.2.3")), | ||||
| {}, | {}, | ||||
| "example", | "example", | ||||
| dds::git_remote_listing{std::nullopt, {}, "git+http://example.com", "master"}, | |||||
| dds::git_remote_listing{std::nullopt, "git+http://example.com", "master"}, | |||||
| }); | }); | ||||
| auto pkgs = db.by_name("foo"); | auto pkgs = db.by_name("foo"); | ||||
| dds::package_id("foo", semver::version::parse("1.2.3")), | dds::package_id("foo", semver::version::parse("1.2.3")), | ||||
| {}, | {}, | ||||
| "example", | "example", | ||||
| dds::git_remote_listing{std::nullopt, {}, "git+http://example.com", "develop"}, | |||||
| dds::git_remote_listing{std::nullopt, "git+http://example.com", "develop"}, | |||||
| })); | })); | ||||
| // The previous pkg_id is still a valid lookup key | // The previous pkg_id is still a valid lookup key | ||||
| info = db.get(pkgs[0]); | info = db.get(pkgs[0]); | ||||
| {"baz", {semver::version::parse("5.3.0"), semver::version::parse("6.0.0")}}, | {"baz", {semver::version::parse("5.3.0"), semver::version::parse("6.0.0")}}, | ||||
| }, | }, | ||||
| "example", | "example", | ||||
| dds::git_remote_listing{std::nullopt, {}, "git+http://example.com", "master"}, | |||||
| dds::git_remote_listing{std::nullopt, "git+http://example.com", "master"}, | |||||
| }); | }); | ||||
| auto pkgs = db.by_name("foo"); | auto pkgs = db.by_name("foo"); | ||||
| REQUIRE(pkgs.size() == 1); | REQUIRE(pkgs.size() == 1); | ||||
| CHECK(deps[0].name == "bar"); | CHECK(deps[0].name == "bar"); | ||||
| CHECK(deps[1].name == "baz"); | CHECK(deps[1].name == "baz"); | ||||
| } | } | ||||
| TEST_CASE_METHOD(catalog_test_case, "Parse JSON repo") { | |||||
| db.import_json_str(R"({ | |||||
| "version": 2, | |||||
| "packages": { | |||||
| "foo": { | |||||
| "1.2.3": { | |||||
| "depends": [ | |||||
| "bar~4.2.1" | |||||
| ], | |||||
| url: "git+http://example.com#master" | |||||
| } | |||||
| } | |||||
| } | |||||
| })"); | |||||
| auto pkgs = db.by_name("foo"); | |||||
| REQUIRE(pkgs.size() == 1); | |||||
| CHECK(pkgs[0].name == "foo"); | |||||
| CHECK(pkgs[0].version == semver::version::parse("1.2.3")); | |||||
| auto deps = db.dependencies_of(pkgs[0]); | |||||
| REQUIRE(deps.size() == 1); | |||||
| CHECK(deps[0].name == "bar"); | |||||
| CHECK(deps[0].versions | |||||
| == dds::version_range_set{semver::version::parse("4.2.1"), | |||||
| semver::version::parse("4.3.0")}); | |||||
| } |
| auto tmpdir = dds::temporary_dir::create(); | auto tmpdir = dds::temporary_dir::create(); | ||||
| remote.pull_source(tmpdir.path()); | remote.pull_source(tmpdir.path()); | ||||
| remote.apply_transforms(tmpdir.path()); | |||||
| remote.generate_auto_lib_files(listing.ident, tmpdir.path()); | remote.generate_auto_lib_files(listing.ident, tmpdir.path()); | ||||
| dds_log(info, "Create sdist ..."); | dds_log(info, "Create sdist ..."); |
| #include "./import.hpp" | |||||
| #include <dds/error/errors.hpp> | |||||
| #include <dds/util/log.hpp> | |||||
| #include <fmt/core.h> | |||||
| #include <json5/parse_data.hpp> | |||||
| #include <neo/assert.hpp> | |||||
| #include <neo/url.hpp> | |||||
| #include <semester/walk.hpp> | |||||
| #include <optional> | |||||
| using namespace dds; | |||||
| template <typename KeyFunc, typename... Args> | |||||
| struct any_key { | |||||
| KeyFunc _key_fn; | |||||
| semester::walk_seq<Args...> _seq; | |||||
| any_key(KeyFunc&& kf, Args&&... args) | |||||
| : _key_fn(kf) | |||||
| , _seq(NEO_FWD(args)...) {} | |||||
| template <typename Data> | |||||
| semester::walk_result operator()(std::string_view key, Data&& dat) { | |||||
| auto res = _key_fn(key); | |||||
| if (res.rejected()) { | |||||
| return res; | |||||
| } | |||||
| return _seq.invoke(NEO_FWD(dat)); | |||||
| } | |||||
| }; | |||||
| template <typename KF, typename... Args> | |||||
| any_key(KF&&, Args&&...) -> any_key<KF, Args...>; | |||||
| namespace { | |||||
| using require_obj = semester::require_type<json5::data::mapping_type>; | |||||
| using require_array = semester::require_type<json5::data::array_type>; | |||||
| using require_str = semester::require_type<std::string>; | |||||
| template <typename... Args> | |||||
| [[noreturn]] void import_error(Args&&... args) { | |||||
| throw_user_error<dds::errc::invalid_catalog_json>(NEO_FWD(args)...); | |||||
| } | |||||
| auto make_dep = [](std::string const& str) { | |||||
| using namespace semester::walk_ops; | |||||
| try { | |||||
| return dependency::parse_depends_string(str); | |||||
| } catch (std::runtime_error const& e) { | |||||
| import_error(std::string(walk.path()) + e.what()); | |||||
| } | |||||
| }; | |||||
| auto convert_version_str = [](std::string_view str) { | |||||
| using namespace semester::walk_ops; | |||||
| try { | |||||
| return semver::version::parse(str); | |||||
| } catch (const semver::invalid_version& e) { | |||||
| import_error("{}: version string '{}' is invalid: {}", walk.path(), str, e.what()); | |||||
| } | |||||
| }; | |||||
| auto parse_remote = [](const std::string& str) { | |||||
| using namespace semester::walk_ops; | |||||
| try { | |||||
| return parse_remote_url(str); | |||||
| } catch (const neo::url_validation_error& e) { | |||||
| import_error("{}: Invalid URL [{}]: {}", walk.path(), str, e.what()); | |||||
| } catch (const user_error<errc::invalid_remote_url>& e) { | |||||
| import_error("{}: Invalid URL: {}", walk.path(), e.what()); | |||||
| } | |||||
| }; | |||||
| auto parse_fs_transforms = [](auto&& tr_vec) { | |||||
| using namespace semester::walk_ops; | |||||
| return walk_seq{ | |||||
| require_array{"Expect an array of transforms"}, | |||||
| for_each{ | |||||
| put_into(std::back_inserter(tr_vec), | |||||
| [&](auto&& dat) { | |||||
| try { | |||||
| return fs_transformation::from_json(dat); | |||||
| } catch (const semester::walk_error& e) { | |||||
| import_error(e.what()); | |||||
| } | |||||
| }), | |||||
| }, | |||||
| }; | |||||
| }; | |||||
| package_info | |||||
| parse_pkg_json_v2(std::string_view name, semver::version version, const json5::data& data) { | |||||
| package_info ret; | |||||
| ret.ident = package_id{std::string{name}, version}; | |||||
| std::vector<fs_transformation> fs_trs; | |||||
| using namespace semester::walk_ops; | |||||
| auto check_one_remote = [&](auto&&) { | |||||
| if (!semester::holds_alternative<std::monostate>(ret.remote)) { | |||||
| return walk.reject("Cannot specify multiple remotes for a package"); | |||||
| } | |||||
| return walk.pass; | |||||
| }; | |||||
| walk(data, | |||||
| mapping{if_key{"description", | |||||
| require_str{"'description' should be a string"}, | |||||
| put_into{ret.description}}, | |||||
| if_key{"depends", | |||||
| require_array{"'depends' must be an array of dependency strings"}, | |||||
| for_each{require_str{"Each dependency should be a string"}, | |||||
| put_into{std::back_inserter(ret.deps), make_dep}}}, | |||||
| if_key{ | |||||
| "url", | |||||
| require_str{"Remote URL should be a string"}, | |||||
| check_one_remote, | |||||
| put_into(ret.remote, parse_remote), | |||||
| }, | |||||
| if_key{"transform", parse_fs_transforms(fs_trs)}}); | |||||
| if (semester::holds_alternative<std::monostate>(ret.remote)) { | |||||
| import_error("{}: Package listing for {} does not have any remote information", | |||||
| walk.path(), | |||||
| ret.ident.to_string()); | |||||
| } | |||||
| if (semester::holds_alternative<git_remote_listing>(ret.remote)) { | |||||
| semester::get<git_remote_listing>(ret.remote).transforms = std::move(fs_trs); | |||||
| } else { | |||||
| if (!fs_trs.empty()) { | |||||
| throw_user_error<errc::invalid_catalog_json>( | |||||
| "{}: Filesystem transforms are not supported for this remote type", walk.path()); | |||||
| } | |||||
| } | |||||
| return ret; | |||||
| } | |||||
| std::vector<package_info> parse_json_v2(const json5::data& data) { | |||||
| std::vector<package_info> acc_pkgs; | |||||
| std::string pkg_name; | |||||
| semver::version pkg_version; | |||||
| package_info dummy; | |||||
| using namespace semester::walk_ops; | |||||
| auto convert_pkg_obj | |||||
| = [&](auto&& dat) { return parse_pkg_json_v2(pkg_name, pkg_version, dat); }; | |||||
| auto import_pkg_versions | |||||
| = walk_seq{require_obj{"Package entries must be JSON objects"}, | |||||
| mapping{any_key{put_into(pkg_version, convert_version_str), | |||||
| require_obj{"Package+version entries must be JSON"}, | |||||
| put_into{std::back_inserter(acc_pkgs), convert_pkg_obj}}}}; | |||||
| auto import_pkgs = walk_seq{require_obj{"'packages' should be a JSON object"}, | |||||
| mapping{any_key{put_into(pkg_name), import_pkg_versions}}}; | |||||
| walk(data, | |||||
| mapping{ | |||||
| if_key{"version", just_accept}, | |||||
| required_key{"packages", "'packages' should be an object of packages", import_pkgs}, | |||||
| }); | |||||
| return acc_pkgs; | |||||
| } | |||||
| } // namespace | |||||
| std::vector<package_info> dds::parse_packages_json(std::string_view content) { | |||||
| json5::data data; | |||||
| try { | |||||
| dds_log(trace, "Parsing packages JSON data: {}", content); | |||||
| data = json5::parse_data(content); | |||||
| } catch (const json5::parse_error& e) { | |||||
| throw_user_error<errc::invalid_catalog_json>("JSON5 syntax error: {}", e.what()); | |||||
| } | |||||
| if (!data.is_object()) { | |||||
| throw_user_error<errc::invalid_catalog_json>("Root of import JSON must be a JSON object"); | |||||
| } | |||||
| auto& data_obj = data.as_object(); | |||||
| auto version_it = data_obj.find("version"); | |||||
| if (version_it == data_obj.end() || !version_it->second.is_number()) { | |||||
| throw_user_error<errc::invalid_catalog_json>( | |||||
| "Root JSON import requires a 'version' property"); | |||||
| } | |||||
| double version = version_it->second.as_number(); | |||||
| try { | |||||
| if (version == 1.0) { | |||||
| throw_user_error<errc::invalid_catalog_json>( | |||||
| "Support for catalog JSON v1 has been removed"); | |||||
| } else if (version == 2.0) { | |||||
| dds_log(trace, "Processing JSON data as v2 data"); | |||||
| return parse_json_v2(data); | |||||
| } else { | |||||
| throw_user_error<errc::invalid_catalog_json>("Unknown catalog JSON version '{}'", | |||||
| version); | |||||
| } | |||||
| } catch (const semester::walk_error& e) { | |||||
| throw_user_error<errc::invalid_catalog_json>(e.what()); | |||||
| } | |||||
| } |
| #include "./import.hpp" | |||||
| #include <dds/error/errors.hpp> | |||||
| #include <catch2/catch.hpp> | |||||
| TEST_CASE("An empty import is okay") { | |||||
| // An empty JSON with no packages in it | |||||
| auto pkgs = dds::parse_packages_json("{version: 2, packages: {}}"); | |||||
| CHECK(pkgs.empty()); | |||||
| } | |||||
| TEST_CASE("Valid/invalid package JSON5") { | |||||
| std::string_view bads[] = { | |||||
| // Invalid JSON: | |||||
| "", | |||||
| // Should be an object | |||||
| "[]", | |||||
| // Missing keys | |||||
| "{}", | |||||
| // Missing "packages" | |||||
| "{version: 2}", | |||||
| // Bad version | |||||
| "{version: 2.7, packages: {}}", | |||||
| "{version: [], packages: {}}", | |||||
| "{version: null, packages: {}}", | |||||
| // 'packages' should be an object | |||||
| "{version: 2, packages: []}", | |||||
| "{version: 2, packages: null}", | |||||
| "{version: 2, packages: 4}", | |||||
| "{version: 2, packages: 'lol'}", | |||||
| // Objects in 'packages' should be objects | |||||
| "{version:2, packages:{foo:null}}", | |||||
| "{version:2, packages:{foo:[]}}", | |||||
| "{version:2, packages:{foo:9}}", | |||||
| "{version:2, packages:{foo:'lol'}}", | |||||
| // Objects in 'packages' shuold have version strings | |||||
| "{version:2, packages:{foo:{'lol':{}}}}", | |||||
| "{version:2, packages:{foo:{'1.2':{}}}}", | |||||
| // No remote | |||||
| "{version:2, packages:{foo:{'1.2.3':{}}}}", | |||||
| // Bad empty URL | |||||
| "{version:2, packages:{foo:{'1.2.3':{url: ''}}}}", | |||||
| // Git URL must have a fragment | |||||
| "{version:2, packages:{foo:{'1.2.3':{url:'git+http://example.com'}}}}", | |||||
| // 'auto-lib' should be a usage string | |||||
| "{version:2, packages:{foo:{'1.2.3':{url:'git+http://example.com?lm=lol#1.0}}}}", | |||||
| // 'transform' should be an array | |||||
| R"( | |||||
| { | |||||
| version: 2, | |||||
| packages: {foo: {'1.2.3': { | |||||
| url: 'git+http://example.com#master, | |||||
| transform: 'lol hi' | |||||
| }}} | |||||
| } | |||||
| )", | |||||
| }; | |||||
| for (auto bad : bads) { | |||||
| INFO("Bad: " << bad); | |||||
| CHECK_THROWS_AS(dds::parse_packages_json(bad), | |||||
| dds::user_error<dds::errc::invalid_catalog_json>); | |||||
| } | |||||
| std::string_view goods[] = { | |||||
| // Basic empty: | |||||
| "{version:2, packages:{}}", | |||||
| // No versions for 'foo' is weird, but okay | |||||
| "{version:2, packages:{foo:{}}}", | |||||
| // Basic package with minimum info: | |||||
| "{version:2, packages:{foo:{'1.2.3':{url: 'git+http://example.com#master'}}}}", | |||||
| // Minimal auto-lib: | |||||
| "{version:2, packages:{foo:{'1.2.3':{url: 'git+http://example.com?lm=a/b#master'}}}}", | |||||
| // Empty transforms: | |||||
| R"( | |||||
| { | |||||
| version: 2, | |||||
| packages: {foo: {'1.2.3': { | |||||
| url: 'git+http://example.com#master', | |||||
| transform: [], | |||||
| }}} | |||||
| } | |||||
| )", | |||||
| // Basic transform: | |||||
| R"( | |||||
| { | |||||
| version: 2, | |||||
| packages: {foo: {'1.2.3': { | |||||
| url: 'git+http://example.com#master', | |||||
| transform: [{ | |||||
| copy: { | |||||
| from: 'here', | |||||
| to: 'there', | |||||
| include: [ | |||||
| "*.c", | |||||
| "*.cpp", | |||||
| "*.h", | |||||
| '*.txt' | |||||
| ] | |||||
| } | |||||
| }], | |||||
| }}} | |||||
| } | |||||
| )", | |||||
| }; | |||||
| for (auto good : goods) { | |||||
| INFO("Parse: " << good); | |||||
| CHECK_NOTHROW(dds::parse_packages_json(good)); | |||||
| } | |||||
| } | |||||
| TEST_CASE("Check a single object") { | |||||
| // An empty JSON with no packages in it | |||||
| auto pkgs = dds::parse_packages_json(R"({ | |||||
| version: 2, | |||||
| packages: { | |||||
| foo: { | |||||
| '1.2.3': { | |||||
| url: 'git+http://example.com?lm=a/b#master', | |||||
| } | |||||
| } | |||||
| } | |||||
| })"); | |||||
| REQUIRE(pkgs.size() == 1); | |||||
| CHECK(pkgs[0].ident.name == "foo"); | |||||
| CHECK(pkgs[0].ident.to_string() == "foo@1.2.3"); | |||||
| CHECK(std::holds_alternative<dds::git_remote_listing>(pkgs[0].remote)); | |||||
| auto git = std::get<dds::git_remote_listing>(pkgs[0].remote); | |||||
| CHECK(git.url == "http://example.com"); | |||||
| CHECK(git.ref == "master"); | |||||
| REQUIRE(git.auto_lib); | |||||
| CHECK(git.auto_lib->namespace_ == "a"); | |||||
| CHECK(git.auto_lib->name == "b"); | |||||
| } |
| #include <dds/deps.hpp> | #include <dds/deps.hpp> | ||||
| #include <dds/package/id.hpp> | #include <dds/package/id.hpp> | ||||
| #include <dds/util/fs_transform.hpp> | |||||
| #include <dds/util/glob.hpp> | #include <dds/util/glob.hpp> | ||||
| #include <optional> | #include <optional> |
| using namespace dds; | using namespace dds; | ||||
| void remote_listing_base::apply_transforms(path_ref root) const { | |||||
| for (const auto& tr : transforms) { | |||||
| tr.apply_to(root); | |||||
| } | |||||
| } | |||||
| void remote_listing_base::generate_auto_lib_files(const package_id& pid, path_ref root) const { | void remote_listing_base::generate_auto_lib_files(const package_id& pid, path_ref root) const { | ||||
| if (auto_lib.has_value()) { | if (auto_lib.has_value()) { | ||||
| dds_log(info, "Generating library data automatically"); | dds_log(info, "Generating library data automatically"); |
| #pragma once | #pragma once | ||||
| #include <dds/util/fs_transform.hpp> | |||||
| #include <libman/package.hpp> | #include <libman/package.hpp> | ||||
| #include <neo/concepts.hpp> | #include <neo/concepts.hpp> | ||||
| struct package_id; | struct package_id; | ||||
| struct remote_listing_base { | struct remote_listing_base { | ||||
| std::optional<lm::usage> auto_lib{}; | |||||
| std::vector<fs_transformation> transforms{}; | |||||
| std::optional<lm::usage> auto_lib{}; | |||||
| void apply_transforms(path_ref root) const; | |||||
| void generate_auto_lib_files(const package_id& pid, path_ref root) const; | void generate_auto_lib_files(const package_id& pid, path_ref root) const; | ||||
| }; | }; | ||||
| #include "./fs_transform.hpp" | |||||
| #include <dds/error/errors.hpp> | |||||
| #include <dds/util/fs.hpp> | |||||
| #include <range/v3/algorithm/any_of.hpp> | |||||
| #include <range/v3/distance.hpp> | |||||
| #include <range/v3/numeric/accumulate.hpp> | |||||
| #include <semester/walk.hpp> | |||||
| #include <nlohmann/json.hpp> | |||||
| #include <iostream> | |||||
| using namespace dds; | |||||
| using require_obj = semester::require_type<json5::data::mapping_type>; | |||||
| using require_array = semester::require_type<json5::data::array_type>; | |||||
| using require_str = semester::require_type<std::string>; | |||||
| dds::fs_transformation dds::fs_transformation::from_json(const json5::data& data) { | |||||
| fs_transformation ret; | |||||
| using namespace semester::walk_ops; | |||||
| auto prep_optional = [](auto& opt) { | |||||
| return [&](auto&&) { | |||||
| opt.emplace(); | |||||
| return walk.pass; | |||||
| }; | |||||
| }; | |||||
| auto str_to_path = [](std::string const& s) { | |||||
| auto p = fs::path(s); | |||||
| if (p.is_absolute()) { | |||||
| throw semester::walk_error(std::string(walk.path()) | |||||
| + ": Only relative paths are accepted"); | |||||
| } | |||||
| return p; | |||||
| }; | |||||
| auto get_strip_components = [](double d) { | |||||
| if (d != double(int(d)) || d < 0) { | |||||
| throw semester::walk_error(std::string(walk.path()) + ": " | |||||
| + "'strip-components' should be a positive whole number"); | |||||
| } | |||||
| return int(d); | |||||
| }; | |||||
| auto populate_globs = [&](std::vector<dds::glob>& globs) { | |||||
| return for_each{ | |||||
| require_str{"Include/exclude list should be a list of globs"}, | |||||
| put_into(std::back_inserter(globs), | |||||
| [](const std::string& glob) { | |||||
| try { | |||||
| return dds::glob::compile(glob); | |||||
| } catch (const std::runtime_error& e) { | |||||
| throw semester::walk_error{std::string(walk.path()) + ": " + e.what()}; | |||||
| } | |||||
| }), | |||||
| }; | |||||
| }; | |||||
| auto populate_reloc = [&](auto& op) { | |||||
| return [&](auto&& dat) { | |||||
| op.emplace(); | |||||
| return mapping{ | |||||
| required_key{"from", | |||||
| "a 'from' path is required", | |||||
| require_str{"'from' should be a path string"}, | |||||
| put_into(op->from, str_to_path)}, | |||||
| required_key{"to", | |||||
| "a 'to' path is required", | |||||
| require_str{"'to' should be a path string"}, | |||||
| put_into(op->to, str_to_path)}, | |||||
| if_key{"strip-components", | |||||
| require_type<double>{"'strip-components' should be an integer"}, | |||||
| put_into(op->strip_components, get_strip_components)}, | |||||
| if_key{"include", | |||||
| require_array{"'include' should be an array"}, | |||||
| populate_globs(op->include)}, | |||||
| if_key{"exclude", | |||||
| require_array{"'exclude' should be an array"}, | |||||
| populate_globs(op->exclude)}, | |||||
| }(dat); | |||||
| }; | |||||
| }; | |||||
| struct fs_transformation::edit pending_edit; | |||||
| fs_transformation::one_edit pending_edit_item; | |||||
| walk(data, | |||||
| require_obj{"Each transform must be a JSON object"}, | |||||
| mapping{ | |||||
| if_key{"copy", populate_reloc(ret.copy)}, | |||||
| if_key{"move", populate_reloc(ret.move)}, | |||||
| if_key{"remove", | |||||
| require_obj{"'remove' should be a JSON object"}, | |||||
| prep_optional(ret.remove), | |||||
| mapping{ | |||||
| required_key{"path", | |||||
| "'path' is required", | |||||
| require_str{"'path' should be a string path to remove"}, | |||||
| put_into(ret.remove->path, str_to_path)}, | |||||
| if_key{"only-matching", | |||||
| require_array{"'only-matching' should be an array of globs"}, | |||||
| populate_globs(ret.remove->only_matching)}, | |||||
| }}, | |||||
| if_key{"write", | |||||
| require_obj{"'write' should be a JSON object"}, | |||||
| prep_optional(ret.write), | |||||
| mapping{ | |||||
| required_key{"path", | |||||
| "'path' is required", | |||||
| require_str{"'path' should be a string path to write to"}, | |||||
| put_into(ret.write->path, str_to_path)}, | |||||
| required_key{"content", | |||||
| "'content' is required", | |||||
| require_str{"'content' must be a string"}, | |||||
| put_into(ret.write->content)}, | |||||
| }}, | |||||
| if_key{ | |||||
| "edit", | |||||
| require_obj{"'edit' should be a JSON object"}, | |||||
| prep_optional(ret.edit), | |||||
| mapping{ | |||||
| required_key{"path", | |||||
| "'path' is required", | |||||
| require_str{"'path' should be a string path"}, | |||||
| put_into(ret.edit->path, str_to_path)}, | |||||
| required_key{ | |||||
| "edits", | |||||
| "An 'edits' array is required", | |||||
| require_array{"'edits' should be an array"}, | |||||
| for_each{ | |||||
| require_obj{"Each edit should be a JSON object"}, | |||||
| [&](auto&&) { | |||||
| ret.edit->edits.emplace_back(); | |||||
| return walk.pass; | |||||
| }, | |||||
| [&](auto&& dat) { | |||||
| return mapping{ | |||||
| required_key{ | |||||
| "kind", | |||||
| "Edit 'kind' is required", | |||||
| require_str{"'kind' should be a string"}, | |||||
| [&](std::string s) { | |||||
| auto& ed = ret.edit->edits.back(); | |||||
| if (s == "delete") { | |||||
| ed.kind = ed.delete_; | |||||
| } else if (s == "insert") { | |||||
| ed.kind = ed.insert; | |||||
| } else { | |||||
| return walk.reject("Invalid edit kind"); | |||||
| } | |||||
| return walk.accept; | |||||
| }, | |||||
| }, | |||||
| required_key{ | |||||
| "line", | |||||
| "Edit 'line' number is required", | |||||
| require_type<double>{"'line' should be an integer"}, | |||||
| [&](double d) { | |||||
| ret.edit->edits.back().line = int(d); | |||||
| return walk.accept; | |||||
| }, | |||||
| }, | |||||
| if_key{ | |||||
| "content", | |||||
| require_str{"'content' should be a string"}, | |||||
| [&](std::string s) { | |||||
| ret.edit->edits.back().content = s; | |||||
| return walk.accept; | |||||
| }, | |||||
| }, | |||||
| }(dat); | |||||
| }, | |||||
| }, | |||||
| }, | |||||
| }, | |||||
| }, | |||||
| }); | |||||
| return ret; | |||||
| } | |||||
| namespace { | |||||
| bool matches_any(path_ref path, const std::vector<glob>& globs) { | |||||
| return std::any_of(globs.begin(), globs.end(), [&](auto&& gl) { return gl.match(path); }); | |||||
| } | |||||
| bool parent_dir_of(fs::path root, fs::path child) { | |||||
| auto root_str = (root += "/").lexically_normal().generic_string(); | |||||
| auto child_str = (child += "/").lexically_normal().generic_string(); | |||||
| return child_str.find(root_str) == 0; | |||||
| } | |||||
| void do_relocate(const dds::fs_transformation::copy_move_base& oper, | |||||
| dds::path_ref root, | |||||
| bool is_copy) { | |||||
| auto from = fs::weakly_canonical(root / oper.from); | |||||
| auto to = fs::weakly_canonical(root / oper.to); | |||||
| if (!parent_dir_of(root, from)) { | |||||
| throw_external_error<errc::invalid_repo_transform>( | |||||
| "Filesystem transformation attempts to copy/move a file/directory from outside of the " | |||||
| "root [{}] into the root [{}].", | |||||
| from.string(), | |||||
| root.string()); | |||||
| } | |||||
| if (!parent_dir_of(root, to)) { | |||||
| throw_external_error<errc::invalid_repo_transform>( | |||||
| "Filesystem transformation attempts to copy/move a file/directory [{}] to a " | |||||
| "destination outside of the restricted root [{}].", | |||||
| to.string(), | |||||
| root.string()); | |||||
| } | |||||
| if (!fs::exists(from)) { | |||||
| throw_external_error<errc::invalid_repo_transform>( | |||||
| "Filesystem transformation attempting to copy/move a non-existint file/directory [{}] " | |||||
| "to [{}].", | |||||
| from.string(), | |||||
| to.string()); | |||||
| } | |||||
| fs::create_directories(to.parent_path()); | |||||
| if (fs::is_regular_file(from)) { | |||||
| if (is_copy) { | |||||
| fs::copy_file(from, to, fs::copy_options::overwrite_existing); | |||||
| } else { | |||||
| safe_rename(from, to); | |||||
| } | |||||
| return; | |||||
| } | |||||
| for (auto item : fs::recursive_directory_iterator(from)) { | |||||
| auto relpath = fs::relative(item, from); | |||||
| auto matches_glob = [&](auto glob) { return glob.match(relpath.string()); }; | |||||
| auto included = oper.include.empty() || ranges::any_of(oper.include, matches_glob); | |||||
| auto excluded = ranges::any_of(oper.exclude, matches_glob); | |||||
| if (!included || excluded) { | |||||
| continue; | |||||
| } | |||||
| auto n_components = ranges::distance(relpath); | |||||
| if (n_components <= oper.strip_components) { | |||||
| continue; | |||||
| } | |||||
| auto it = relpath.begin(); | |||||
| std::advance(it, oper.strip_components); | |||||
| relpath = ranges::accumulate(it, relpath.end(), fs::path(), std::divides<>()); | |||||
| auto dest = to / relpath; | |||||
| fs::create_directories(dest.parent_path()); | |||||
| if (item.is_directory()) { | |||||
| fs::create_directories(dest); | |||||
| } else { | |||||
| if (is_copy) { | |||||
| fs::copy_file(item, dest, fs::copy_options::overwrite_existing); | |||||
| } else { | |||||
| safe_rename(item, dest); | |||||
| } | |||||
| } | |||||
| } | |||||
| } | |||||
| void do_remove(const struct fs_transformation::remove& oper, path_ref root) { | |||||
| auto from = fs::weakly_canonical(root / oper.path); | |||||
| if (!parent_dir_of(root, from)) { | |||||
| throw_external_error<errc::invalid_repo_transform>( | |||||
| "Filesystem transformation attempts to deletes files/directories outside of the " | |||||
| "root. Attempted to remove [{}]. Removal is restricted to [{}].", | |||||
| from.string(), | |||||
| root.string()); | |||||
| } | |||||
| if (!fs::exists(from)) { | |||||
| throw_external_error<errc::invalid_repo_transform>( | |||||
| "Filesystem transformation attempts to delete a non-existint file/directory [{}].", | |||||
| from.string()); | |||||
| } | |||||
| if (fs::is_directory(from)) { | |||||
| for (auto child : fs::recursive_directory_iterator{from}) { | |||||
| if (child.is_directory()) { | |||||
| continue; | |||||
| } | |||||
| auto relpath = child.path().lexically_proximate(from); | |||||
| if (!oper.only_matching.empty() && !matches_any(relpath, oper.only_matching)) { | |||||
| continue; | |||||
| } | |||||
| fs::remove_all(child); | |||||
| } | |||||
| } else { | |||||
| fs::remove_all(from); | |||||
| } | |||||
| } | |||||
| void do_write(const struct fs_transformation::write& oper, path_ref root) { | |||||
| auto dest = fs::weakly_canonical(root / oper.path); | |||||
| if (!parent_dir_of(root, dest)) { | |||||
| throw_external_error<errc::invalid_repo_transform>( | |||||
| "Filesystem transformation is trying to write outside of the root. Attempted to write " | |||||
| "to [{}]. Writing is restricted to [{}].", | |||||
| dest.string(), | |||||
| root.string()); | |||||
| } | |||||
| auto of = dds::open(dest, std::ios::binary | std::ios::out); | |||||
| of << oper.content; | |||||
| } | |||||
| void do_edit(path_ref filepath, const fs_transformation::one_edit& edit) { | |||||
| auto file = open(filepath, std::ios::in | std::ios::binary); | |||||
| file.exceptions(std::ios::badbit); | |||||
| std::string lines; | |||||
| std::string line; | |||||
| int line_n = 1; | |||||
| for (; std::getline(file, line, '\n'); ++line_n) { | |||||
| if (line_n != edit.line) { | |||||
| lines += line + "\n"; | |||||
| continue; | |||||
| } | |||||
| switch (edit.kind) { | |||||
| case edit.delete_: | |||||
| // Just delete the line. Ignore it. | |||||
| continue; | |||||
| case edit.insert: | |||||
| // Insert some new content | |||||
| lines += edit.content + "\n"; | |||||
| lines += line + "\n"; | |||||
| continue; | |||||
| } | |||||
| } | |||||
| file = open(filepath, std::ios::out | std::ios::binary); | |||||
| file << lines; | |||||
| } | |||||
| } // namespace | |||||
| void dds::fs_transformation::apply_to(dds::path_ref root_) const { | |||||
| auto root = fs::weakly_canonical(root_); | |||||
| if (copy) { | |||||
| do_relocate(*copy, root, true); | |||||
| } | |||||
| if (move) { | |||||
| do_relocate(*move, root, false); | |||||
| } | |||||
| if (remove) { | |||||
| do_remove(*remove, root); | |||||
| } | |||||
| if (write) { | |||||
| do_write(*write, root); | |||||
| } | |||||
| if (edit) { | |||||
| auto fpath = root / edit->path; | |||||
| if (!parent_dir_of(root, fpath)) { | |||||
| throw_external_error<errc::invalid_repo_transform>( | |||||
| "Filesystem transformation wants to edit a file outside of the root. Attempted to " | |||||
| "modify [{}]. Writing is restricted to [{}].", | |||||
| fpath.string(), | |||||
| root.string()); | |||||
| } | |||||
| for (auto&& ed : edit->edits) { | |||||
| do_edit(fpath, ed); | |||||
| } | |||||
| } | |||||
| } | |||||
| namespace { | |||||
| nlohmann::json reloc_as_json(const fs_transformation::copy_move_base& oper) { | |||||
| auto obj = nlohmann::json::object(); | |||||
| obj["from"] = oper.from.string(); | |||||
| obj["to"] = oper.to.string(); | |||||
| obj["strip-components"] = oper.strip_components; | |||||
| auto inc_list = nlohmann::json::array(); | |||||
| for (auto& inc : oper.include) { | |||||
| inc_list.push_back(inc.string()); | |||||
| } | |||||
| auto exc_list = nlohmann::json::array(); | |||||
| for (auto& exc : oper.exclude) { | |||||
| exc_list.push_back(exc.string()); | |||||
| } | |||||
| if (!inc_list.empty()) { | |||||
| obj["include"] = inc_list; | |||||
| } | |||||
| if (!exc_list.empty()) { | |||||
| obj["exclude"] = exc_list; | |||||
| } | |||||
| return obj; | |||||
| } | |||||
| } // namespace | |||||
| std::string fs_transformation::as_json() const noexcept { | |||||
| auto obj = nlohmann::json::object(); | |||||
| if (copy) { | |||||
| obj["copy"] = reloc_as_json(*copy); | |||||
| } | |||||
| if (move) { | |||||
| obj["move"] = reloc_as_json(*move); | |||||
| } | |||||
| if (remove) { | |||||
| auto rm = nlohmann::json::object(); | |||||
| rm["path"] = remove->path.string(); | |||||
| if (!remove->only_matching.empty()) { | |||||
| auto if_arr = nlohmann::json::array(); | |||||
| for (auto&& gl : remove->only_matching) { | |||||
| if_arr.push_back(gl.string()); | |||||
| } | |||||
| rm["only-matching"] = if_arr; | |||||
| } | |||||
| obj["remove"] = rm; | |||||
| } | |||||
| if (write) { | |||||
| auto wr = nlohmann::json::object(); | |||||
| wr["path"] = write->path.string(); | |||||
| wr["content"] = write->content; | |||||
| obj["write"] = wr; | |||||
| } | |||||
| if (edit) { | |||||
| auto ed = nlohmann::json::object(); | |||||
| ed["path"] = edit->path.string(); | |||||
| auto edits = nlohmann::json::array(); | |||||
| for (auto&& one : edit->edits) { | |||||
| auto one_ed = nlohmann::json::object(); | |||||
| one_ed["kind"] = one.kind == one.delete_ ? "delete" : "insert"; | |||||
| one_ed["line"] = one.line; | |||||
| one_ed["content"] = one.content; | |||||
| edits.push_back(std::move(one_ed)); | |||||
| } | |||||
| ed["edits"] = edits; | |||||
| obj["edit"] = ed; | |||||
| } | |||||
| return to_string(obj); | |||||
| } |
| #pragma once | |||||
| #include "./fs.hpp" | |||||
| #include "./glob.hpp" | |||||
| #include <json5/data.hpp> | |||||
| #include <optional> | |||||
| #include <variant> | |||||
| namespace dds { | |||||
| struct fs_transformation { | |||||
| struct copy_move_base { | |||||
| fs::path from; | |||||
| fs::path to; | |||||
| int strip_components = 0; | |||||
| std::vector<dds::glob> include; | |||||
| std::vector<dds::glob> exclude; | |||||
| }; | |||||
| struct copy : copy_move_base {}; | |||||
| struct move : copy_move_base {}; | |||||
| struct remove { | |||||
| fs::path path; | |||||
| std::vector<dds::glob> only_matching; | |||||
| }; | |||||
| struct write { | |||||
| fs::path path; | |||||
| std::string content; | |||||
| }; | |||||
| struct one_edit { | |||||
| int line = 0; | |||||
| std::string content; | |||||
| enum kind_t { | |||||
| delete_, | |||||
| insert, | |||||
| } kind | |||||
| = delete_; | |||||
| }; | |||||
| struct edit { | |||||
| fs::path path; | |||||
| std::vector<one_edit> edits; | |||||
| }; | |||||
| std::optional<struct copy> copy; | |||||
| std::optional<struct move> move; | |||||
| std::optional<struct remove> remove; | |||||
| std::optional<struct write> write; | |||||
| std::optional<struct edit> edit; | |||||
| void apply_to(path_ref root) const; | |||||
| static fs_transformation from_json(const json5::data&); | |||||
| std::string as_json() const noexcept; | |||||
| }; | |||||
| } // namespace dds |
| from pathlib import Path | from pathlib import Path | ||||
| sys.path.append(str(Path(__file__).absolute().parent.parent / 'tools')) | sys.path.append(str(Path(__file__).absolute().parent.parent / 'tools')) | ||||
| from .dds import DDS, DDSFixtureParams, scoped_dds, dds_fixture_conf, dds_fixture_conf_1 | |||||
| from .dds import DDS, DDSFixtureParams, scoped_dds, dds_fixture_conf, dds_fixture_conf_1 | |||||
| from .http import http_repo, RepoFixture |
| import json | import json | ||||
| from contextlib import contextmanager | |||||
| from tests import dds, DDS | |||||
| from tests.fileutil import ensure_dir | from tests.fileutil import ensure_dir | ||||
| import pytest | |||||
| def load_catalog(dds: DDS, data): | |||||
| dds.scope.enter_context(ensure_dir(dds.build_dir)) | |||||
| dds.catalog_create() | |||||
| json_path = dds.build_dir / 'catalog.json' | |||||
| dds.scope.enter_context( | |||||
| dds.set_contents(json_path, | |||||
| json.dumps(data).encode())) | |||||
| dds.catalog_import(json_path) | |||||
| def test_get(dds: DDS): | |||||
| load_catalog( | |||||
| dds, { | |||||
| 'version': 2, | |||||
| 'packages': { | |||||
| 'neo-sqlite3': { | |||||
| '0.3.0': { | |||||
| 'url': | |||||
| 'git+https://github.com/vector-of-bool/neo-sqlite3.git#0.3.0', | |||||
| }, | |||||
| }, | |||||
| }, | |||||
| }) | |||||
| from tests import dds, DDS | |||||
| from tests.http import RepoFixture | |||||
| def test_get(dds: DDS, http_repo: RepoFixture): | |||||
| http_repo.import_json_data({ | |||||
| 'version': 2, | |||||
| 'packages': { | |||||
| 'neo-sqlite3': { | |||||
| '0.3.0': { | |||||
| 'remote': { | |||||
| 'git': { | |||||
| 'url': 'https://github.com/vector-of-bool/neo-sqlite3.git', | |||||
| 'ref': '0.3.0', | |||||
| } | |||||
| } | |||||
| } | |||||
| } | |||||
| } | |||||
| }) | |||||
| dds.repo_add(http_repo.url) | |||||
| dds.catalog_get('neo-sqlite3@0.3.0') | dds.catalog_get('neo-sqlite3@0.3.0') | ||||
| assert (dds.scratch_dir / 'neo-sqlite3@0.3.0').is_dir() | assert (dds.scratch_dir / 'neo-sqlite3@0.3.0').is_dir() | ||||
| assert (dds.scratch_dir / 'neo-sqlite3@0.3.0/package.jsonc').is_file() | assert (dds.scratch_dir / 'neo-sqlite3@0.3.0/package.jsonc').is_file() | ||||
| def test_get_http(dds: DDS): | |||||
| load_catalog( | |||||
| dds, { | |||||
| 'version': 2, | |||||
| 'packages': { | |||||
| 'cmcstl2': { | |||||
| '2020.2.24': { | |||||
| 'url': | |||||
| 'https://github.com/CaseyCarter/cmcstl2/archive/684a96d527e4dc733897255c0177b784dc280980.tar.gz?dds_lm=cmc/stl2;', | |||||
| }, | |||||
| def test_get_http(dds: DDS, http_repo: RepoFixture): | |||||
| http_repo.import_json_data({ | |||||
| 'packages': { | |||||
| 'cmcstl2': { | |||||
| '2020.2.24': { | |||||
| 'remote': { | |||||
| 'http': { | |||||
| 'url': | |||||
| 'https://github.com/CaseyCarter/cmcstl2/archive/684a96d527e4dc733897255c0177b784dc280980.tar.gz?dds_lm=cmc/stl2;', | |||||
| }, | |||||
| 'auto-lib': 'cmc/stl2', | |||||
| } | |||||
| }, | }, | ||||
| }, | }, | ||||
| }) | |||||
| }, | |||||
| }) | |||||
| dds.scope.enter_context(ensure_dir(dds.source_root)) | |||||
| dds.repo_add(http_repo.url) | |||||
| dds.catalog_get('cmcstl2@2020.2.24') | dds.catalog_get('cmcstl2@2020.2.24') | ||||
| assert dds.scratch_dir.joinpath('cmcstl2@2020.2.24/include').is_dir() | assert dds.scratch_dir.joinpath('cmcstl2@2020.2.24/include').is_dir() |
| import json | |||||
| from pathlib import Path | |||||
| from functools import partial | |||||
| from concurrent.futures import ThreadPoolExecutor | |||||
| from http.server import SimpleHTTPRequestHandler, HTTPServer | |||||
| import time | |||||
| import pytest | |||||
| from tests import dds, DDS | |||||
| from tests.fileutil import ensure_dir | |||||
| class DirectoryServingHTTPRequestHandler(SimpleHTTPRequestHandler): | |||||
| def __init__(self, *args, **kwargs) -> None: | |||||
| self.dir = kwargs.pop('dir') | |||||
| super().__init__(*args, **kwargs) | |||||
| def translate_path(self, path) -> str: | |||||
| abspath = Path(super().translate_path(path)) | |||||
| relpath = abspath.relative_to(Path.cwd()) | |||||
| return self.dir / relpath | |||||
| def test_import_json(dds: DDS): | |||||
| dds.scope.enter_context(ensure_dir(dds.build_dir)) | |||||
| dds.catalog_create() | |||||
| json_fpath = dds.build_dir / 'data.json' | |||||
| import_data = { | |||||
| 'version': 2, | |||||
| 'packages': { | |||||
| 'foo': { | |||||
| '1.2.4': { | |||||
| 'url': 'git+http://example.com#master', | |||||
| 'depends': [], | |||||
| }, | |||||
| '1.2.5': { | |||||
| 'url': 'git+http://example.com#master', | |||||
| }, | |||||
| }, | |||||
| 'bar': { | |||||
| '1.5.1': { | |||||
| 'url': 'http://example.com/bar-1.5.2.tgz' | |||||
| }, | |||||
| } | |||||
| }, | |||||
| } | |||||
| dds.scope.enter_context(dds.set_contents(json_fpath, json.dumps(import_data).encode())) | |||||
| dds.catalog_import(json_fpath) | |||||
| @pytest.yield_fixture | |||||
| def http_import_server(): | |||||
| handler = partial(DirectoryServingHTTPRequestHandler, dir=Path.cwd() / 'data/http-test-1') | |||||
| addr = ('0.0.0.0', 8000) | |||||
| pool = ThreadPoolExecutor() | |||||
| with HTTPServer(addr, handler) as httpd: | |||||
| pool.submit(lambda: httpd.serve_forever(poll_interval=0.1)) | |||||
| try: | |||||
| yield | |||||
| finally: | |||||
| httpd.shutdown() | |||||
| @pytest.yield_fixture | |||||
| def http_repo_server(): | |||||
| handler = partial(DirectoryServingHTTPRequestHandler, dir=Path.cwd() / 'data/test-repo-1') | |||||
| addr = ('0.0.0.0', 4646) | |||||
| pool = ThreadPoolExecutor() | |||||
| with HTTPServer(addr, handler) as httpd: | |||||
| pool.submit(lambda: httpd.serve_forever(poll_interval=0.1)) | |||||
| try: | |||||
| yield 'http://localhost:4646' | |||||
| finally: | |||||
| httpd.shutdown() | |||||
| def test_repo_add(dds: DDS, http_repo_server): | |||||
| dds.repo_dir.mkdir(parents=True, exist_ok=True) | |||||
| dds.run([ | |||||
| 'repo', | |||||
| dds.repo_dir_arg, | |||||
| 'add', | |||||
| dds.catalog_path_arg, | |||||
| http_repo_server, | |||||
| '--update', | |||||
| ]) | |||||
| dds.build_deps(['neo-fun@0.6.0']) |
| import pytest | import pytest | ||||
| from tests import scoped_dds, DDSFixtureParams | from tests import scoped_dds, DDSFixtureParams | ||||
| from .http import * # Exposes the HTTP fixtures | |||||
| @pytest.fixture(scope='session') | @pytest.fixture(scope='session') |
| ]) | ]) | ||||
| def repo_add(self, url: str) -> None: | def repo_add(self, url: str) -> None: | ||||
| return self.run(['repo', 'add', url, '--update', self.catalog_path_arg]) | |||||
| self.run(['repo', 'add', url, '--update', self.catalog_path_arg]) | |||||
| def build(self, | def build(self, | ||||
| *, | *, | ||||
| self.scratch_dir.mkdir(parents=True, exist_ok=True) | self.scratch_dir.mkdir(parents=True, exist_ok=True) | ||||
| return self.run(['catalog', 'create', f'--catalog={self.catalog_path}'], cwd=self.test_dir) | return self.run(['catalog', 'create', f'--catalog={self.catalog_path}'], cwd=self.test_dir) | ||||
| def catalog_import(self, json_path: Path) -> subprocess.CompletedProcess: | |||||
| self.scratch_dir.mkdir(parents=True, exist_ok=True) | |||||
| return self.run([ | |||||
| 'catalog', | |||||
| 'import', | |||||
| f'--catalog={self.catalog_path}', | |||||
| f'--json={json_path}', | |||||
| ]) | |||||
| def catalog_get(self, req: str) -> subprocess.CompletedProcess: | def catalog_get(self, req: str) -> subprocess.CompletedProcess: | ||||
| return self.run([ | return self.run([ | ||||
| 'catalog', | 'catalog', |
| "packages": { | "packages": { | ||||
| "neo-fun": { | "neo-fun": { | ||||
| "0.3.0": { | "0.3.0": { | ||||
| "url": "git+https://github.com/vector-of-bool/neo-fun.git#0.3.0" | |||||
| "remote": { | |||||
| "git": { | |||||
| "url": "https://github.com/vector-of-bool/neo-fun.git", | |||||
| "ref": "0.3.0" | |||||
| } | |||||
| } | |||||
| } | } | ||||
| } | } | ||||
| } | } |
| from tests import dds, DDS | from tests import dds, DDS | ||||
| from tests.http import RepoFixture | |||||
| def test_build_deps_from_file(dds: DDS): | |||||
| def test_build_deps_from_file(dds: DDS, http_repo: RepoFixture): | |||||
| assert not dds.deps_build_dir.is_dir() | assert not dds.deps_build_dir.is_dir() | ||||
| dds.catalog_import(dds.source_root / 'catalog.json') | |||||
| http_repo.import_json_file(dds.source_root / 'catalog.json') | |||||
| dds.repo_add(http_repo.url) | |||||
| dds.build_deps(['-d', 'deps.json5']) | dds.build_deps(['-d', 'deps.json5']) | ||||
| assert (dds.deps_build_dir / 'neo-fun@0.3.0').is_dir() | assert (dds.deps_build_dir / 'neo-fun@0.3.0').is_dir() | ||||
| assert (dds.scratch_dir / 'INDEX.lmi').is_file() | assert (dds.scratch_dir / 'INDEX.lmi').is_file() | ||||
| assert (dds.deps_build_dir / '_libman/neo/fun.lml').is_file() | assert (dds.deps_build_dir / '_libman/neo/fun.lml').is_file() | ||||
| def test_build_deps_from_cmd(dds: DDS): | |||||
| def test_build_deps_from_cmd(dds: DDS, http_repo: RepoFixture): | |||||
| assert not dds.deps_build_dir.is_dir() | assert not dds.deps_build_dir.is_dir() | ||||
| dds.catalog_import(dds.source_root / 'catalog.json') | |||||
| http_repo.import_json_file(dds.source_root / 'catalog.json') | |||||
| dds.repo_add(http_repo.url) | |||||
| dds.build_deps(['neo-fun=0.3.0']) | dds.build_deps(['neo-fun=0.3.0']) | ||||
| assert (dds.deps_build_dir / 'neo-fun@0.3.0').is_dir() | assert (dds.deps_build_dir / 'neo-fun@0.3.0').is_dir() | ||||
| assert (dds.scratch_dir / 'INDEX.lmi').is_file() | assert (dds.scratch_dir / 'INDEX.lmi').is_file() | ||||
| assert (dds.deps_build_dir / '_libman/neo/fun.lml').is_file() | assert (dds.deps_build_dir / '_libman/neo/fun.lml').is_file() | ||||
| def test_multiple_deps(dds: DDS): | |||||
| def test_multiple_deps(dds: DDS, http_repo: RepoFixture): | |||||
| assert not dds.deps_build_dir.is_dir() | assert not dds.deps_build_dir.is_dir() | ||||
| dds.catalog_import(dds.source_root / 'catalog.json') | |||||
| http_repo.import_json_file(dds.source_root / 'catalog.json') | |||||
| dds.repo_add(http_repo.url) | |||||
| dds.build_deps(['neo-fun^0.2.0', 'neo-fun~0.3.0']) | dds.build_deps(['neo-fun^0.2.0', 'neo-fun~0.3.0']) | ||||
| assert (dds.deps_build_dir / 'neo-fun@0.3.0').is_dir() | assert (dds.deps_build_dir / 'neo-fun@0.3.0').is_dir() | ||||
| assert (dds.scratch_dir / 'INDEX.lmi').is_file() | assert (dds.scratch_dir / 'INDEX.lmi').is_file() |
| import subprocess | import subprocess | ||||
| from tests import DDS, DDSFixtureParams, dds_fixture_conf, dds_fixture_conf_1 | from tests import DDS, DDSFixtureParams, dds_fixture_conf, dds_fixture_conf_1 | ||||
| from tests.http import RepoFixture | |||||
| dds_conf = dds_fixture_conf( | dds_conf = dds_fixture_conf( | ||||
| DDSFixtureParams(ident='git-remote', subdir='git-remote'), | DDSFixtureParams(ident='git-remote', subdir='git-remote'), | ||||
| @dds_conf | @dds_conf | ||||
| def test_deps_build(dds: DDS): | |||||
| dds.catalog_import(dds.source_root / 'catalog.json') | |||||
| def test_deps_build(dds: DDS, http_repo: RepoFixture): | |||||
| http_repo.import_json_file(dds.source_root / 'catalog.json') | |||||
| dds.repo_add(http_repo.url) | |||||
| assert not dds.repo_dir.exists() | assert not dds.repo_dir.exists() | ||||
| dds.build() | dds.build() | ||||
| assert dds.repo_dir.exists(), '`Building` did not generate a repo directory' | assert dds.repo_dir.exists(), '`Building` did not generate a repo directory' | ||||
| @dds_fixture_conf_1('use-remote') | @dds_fixture_conf_1('use-remote') | ||||
| def test_use_nlohmann_json_remote(dds: DDS): | |||||
| dds.catalog_import(dds.source_root / 'catalog.json') | |||||
| def test_use_nlohmann_json_remote(dds: DDS, http_repo: RepoFixture): | |||||
| http_repo.import_json_file(dds.source_root / 'catalog.json') | |||||
| dds.repo_add(http_repo.url) | |||||
| dds.build(apps=True) | dds.build(apps=True) | ||||
| app_exe = dds.build_dir / f'app{dds.exe_suffix}' | app_exe = dds.build_dir / f'app{dds.exe_suffix}' |
| "packages": { | "packages": { | ||||
| "neo-fun": { | "neo-fun": { | ||||
| "0.3.2": { | "0.3.2": { | ||||
| "url": "git+https://github.com/vector-of-bool/neo-fun.git#0.3.2" | |||||
| "remote": { | |||||
| "git": { | |||||
| "url": "https://github.com/vector-of-bool/neo-fun.git", | |||||
| "ref": "0.3.2" | |||||
| } | |||||
| } | |||||
| } | } | ||||
| }, | }, | ||||
| "range-v3": { | "range-v3": { | ||||
| "0.9.1": { | "0.9.1": { | ||||
| "url": "git+https://github.com/ericniebler/range-v3.git?lm=Niebler/range-v3#0.9.1" | |||||
| "remote": { | |||||
| "auto-lib": "Niebler/range-v3", | |||||
| "git": { | |||||
| "url": "https://github.com/ericniebler/range-v3.git", | |||||
| "ref": "0.9.1" | |||||
| } | |||||
| } | |||||
| } | } | ||||
| } | } | ||||
| } | } |
| "packages": { | "packages": { | ||||
| "cryptopp": { | "cryptopp": { | ||||
| "8.2.0": { | "8.2.0": { | ||||
| "url": "git+https://github.com/weidai11/cryptopp.git?lm=cryptopp/cryptopp#CRYPTOPP_8_2_0", | |||||
| "transform": [ | |||||
| { | |||||
| "move": { | |||||
| "from": ".", | |||||
| "to": "src/cryptopp", | |||||
| "include": [ | |||||
| "*.c", | |||||
| "*.cpp", | |||||
| "*.h" | |||||
| ] | |||||
| "remote": { | |||||
| "git": { | |||||
| "url": "https://github.com/weidai11/cryptopp.git", | |||||
| "ref": "CRYPTOPP_8_2_0" | |||||
| }, | |||||
| "auto-lib": "cryptopp/cryptopp", | |||||
| "transform": [ | |||||
| { | |||||
| "move": { | |||||
| "from": ".", | |||||
| "to": "src/cryptopp", | |||||
| "include": [ | |||||
| "*.c", | |||||
| "*.cpp", | |||||
| "*.h" | |||||
| ] | |||||
| } | |||||
| } | } | ||||
| } | |||||
| ] | |||||
| ] | |||||
| } | |||||
| } | } | ||||
| } | } | ||||
| } | } |
| from tests import DDS | from tests import DDS | ||||
| from tests.http import RepoFixture | |||||
| import platform | import platform | ||||
| import pytest | import pytest | ||||
| from dds_ci import proc | from dds_ci import proc | ||||
| @pytest.mark.skipif( | |||||
| platform.system() == 'FreeBSD', | |||||
| reason='This one has trouble running on FreeBSD') | |||||
| def test_get_build_use_cryptopp(dds: DDS): | |||||
| dds.catalog_import(dds.source_root / 'catalog.json') | |||||
| @pytest.mark.skipif(platform.system() == 'FreeBSD', reason='This one has trouble running on FreeBSD') | |||||
| def test_get_build_use_cryptopp(dds: DDS, http_repo: RepoFixture): | |||||
| http_repo.import_json_file(dds.source_root / 'catalog.json') | |||||
| dds.repo_add(http_repo.url) | |||||
| tc_fname = 'gcc.tc.jsonc' if 'gcc' in dds.default_builtin_toolchain else 'msvc.tc.jsonc' | tc_fname = 'gcc.tc.jsonc' if 'gcc' in dds.default_builtin_toolchain else 'msvc.tc.jsonc' | ||||
| tc = str(dds.test_dir / tc_fname) | tc = str(dds.test_dir / tc_fname) | ||||
| dds.build(toolchain=tc) | dds.build(toolchain=tc) | ||||
| proc.check_run( | |||||
| (dds.build_dir / 'use-cryptopp').with_suffix(dds.exe_suffix)) | |||||
| proc.check_run((dds.build_dir / 'use-cryptopp').with_suffix(dds.exe_suffix)) |
| "packages": { | "packages": { | ||||
| "nlohmann-json": { | "nlohmann-json": { | ||||
| "3.7.1": { | "3.7.1": { | ||||
| "url": "git+https://github.com/vector-of-bool/json.git#dds/3.7.1", | |||||
| "remote": { | |||||
| "git": { | |||||
| "url": "https://github.com/vector-of-bool/json.git", | |||||
| "ref": "dds/3.7.1" | |||||
| } | |||||
| }, | |||||
| "depends": [] | "depends": [] | ||||
| } | } | ||||
| } | } |
| "packages": { | "packages": { | ||||
| "spdlog": { | "spdlog": { | ||||
| "1.4.2": { | "1.4.2": { | ||||
| "url": "git+https://github.com/gabime/spdlog.git?lm=spdlog/spdlog#v1.4.2", | |||||
| "depends": [] | |||||
| "remote": { | |||||
| "git": { | |||||
| "url": "https://github.com/gabime/spdlog.git", | |||||
| "ref": "v1.4.2" | |||||
| }, | |||||
| "auto-lib": "spdlog/spdlog" | |||||
| } | |||||
| } | } | ||||
| } | } | ||||
| } | } |
| from tests import DDS | from tests import DDS | ||||
| from tests.http import RepoFixture | |||||
| from dds_ci import proc | from dds_ci import proc | ||||
| def test_get_build_use_spdlog(dds: DDS): | |||||
| dds.catalog_import(dds.source_root / 'catalog.json') | |||||
| def test_get_build_use_spdlog(dds: DDS, http_repo: RepoFixture): | |||||
| http_repo.import_json_file(dds.source_root / 'catalog.json') | |||||
| dds.repo_add(http_repo.url) | |||||
| tc_fname = 'gcc.tc.jsonc' if 'gcc' in dds.default_builtin_toolchain else 'msvc.tc.jsonc' | tc_fname = 'gcc.tc.jsonc' if 'gcc' in dds.default_builtin_toolchain else 'msvc.tc.jsonc' | ||||
| tc = str(dds.test_dir / tc_fname) | tc = str(dds.test_dir / tc_fname) | ||||
| dds.build(toolchain=tc, apps=True) | dds.build(toolchain=tc, apps=True) |
| from pathlib import Path | |||||
| from contextlib import contextmanager | |||||
| import json | |||||
| from http.server import SimpleHTTPRequestHandler, HTTPServer | |||||
| from typing import NamedTuple | |||||
| from concurrent.futures import ThreadPoolExecutor | |||||
| from functools import partial | |||||
| import tempfile | |||||
| import sys | |||||
| import subprocess | |||||
| import pytest | |||||
| class DirectoryServingHTTPRequestHandler(SimpleHTTPRequestHandler): | |||||
| """ | |||||
| A simple HTTP request handler that simply serves files from a directory given to the constructor. | |||||
| """ | |||||
| def __init__(self, *args, **kwargs) -> None: | |||||
| self.dir = kwargs.pop('dir') | |||||
| super().__init__(*args, **kwargs) | |||||
| def translate_path(self, path) -> str: | |||||
| # Convert the given URL path to a path relative to the directory we are serving | |||||
| abspath = Path(super().translate_path(path)) | |||||
| relpath = abspath.relative_to(Path.cwd()) | |||||
| return self.dir / relpath | |||||
| class ServerInfo(NamedTuple): | |||||
| """ | |||||
| Information about an HTTP server fixture | |||||
| """ | |||||
| base_url: str | |||||
| root: Path | |||||
| @contextmanager | |||||
| def run_http_server(dirpath: Path, port: int): | |||||
| """ | |||||
| Context manager that spawns an HTTP server that serves thegiven directory on | |||||
| the given TCP port. | |||||
| """ | |||||
| handler = partial(DirectoryServingHTTPRequestHandler, dir=dirpath) | |||||
| addr = ('localhost', port) | |||||
| pool = ThreadPoolExecutor() | |||||
| with HTTPServer(addr, handler) as httpd: | |||||
| pool.submit(lambda: httpd.serve_forever(poll_interval=0.1)) | |||||
| try: | |||||
| yield ServerInfo(f'http://localhost:{port}', dirpath) | |||||
| finally: | |||||
| httpd.shutdown() | |||||
| @pytest.yield_fixture() | |||||
| def http_tmp_dir_server(tmp_path: Path, unused_tcp_port: int): | |||||
| """ | |||||
| Creates an HTTP server that serves the contents of a new | |||||
| temporary directory. | |||||
| """ | |||||
| with run_http_server(tmp_path, unused_tcp_port) as s: | |||||
| yield s | |||||
| class RepoFixture: | |||||
| """ | |||||
| A fixture handle to a dds HTTP repository, including a path and URL. | |||||
| """ | |||||
| def __init__(self, dds_exe: Path, info: ServerInfo) -> None: | |||||
| self.server = info | |||||
| self.url = info.base_url | |||||
| self.dds_exe = dds_exe | |||||
| def import_json_data(self, data) -> None: | |||||
| """ | |||||
| Import some packages into the repo for the given JSON data. Uses | |||||
| mkrepo.py | |||||
| """ | |||||
| with tempfile.NamedTemporaryFile() as f: | |||||
| f.write(json.dumps(data).encode()) | |||||
| f.flush() | |||||
| self.import_json_file(Path(f.name)) | |||||
| def import_json_file(self, fpath: Path) -> None: | |||||
| """ | |||||
| Import some package into the repo for the given JSON file. Uses mkrepo.py | |||||
| """ | |||||
| subprocess.check_call([ | |||||
| sys.executable, | |||||
| str(Path.cwd() / 'tools/mkrepo.py'), | |||||
| f'--dir={self.server.root}', | |||||
| f'--spec={fpath}', | |||||
| ]) | |||||
| @pytest.yield_fixture() | |||||
| def http_repo(dds_exe: Path, http_tmp_dir_server: ServerInfo): | |||||
| """ | |||||
| Fixture that creates a new empty dds repository and an HTTP server to serve | |||||
| it. | |||||
| """ | |||||
| subprocess.check_call([dds_exe, 'repoman', 'init', str(http_tmp_dir_server.root)]) | |||||
| yield RepoFixture(dds_exe, http_tmp_dir_server) |
| from pathlib import Path | from pathlib import Path | ||||
| import sys | import sys | ||||
| import textwrap | import textwrap | ||||
| import requests | |||||
| from threading import local | |||||
| from concurrent.futures import ThreadPoolExecutor | from concurrent.futures import ThreadPoolExecutor | ||||
| class Git(NamedTuple): | class Git(NamedTuple): | ||||
| url: str | url: str | ||||
| ref: str | ref: str | ||||
| auto_lib: Optional[str] = None | |||||
| transforms: Sequence[FSTransform] = [] | |||||
| def to_dict(self) -> dict: | def to_dict(self) -> dict: | ||||
| d = { | d = { | ||||
| 'url': self.url, | 'url': self.url, | ||||
| 'ref': self.ref, | 'ref': self.ref, | ||||
| 'transform': [f.to_dict() for f in self.transforms], | |||||
| } | } | ||||
| if self.auto_lib: | |||||
| d['auto-lib'] = self.auto_lib | |||||
| return d | return d | ||||
| RemoteInfo = Union[Git] | RemoteInfo = Union[Git] | ||||
| class ForeignInfo(NamedTuple): | |||||
| remote: RemoteInfo | |||||
| auto_lib: Optional[str] = None | |||||
| transforms: Sequence[FSTransform] = [] | |||||
| def to_dict(self) -> dict: | |||||
| d = { | |||||
| 'transform': [tr.to_dict() for tr in self.transforms], | |||||
| } | |||||
| if isinstance(self.remote, Git): | |||||
| d['git'] = self.remote.to_dict() | |||||
| if self.auto_lib: | |||||
| d['auto-lib'] = self.auto_lib | |||||
| return d | |||||
| class Version(NamedTuple): | class Version(NamedTuple): | ||||
| version: str | version: str | ||||
| remote: RemoteInfo | |||||
| remote: ForeignInfo | |||||
| depends: Sequence[str] = [] | depends: Sequence[str] = [] | ||||
| description: str = '(No description provided)' | description: str = '(No description provided)' | ||||
| ret: dict = { | ret: dict = { | ||||
| 'description': self.description, | 'description': self.description, | ||||
| 'depends': list(self.depends), | 'depends': list(self.depends), | ||||
| 'remote': self.remote.to_dict(), | |||||
| } | } | ||||
| if isinstance(self.remote, Git): | |||||
| ret['git'] = self.remote.to_dict() | |||||
| return ret | return ret | ||||
| HTTP_POOL = ThreadPoolExecutor(10) | HTTP_POOL = ThreadPoolExecutor(10) | ||||
| HTTP_SESSION = requests.Session() | |||||
| def github_http_get(url: str): | def github_http_get(url: str): | ||||
| url_dat = url_parse.urlparse(url) | url_dat = url_parse.urlparse(url) | ||||
| req.add_header('Authorization', f'token {os.environ["GITHUB_API_TOKEN"]}') | req.add_header('Authorization', f'token {os.environ["GITHUB_API_TOKEN"]}') | ||||
| if url_dat.hostname != 'api.github.com': | if url_dat.hostname != 'api.github.com': | ||||
| raise RuntimeError(f'Request is outside of api.github.com [{url}]') | raise RuntimeError(f'Request is outside of api.github.com [{url}]') | ||||
| resp = request.urlopen(req) | |||||
| if resp.status != 200: | |||||
| raise RuntimeError(f'Request to [{url}] failed [{resp.status} {resp.reason}]') | |||||
| return json5.loads(resp.read()) | |||||
| print(f'Request {url}') | |||||
| resp = HTTP_SESSION.get(url, headers=req.headers) | |||||
| # resp = request.urlopen(req) | |||||
| resp.raise_for_status() | |||||
| # if resp.status != 200: | |||||
| # raise RuntimeError(f'Request to [{url}] failed [{resp.status} {resp.reason}]') | |||||
| return json5.loads(resp.text) | |||||
| def _get_github_tree_file_content(url: str) -> bytes: | def _get_github_tree_file_content(url: str) -> bytes: | ||||
| raise RuntimeError(f'Unknown "depends" object from json file: {depends!r}') | raise RuntimeError(f'Unknown "depends" object from json file: {depends!r}') | ||||
| remote = Git(url=clone_url, ref=tag['name']) | remote = Git(url=clone_url, ref=tag['name']) | ||||
| return Version(version, description=desc, depends=list(pairs), remote=remote) | |||||
| return Version(version, description=desc, depends=list(pairs), remote=ForeignInfo(remote)) | |||||
| def github_package(name: str, repo: str, want_tags: Iterable[str]) -> Package: | def github_package(name: str, repo: str, want_tags: Iterable[str]) -> Package: | ||||
| Version( | Version( | ||||
| ver.version, | ver.version, | ||||
| description=description, | description=description, | ||||
| remote=Git(git_url, tag_fmt.format(ver.version), auto_lib=auto_lib), | |||||
| remote=ForeignInfo(remote=Git(git_url, tag_fmt.format(ver.version)), auto_lib=auto_lib), | |||||
| depends=ver.depends) for ver in versions | depends=ver.depends) for ver in versions | ||||
| ]) | ]) | ||||
| Version( | Version( | ||||
| ver, | ver, | ||||
| description='\n'.join(textwrap.wrap(description)), | description='\n'.join(textwrap.wrap(description)), | ||||
| remote=Git(url=git_url, ref=tag_fmt.format(ver), auto_lib=auto_lib, transforms=transforms)) | |||||
| remote=ForeignInfo( | |||||
| remote=Git(url=git_url, ref=tag_fmt.format(ver)), auto_lib=auto_lib, transforms=transforms)) | |||||
| for ver in versions | for ver in versions | ||||
| ]) | ]) | ||||
| # yapf: disable | # yapf: disable | ||||
| PACKAGES = [ | PACKAGES = [ | ||||
| github_package('neo-buffer', 'vector-of-bool/neo-buffer', | |||||
| ['0.2.1', '0.3.0', '0.4.0', '0.4.1', '0.4.2']), | |||||
| github_package('neo-buffer', 'vector-of-bool/neo-buffer', ['0.2.1', '0.3.0', '0.4.0', '0.4.1', '0.4.2']), | |||||
| github_package('neo-compress', 'vector-of-bool/neo-compress', ['0.1.0', '0.1.1', '0.2.0']), | github_package('neo-compress', 'vector-of-bool/neo-compress', ['0.1.0', '0.1.1', '0.2.0']), | ||||
| github_package('neo-url', 'vector-of-bool/neo-url', | |||||
| ['0.1.0', '0.1.1', '0.1.2', '0.2.0', '0.2.1', '0.2.2']), | |||||
| github_package('neo-sqlite3', 'vector-of-bool/neo-sqlite3', | |||||
| ['0.2.3', '0.3.0', '0.4.0', '0.4.1']), | |||||
| github_package('neo-url', 'vector-of-bool/neo-url', ['0.1.0', '0.1.1', '0.1.2', '0.2.0', '0.2.1', '0.2.2']), | |||||
| github_package('neo-sqlite3', 'vector-of-bool/neo-sqlite3', ['0.2.3', '0.3.0', '0.4.0', '0.4.1']), | |||||
| github_package('neo-fun', 'vector-of-bool/neo-fun', [ | github_package('neo-fun', 'vector-of-bool/neo-fun', [ | ||||
| '0.1.1', '0.2.0', '0.2.1', '0.3.0', '0.3.1', '0.3.2', '0.4.0', '0.4.1', | |||||
| '0.4.2', '0.5.0', '0.5.1', '0.5.2', '0.5.3', '0.5.4', '0.5.5', '0.6.0', | |||||
| '0.1.1', | |||||
| '0.2.0', | |||||
| '0.2.1', | |||||
| '0.3.0', | |||||
| '0.3.1', | |||||
| '0.3.2', | |||||
| '0.4.0', | |||||
| '0.4.1', | |||||
| '0.4.2', | |||||
| '0.5.0', | |||||
| '0.5.1', | |||||
| '0.5.2', | |||||
| '0.5.3', | |||||
| '0.5.4', | |||||
| '0.5.5', | |||||
| '0.6.0', | |||||
| ]), | ]), | ||||
| github_package('neo-io', 'vector-of-bool/neo-io', ['0.1.0', '0.1.1']), | github_package('neo-io', 'vector-of-bool/neo-io', ['0.1.0', '0.1.1']), | ||||
| github_package('neo-http', 'vector-of-bool/neo-http', ['0.1.0']), | github_package('neo-http', 'vector-of-bool/neo-http', ['0.1.0']), | ||||
| github_package('semver', 'vector-of-bool/semver', ['0.2.2']), | github_package('semver', 'vector-of-bool/semver', ['0.2.2']), | ||||
| github_package('pubgrub', 'vector-of-bool/pubgrub', ['0.2.1']), | github_package('pubgrub', 'vector-of-bool/pubgrub', ['0.2.1']), | ||||
| github_package('vob-json5', 'vector-of-bool/json5', ['0.1.5']), | github_package('vob-json5', 'vector-of-bool/json5', ['0.1.5']), | ||||
| github_package('vob-semester', 'vector-of-bool/semester', | |||||
| ['0.1.0', '0.1.1', '0.2.0', '0.2.1', '0.2.2']), | |||||
| github_package('vob-semester', 'vector-of-bool/semester', ['0.1.0', '0.1.1', '0.2.0', '0.2.1', '0.2.2']), | |||||
| many_versions( | many_versions( | ||||
| 'magic_enum', | 'magic_enum', | ||||
| ( | ( | ||||
| ), | ), | ||||
| git_url='https://github.com/ericniebler/range-v3.git', | git_url='https://github.com/ericniebler/range-v3.git', | ||||
| auto_lib='range-v3/range-v3', | auto_lib='range-v3/range-v3', | ||||
| description= | |||||
| 'Range library for C++14/17/20, basis for C++20\'s std::ranges', | |||||
| description='Range library for C++14/17/20, basis for C++20\'s std::ranges', | |||||
| ), | ), | ||||
| many_versions( | many_versions( | ||||
| 'nlohmann-json', | 'nlohmann-json', | ||||
| ), | ), | ||||
| Package('ms-wil', [ | Package('ms-wil', [ | ||||
| Version( | Version( | ||||
| '2020.03.16', | |||||
| '2020.3.16', | |||||
| description='The Windows Implementation Library', | description='The Windows Implementation Library', | ||||
| remote=Git('https://github.com/vector-of-bool/wil.git', | |||||
| 'dds/2020.03.16')) | |||||
| remote=ForeignInfo(Git('https://github.com/vector-of-bool/wil.git', 'dds/2020.03.16'))) | |||||
| ]), | |||||
| Package('p-ranav.argparse', [ | |||||
| Version( | |||||
| '2.1.0', | |||||
| description='Argument Parser for Modern C++', | |||||
| remote=ForeignInfo(Git('https://github.com/p-ranav/argparse.git', 'v2.1'), auto_lib='p-ranav/argparse')) | |||||
| ]), | ]), | ||||
| many_versions( | many_versions( | ||||
| 'ctre', | 'ctre', | ||||
| '2.8.3', | '2.8.3', | ||||
| '2.8.4', | '2.8.4', | ||||
| ), | ), | ||||
| git_url= | |||||
| 'https://github.com/hanickadot/compile-time-regular-expressions.git', | |||||
| git_url='https://github.com/hanickadot/compile-time-regular-expressions.git', | |||||
| tag_fmt='v{}', | tag_fmt='v{}', | ||||
| auto_lib='hanickadot/ctre', | auto_lib='hanickadot/ctre', | ||||
| description= | |||||
| 'A compile-time PCRE (almost) compatible regular expression matcher', | |||||
| description='A compile-time PCRE (almost) compatible regular expression matcher', | |||||
| ), | ), | ||||
| Package( | Package( | ||||
| 'spdlog', | 'spdlog', | ||||
| ver, | ver, | ||||
| description='Fast C++ logging library', | description='Fast C++ logging library', | ||||
| depends=['fmt+6.0.0'], | depends=['fmt+6.0.0'], | ||||
| remote=Git( | |||||
| url='https://github.com/gabime/spdlog.git', | |||||
| ref=f'v{ver}', | |||||
| remote=ForeignInfo( | |||||
| Git(url='https://github.com/gabime/spdlog.git', ref=f'v{ver}'), | |||||
| transforms=[ | transforms=[ | ||||
| FSTransform( | FSTransform( | ||||
| write=WriteTransform( | write=WriteTransform( | ||||
| }))), | }))), | ||||
| FSTransform( | FSTransform( | ||||
| write=WriteTransform( | write=WriteTransform( | ||||
| path='library.json', | |||||
| content=json.dumps({ | |||||
| path='library.json', content=json.dumps({ | |||||
| 'name': 'spdlog', | 'name': 'spdlog', | ||||
| 'uses': ['fmt/fmt'] | 'uses': ['fmt/fmt'] | ||||
| }))), | }))), | ||||
| Version( | Version( | ||||
| '2.12.4', | '2.12.4', | ||||
| description='A modern C++ unit testing library', | description='A modern C++ unit testing library', | ||||
| remote=Git( | |||||
| 'https://github.com/catchorg/Catch2.git', | |||||
| 'v2.12.4', | |||||
| remote=ForeignInfo( | |||||
| Git('https://github.com/catchorg/Catch2.git', 'v2.12.4'), | |||||
| auto_lib='catch2/catch2', | auto_lib='catch2/catch2', | ||||
| transforms=[ | transforms=[ | ||||
| FSTransform( | |||||
| move=CopyMoveTransform( | |||||
| frm='include', to='include/catch2')), | |||||
| FSTransform(move=CopyMoveTransform(frm='include', to='include/catch2')), | |||||
| FSTransform( | FSTransform( | ||||
| copy=CopyMoveTransform(frm='include', to='src'), | copy=CopyMoveTransform(frm='include', to='src'), | ||||
| write=WriteTransform( | write=WriteTransform( | ||||
| Version( | Version( | ||||
| ver, | ver, | ||||
| description='Asio asynchronous I/O C++ library', | description='Asio asynchronous I/O C++ library', | ||||
| remote=Git( | |||||
| 'https://github.com/chriskohlhoff/asio.git', | |||||
| f'asio-{ver.replace(".", "-")}', | |||||
| remote=ForeignInfo( | |||||
| Git('https://github.com/chriskohlhoff/asio.git', f'asio-{ver.replace(".", "-")}'), | |||||
| auto_lib='asio/asio', | auto_lib='asio/asio', | ||||
| transforms=[ | transforms=[ | ||||
| FSTransform( | FSTransform( | ||||
| edit=EditTransform( | edit=EditTransform( | ||||
| path='include/asio/detail/config.hpp', | path='include/asio/detail/config.hpp', | ||||
| edits=[ | edits=[ | ||||
| OneEdit( | |||||
| line=13, | |||||
| kind='insert', | |||||
| content='#define ASIO_STANDALONE 1'), | |||||
| OneEdit( | |||||
| line=14, | |||||
| kind='insert', | |||||
| content= | |||||
| '#define ASIO_SEPARATE_COMPILATION 1') | |||||
| OneEdit(line=13, kind='insert', content='#define ASIO_STANDALONE 1'), | |||||
| OneEdit(line=14, kind='insert', content='#define ASIO_SEPARATE_COMPILATION 1') | |||||
| ]), | ]), | ||||
| ), | ), | ||||
| ]), | ]), | ||||
| Version( | Version( | ||||
| ver, | ver, | ||||
| description='Abseil Common Libraries', | description='Abseil Common Libraries', | ||||
| remote=Git( | |||||
| 'https://github.com/abseil/abseil-cpp.git', | |||||
| tag, | |||||
| remote=ForeignInfo( | |||||
| Git('https://github.com/abseil/abseil-cpp.git', tag), | |||||
| auto_lib='abseil/abseil', | auto_lib='abseil/abseil', | ||||
| transforms=[ | transforms=[ | ||||
| FSTransform( | FSTransform( | ||||
| Package('zlib', [ | Package('zlib', [ | ||||
| Version( | Version( | ||||
| ver, | ver, | ||||
| description= | |||||
| 'A massively spiffy yet delicately unobtrusive compression library', | |||||
| remote=Git( | |||||
| 'https://github.com/madler/zlib.git', | |||||
| tag or f'v{ver}', | |||||
| description='A massively spiffy yet delicately unobtrusive compression library', | |||||
| remote=ForeignInfo( | |||||
| Git('https://github.com/madler/zlib.git', tag or f'v{ver}'), | |||||
| auto_lib='zlib/zlib', | auto_lib='zlib/zlib', | ||||
| transforms=[ | transforms=[ | ||||
| FSTransform( | |||||
| move=CopyMoveTransform( | |||||
| frm='.', | |||||
| to='src/', | |||||
| include=[ | |||||
| '*.c', | |||||
| '*.h', | |||||
| ], | |||||
| )), | |||||
| FSTransform( | |||||
| move=CopyMoveTransform( | |||||
| frm='src/', | |||||
| to='include/', | |||||
| include=['zlib.h', 'zconf.h'], | |||||
| )), | |||||
| FSTransform(move=CopyMoveTransform( | |||||
| frm='.', | |||||
| to='src/', | |||||
| include=[ | |||||
| '*.c', | |||||
| '*.h', | |||||
| ], | |||||
| )), | |||||
| FSTransform(move=CopyMoveTransform( | |||||
| frm='src/', | |||||
| to='include/', | |||||
| include=['zlib.h', 'zconf.h'], | |||||
| )), | |||||
| ]), | ]), | ||||
| ) for ver, tag in [ | ) for ver, tag in [ | ||||
| ('1.2.11', None), | ('1.2.11', None), | ||||
| Package('sol2', [ | Package('sol2', [ | ||||
| Version( | Version( | ||||
| ver, | ver, | ||||
| description= | |||||
| 'A C++ <-> Lua API wrapper with advanced features and top notch performance', | |||||
| description='A C++ <-> Lua API wrapper with advanced features and top notch performance', | |||||
| depends=['lua+0.0.0'], | depends=['lua+0.0.0'], | ||||
| remote=Git( | |||||
| 'https://github.com/ThePhD/sol2.git', | |||||
| f'v{ver}', | |||||
| remote=ForeignInfo( | |||||
| Git('https://github.com/ThePhD/sol2.git', f'v{ver}'), | |||||
| transforms=[ | transforms=[ | ||||
| FSTransform( | FSTransform( | ||||
| write=WriteTransform( | write=WriteTransform( | ||||
| }, | }, | ||||
| indent=2, | indent=2, | ||||
| )), | )), | ||||
| move=(None | |||||
| if ver.startswith('3.') else CopyMoveTransform( | |||||
| frm='sol', | |||||
| to='src/sol', | |||||
| )), | |||||
| move=(None if ver.startswith('3.') else CopyMoveTransform( | |||||
| frm='sol', | |||||
| to='src/sol', | |||||
| )), | |||||
| ), | ), | ||||
| FSTransform( | FSTransform( | ||||
| write=WriteTransform( | write=WriteTransform( | ||||
| ver, | ver, | ||||
| description= | description= | ||||
| 'Lua is a powerful and fast programming language that is easy to learn and use and to embed into your application.', | 'Lua is a powerful and fast programming language that is easy to learn and use and to embed into your application.', | ||||
| remote=Git( | |||||
| 'https://github.com/lua/lua.git', | |||||
| f'v{ver}', | |||||
| remote=ForeignInfo( | |||||
| Git('https://github.com/lua/lua.git', f'v{ver}'), | |||||
| auto_lib='lua/lua', | auto_lib='lua/lua', | ||||
| transforms=[ | |||||
| FSTransform( | |||||
| move=CopyMoveTransform( | |||||
| frm='.', | |||||
| to='src/', | |||||
| include=['*.c', '*.h'], | |||||
| )) | |||||
| ]), | |||||
| transforms=[FSTransform(move=CopyMoveTransform( | |||||
| frm='.', | |||||
| to='src/', | |||||
| include=['*.c', '*.h'], | |||||
| ))]), | |||||
| ) for ver in [ | ) for ver in [ | ||||
| '5.4.0', | '5.4.0', | ||||
| '5.3.5', | '5.3.5', | ||||
| Version( | Version( | ||||
| ver, | ver, | ||||
| description='Parsing Expression Grammar Template Library', | description='Parsing Expression Grammar Template Library', | ||||
| remote=Git( | |||||
| 'https://github.com/taocpp/PEGTL.git', | |||||
| ver, | |||||
| remote=ForeignInfo( | |||||
| Git('https://github.com/taocpp/PEGTL.git', ver), | |||||
| auto_lib='tao/pegtl', | auto_lib='tao/pegtl', | ||||
| transforms=[FSTransform(remove=RemoveTransform(path='src/'))], | transforms=[FSTransform(remove=RemoveTransform(path='src/'))], | ||||
| )) for ver in [ | )) for ver in [ | ||||
| ] | ] | ||||
| ]), | ]), | ||||
| many_versions( | many_versions( | ||||
| 'boost.pfr', ['1.0.0', '1.0.1'], | |||||
| auto_lib='boost/pfr', | |||||
| git_url='https://github.com/apolukhin/magic_get.git'), | |||||
| 'boost.pfr', ['1.0.0', '1.0.1'], auto_lib='boost/pfr', git_url='https://github.com/apolukhin/magic_get.git'), | |||||
| many_versions( | many_versions( | ||||
| 'boost.leaf', | 'boost.leaf', | ||||
| [ | [ | ||||
| 'for encryption, decryption, signatures, password hashing and more.', | 'for encryption, decryption, signatures, password hashing and more.', | ||||
| transforms=[ | transforms=[ | ||||
| FSTransform( | FSTransform( | ||||
| move=CopyMoveTransform( | |||||
| frm='src/libsodium/include', to='include/'), | |||||
| move=CopyMoveTransform(frm='src/libsodium/include', to='include/'), | |||||
| edit=EditTransform( | edit=EditTransform( | ||||
| path='include/sodium/export.h', | path='include/sodium/export.h', | ||||
| edits=[ | |||||
| OneEdit( | |||||
| line=8, | |||||
| kind='insert', | |||||
| content='#define SODIUM_STATIC 1') | |||||
| ])), | |||||
| edits=[OneEdit(line=8, kind='insert', content='#define SODIUM_STATIC 1')])), | |||||
| FSTransform( | FSTransform( | ||||
| edit=EditTransform( | edit=EditTransform( | ||||
| path='include/sodium/private/common.h', | path='include/sodium/private/common.h', | ||||
| OneEdit( | OneEdit( | ||||
| kind='insert', | kind='insert', | ||||
| line=1, | line=1, | ||||
| content=Path(__file__).parent.joinpath( | |||||
| 'libsodium-config.h').read_text(), | |||||
| content=Path(__file__).parent.joinpath('libsodium-config.h').read_text(), | |||||
| ) | ) | ||||
| ])), | ])), | ||||
| FSTransform( | FSTransform( | ||||
| ), | ), | ||||
| remove=RemoveTransform(path='src/libsodium'), | remove=RemoveTransform(path='src/libsodium'), | ||||
| ), | ), | ||||
| FSTransform( | |||||
| copy=CopyMoveTransform( | |||||
| frm='include', to='src/', strip_components=1)), | |||||
| FSTransform(copy=CopyMoveTransform(frm='include', to='src/', strip_components=1)), | |||||
| ]), | ]), | ||||
| many_versions( | many_versions( | ||||
| 'tomlpp', | 'tomlpp', | ||||
| tag_fmt='v{}', | tag_fmt='v{}', | ||||
| git_url='https://github.com/marzer/tomlplusplus.git', | git_url='https://github.com/marzer/tomlplusplus.git', | ||||
| auto_lib='tomlpp/tomlpp', | auto_lib='tomlpp/tomlpp', | ||||
| description= | |||||
| 'Header-only TOML config file parser and serializer for modern C++'), | |||||
| description='Header-only TOML config file parser and serializer for modern C++'), | |||||
| Package('inja', [ | Package('inja', [ | ||||
| *(Version( | *(Version( | ||||
| ver, | ver, | ||||
| description='A Template Engine for Modern C++', | description='A Template Engine for Modern C++', | ||||
| remote=Git( | |||||
| 'https://github.com/pantor/inja.git', | |||||
| f'v{ver}', | |||||
| auto_lib='inja/inja')) for ver in ('1.0.0', '2.0.0', '2.0.1')), | |||||
| remote=ForeignInfo(Git('https://github.com/pantor/inja.git', f'v{ver}'), auto_lib='inja/inja')) | |||||
| for ver in ('1.0.0', '2.0.0', '2.0.1')), | |||||
| *(Version( | *(Version( | ||||
| ver, | ver, | ||||
| description='A Template Engine for Modern C++', | description='A Template Engine for Modern C++', | ||||
| depends=['nlohmann-json+0.0.0'], | depends=['nlohmann-json+0.0.0'], | ||||
| remote=Git( | |||||
| 'https://github.com/pantor/inja.git', | |||||
| f'v{ver}', | |||||
| remote=ForeignInfo( | |||||
| Git('https://github.com/pantor/inja.git', f'v{ver}'), | |||||
| transforms=[ | transforms=[ | ||||
| FSTransform( | FSTransform( | ||||
| write=WriteTransform( | write=WriteTransform( | ||||
| path='package.json', | path='package.json', | ||||
| content=json.dumps({ | content=json.dumps({ | ||||
| 'name': | |||||
| 'inja', | |||||
| 'namespace': | |||||
| 'inja', | |||||
| 'version': | |||||
| ver, | |||||
| 'name': 'inja', | |||||
| 'namespace': 'inja', | |||||
| 'version': ver, | |||||
| 'depends': [ | 'depends': [ | ||||
| 'nlohmann-json+0.0.0', | 'nlohmann-json+0.0.0', | ||||
| ] | ] | ||||
| }))), | }))), | ||||
| FSTransform( | FSTransform( | ||||
| write=WriteTransform( | write=WriteTransform( | ||||
| path='library.json', | |||||
| content=json.dumps({ | |||||
| path='library.json', content=json.dumps({ | |||||
| 'name': 'inja', | 'name': 'inja', | ||||
| 'uses': ['nlohmann/json'] | 'uses': ['nlohmann/json'] | ||||
| }))), | }))), | ||||
| ], | ], | ||||
| auto_lib='inja/inja', | |||||
| )) for ver in ('2.1.0', '2.2.0')), | )) for ver in ('2.1.0', '2.2.0')), | ||||
| ]), | ]), | ||||
| many_versions( | many_versions( | ||||
| Version( | Version( | ||||
| '0.98.1', | '0.98.1', | ||||
| description='PCG Randum Number Generation, C++ Edition', | description='PCG Randum Number Generation, C++ Edition', | ||||
| remote=Git( | |||||
| url='https://github.com/imneme/pcg-cpp.git', | |||||
| ref='v0.98.1', | |||||
| auto_lib='pcg/pcg-cpp')) | |||||
| remote=ForeignInfo(Git(url='https://github.com/imneme/pcg-cpp.git', ref='v0.98.1'), auto_lib='pcg/pcg-cpp')) | |||||
| ]), | ]), | ||||
| many_versions( | many_versions( | ||||
| 'hinnant-date', | 'hinnant-date', | ||||
| ['2.4.1', '3.0.0'], | ['2.4.1', '3.0.0'], | ||||
| description= | |||||
| 'A date and time library based on the C++11/14/17 <chrono> header', | |||||
| description='A date and time library based on the C++11/14/17 <chrono> header', | |||||
| auto_lib='hinnant/date', | auto_lib='hinnant/date', | ||||
| git_url='https://github.com/HowardHinnant/date.git', | git_url='https://github.com/HowardHinnant/date.git', | ||||
| tag_fmt='v{}', | tag_fmt='v{}', |
| """ | |||||
| Script for populating a repository with packages declaratively. | |||||
| """ | |||||
| import argparse | |||||
| import itertools | |||||
| import json | |||||
| import tarfile | |||||
| import re | |||||
| import shutil | |||||
| import sys | |||||
| import tempfile | |||||
| from concurrent.futures import ThreadPoolExecutor | |||||
| from contextlib import contextmanager | |||||
| from pathlib import Path | |||||
| from subprocess import check_call | |||||
| from threading import Lock | |||||
| from urllib import request | |||||
| from typing import (Any, Dict, Iterable, Iterator, NamedTuple, NoReturn, Optional, Sequence, Tuple, TypeVar, Type, | |||||
| Union) | |||||
| from semver import VersionInfo | |||||
| from typing_extensions import Protocol | |||||
| T = TypeVar('T') | |||||
| I32_MAX = 0xffff_ffff - 1 | |||||
| MAX_VERSION = VersionInfo(I32_MAX, I32_MAX, I32_MAX) | |||||
| class Dependency(NamedTuple): | |||||
| name: str | |||||
| low: VersionInfo | |||||
| high: VersionInfo | |||||
| @classmethod | |||||
| def parse(cls: Type[T], depstr: str) -> T: | |||||
| mat = re.match(r'(.+?)([\^~\+@])(.+?)$', depstr) | |||||
| if not mat: | |||||
| raise ValueError(f'Invalid dependency string "{depstr}"') | |||||
| name, kind, version_str = mat.groups() | |||||
| version = VersionInfo.parse(version_str) | |||||
| high = { | |||||
| '^': version.bump_major, | |||||
| '~': version.bump_minor, | |||||
| '@': version.bump_patch, | |||||
| '+': lambda: MAX_VERSION, | |||||
| }[kind]() | |||||
| return cls(name, version, high) | |||||
| def glob_if_exists(path: Path, pat: str) -> Iterable[Path]: | |||||
| try: | |||||
| yield from path.glob(pat) | |||||
| except FileNotFoundError: | |||||
| yield from () | |||||
| class MoveTransform(NamedTuple): | |||||
| frm: str | |||||
| to: str | |||||
| strip_components: int = 0 | |||||
| include: Sequence[str] = [] | |||||
| exclude: Sequence[str] = [] | |||||
| @classmethod | |||||
| def parse_data(cls: Type[T], data: Any) -> T: | |||||
| return cls( | |||||
| frm=data.pop('from'), | |||||
| to=data.pop('to'), | |||||
| include=data.pop('include', []), | |||||
| strip_components=data.pop('strip-components', 0), | |||||
| exclude=data.pop('exclude', [])) | |||||
| def apply_to(self, p: Path) -> None: | |||||
| src = p / self.frm | |||||
| dest = p / self.to | |||||
| if src.is_file(): | |||||
| self.do_reloc_file(src, dest) | |||||
| return | |||||
| inc_pats = self.include or ['**/*'] | |||||
| include = set(itertools.chain.from_iterable(glob_if_exists(src, pat) for pat in inc_pats)) | |||||
| exclude = set(itertools.chain.from_iterable(glob_if_exists(src, pat) for pat in self.exclude)) | |||||
| to_reloc = sorted(include - exclude) | |||||
| for source_file in to_reloc: | |||||
| relpath = source_file.relative_to(src) | |||||
| strip_relpath = Path('/'.join(relpath.parts[self.strip_components:])) | |||||
| dest_file = dest / strip_relpath | |||||
| self.do_reloc_file(source_file, dest_file) | |||||
| def do_reloc_file(self, src: Path, dest: Path) -> None: | |||||
| if src.is_dir(): | |||||
| dest.mkdir(exist_ok=True, parents=True) | |||||
| else: | |||||
| dest.parent.mkdir(exist_ok=True, parents=True) | |||||
| src.rename(dest) | |||||
| class CopyTransform(MoveTransform): | |||||
| def do_reloc_file(self, src: Path, dest: Path) -> None: | |||||
| if src.is_dir(): | |||||
| dest.mkdir(exist_ok=True, parents=True) | |||||
| else: | |||||
| shutil.copy2(src, dest) | |||||
| class OneEdit(NamedTuple): | |||||
| kind: str | |||||
| line: int | |||||
| content: Optional[str] = None | |||||
| @classmethod | |||||
| def parse_data(cls, data: Dict) -> 'OneEdit': | |||||
| return OneEdit(data.pop('kind'), data.pop('line'), data.pop('content', None)) | |||||
| def apply_to(self, fpath: Path) -> None: | |||||
| fn = { | |||||
| 'insert': self._insert, | |||||
| # 'delete': self._delete, | |||||
| }[self.kind] | |||||
| fn(fpath) | |||||
| def _insert(self, fpath: Path) -> None: | |||||
| content = fpath.read_bytes() | |||||
| lines = content.split(b'\n') | |||||
| assert self.content | |||||
| lines.insert(self.line, self.content.encode()) | |||||
| fpath.write_bytes(b'\n'.join(lines)) | |||||
| class EditTransform(NamedTuple): | |||||
| path: str | |||||
| edits: Sequence[OneEdit] = [] | |||||
| @classmethod | |||||
| def parse_data(cls, data: Dict) -> 'EditTransform': | |||||
| return EditTransform(data.pop('path'), [OneEdit.parse_data(ed) for ed in data.pop('edits')]) | |||||
| def apply_to(self, p: Path) -> None: | |||||
| fpath = p / self.path | |||||
| for ed in self.edits: | |||||
| ed.apply_to(fpath) | |||||
| class WriteTransform(NamedTuple): | |||||
| path: str | |||||
| content: str | |||||
| @classmethod | |||||
| def parse_data(self, data: Dict) -> 'WriteTransform': | |||||
| return WriteTransform(data.pop('path'), data.pop('content')) | |||||
| def apply_to(self, p: Path) -> None: | |||||
| fpath = p / self.path | |||||
| print('Writing to file', p, self.content) | |||||
| fpath.write_text(self.content) | |||||
| class RemoveTransform(NamedTuple): | |||||
| path: Path | |||||
| only_matching: Sequence[str] = () | |||||
| @classmethod | |||||
| def parse_data(self, d: Any) -> 'RemoveTransform': | |||||
| p = d.pop('path') | |||||
| pat = d.pop('only-matching') | |||||
| return RemoveTransform(Path(p), pat) | |||||
| def apply_to(self, p: Path) -> None: | |||||
| if p.is_dir(): | |||||
| self._apply_dir(p) | |||||
| else: | |||||
| p.unlink() | |||||
| def _apply_dir(self, p: Path) -> None: | |||||
| abspath = p / self.path | |||||
| if not self.only_matching: | |||||
| # Remove everything | |||||
| if abspath.is_dir(): | |||||
| shutil.rmtree(abspath) | |||||
| else: | |||||
| abspath.unlink() | |||||
| return | |||||
| for pat in self.only_matching: | |||||
| items = glob_if_exists(abspath, pat) | |||||
| for f in items: | |||||
| if f.is_dir(): | |||||
| shutil.rmtree(f) | |||||
| else: | |||||
| f.unlink() | |||||
| class FSTransform(NamedTuple): | |||||
| copy: Optional[CopyTransform] = None | |||||
| move: Optional[MoveTransform] = None | |||||
| remove: Optional[RemoveTransform] = None | |||||
| write: Optional[WriteTransform] = None | |||||
| edit: Optional[EditTransform] = None | |||||
| def apply_to(self, p: Path) -> None: | |||||
| for tr in (self.copy, self.move, self.remove, self.write, self.edit): | |||||
| if tr: | |||||
| tr.apply_to(p) | |||||
| @classmethod | |||||
| def parse_data(self, data: Any) -> 'FSTransform': | |||||
| move = data.pop('move', None) | |||||
| copy = data.pop('copy', None) | |||||
| remove = data.pop('remove', None) | |||||
| write = data.pop('write', None) | |||||
| edit = data.pop('edit', None) | |||||
| return FSTransform( | |||||
| copy=None if copy is None else CopyTransform.parse_data(copy), | |||||
| move=None if move is None else MoveTransform.parse_data(move), | |||||
| remove=None if remove is None else RemoveTransform.parse_data(remove), | |||||
| write=None if write is None else WriteTransform.parse_data(write), | |||||
| edit=None if edit is None else EditTransform.parse_data(edit), | |||||
| ) | |||||
| class HTTPRemoteSpec(NamedTuple): | |||||
| url: str | |||||
| transform: Sequence[FSTransform] | |||||
| @classmethod | |||||
| def parse_data(cls, data: Dict[str, Any]) -> 'HTTPRemoteSpec': | |||||
| url = data.pop('url') | |||||
| trs = [FSTransform.parse_data(tr) for tr in data.pop('transforms', [])] | |||||
| return HTTPRemoteSpec(url, trs) | |||||
| def make_local_dir(self): | |||||
| return http_dl_unpack(self.url) | |||||
| class GitSpec(NamedTuple): | |||||
| url: str | |||||
| ref: str | |||||
| transform: Sequence[FSTransform] | |||||
| @classmethod | |||||
| def parse_data(cls, data: Dict[str, Any]) -> 'GitSpec': | |||||
| ref = data.pop('ref') | |||||
| url = data.pop('url') | |||||
| trs = [FSTransform.parse_data(tr) for tr in data.pop('transform', [])] | |||||
| return GitSpec(url=url, ref=ref, transform=trs) | |||||
| @contextmanager | |||||
| def make_local_dir(self) -> Iterator[Path]: | |||||
| tdir = Path(tempfile.mkdtemp()) | |||||
| try: | |||||
| check_call(['git', 'clone', '--quiet', self.url, f'--depth=1', f'--branch={self.ref}', str(tdir)]) | |||||
| yield tdir | |||||
| finally: | |||||
| shutil.rmtree(tdir) | |||||
| class ForeignPackage(NamedTuple): | |||||
| remote: Union[HTTPRemoteSpec, GitSpec] | |||||
| transform: Sequence[FSTransform] | |||||
| auto_lib: Optional[Tuple] | |||||
| @classmethod | |||||
| def parse_data(cls, data: Dict[str, Any]) -> 'ForeignPackage': | |||||
| git = data.pop('git', None) | |||||
| http = data.pop('http', None) | |||||
| chosen = git or http | |||||
| assert chosen, data | |||||
| trs = data.pop('transform', []) | |||||
| al = data.pop('auto-lib', None) | |||||
| return ForeignPackage( | |||||
| remote=GitSpec.parse_data(git) if git else HTTPRemoteSpec.parse_data(http), | |||||
| transform=[FSTransform.parse_data(tr) for tr in trs], | |||||
| auto_lib=al.split('/') if al else None, | |||||
| ) | |||||
| @contextmanager | |||||
| def make_local_dir(self, name: str, ver: VersionInfo) -> Iterator[Path]: | |||||
| with self.remote.make_local_dir() as tdir: | |||||
| for tr in self.transform: | |||||
| tr.apply_to(tdir) | |||||
| if self.auto_lib: | |||||
| pkg_json = { | |||||
| 'name': name, | |||||
| 'version': str(ver), | |||||
| 'namespace': self.auto_lib[0], | |||||
| } | |||||
| lib_json = {'name': self.auto_lib[1]} | |||||
| tdir.joinpath('package.jsonc').write_text(json.dumps(pkg_json)) | |||||
| tdir.joinpath('library.jsonc').write_text(json.dumps(lib_json)) | |||||
| yield tdir | |||||
| class SpecPackage(NamedTuple): | |||||
| name: str | |||||
| version: VersionInfo | |||||
| depends: Sequence[Dependency] | |||||
| description: str | |||||
| remote: ForeignPackage | |||||
| @classmethod | |||||
| def parse_data(cls, name: str, version: str, data: Any) -> 'SpecPackage': | |||||
| deps = data.pop('depends', []) | |||||
| desc = data.pop('description', '[No description]') | |||||
| remote = ForeignPackage.parse_data(data.pop('remote')) | |||||
| return SpecPackage( | |||||
| name, | |||||
| VersionInfo.parse(version), | |||||
| description=desc, | |||||
| depends=[Dependency.parse(d) for d in deps], | |||||
| remote=remote) | |||||
| def iter_spec(path: Path) -> Iterable[SpecPackage]: | |||||
| data = json.loads(path.read_text()) | |||||
| pkgs = data['packages'] | |||||
| return iter_spec_packages(pkgs) | |||||
| def iter_spec_packages(data: Dict[str, Any]) -> Iterable[SpecPackage]: | |||||
| for name, versions in data.items(): | |||||
| for version, defin in versions.items(): | |||||
| yield SpecPackage.parse_data(name, version, defin) | |||||
| @contextmanager | |||||
| def http_dl_unpack(url: str) -> Iterator[Path]: | |||||
| req = request.urlopen(url) | |||||
| tdir = Path(tempfile.mkdtemp()) | |||||
| ofile = tdir / '.dl-archive' | |||||
| try: | |||||
| with ofile.open('wb') as fd: | |||||
| fd.write(req.read()) | |||||
| tf = tarfile.open(ofile) | |||||
| tf.extractall(tdir) | |||||
| tf.close() | |||||
| ofile.unlink() | |||||
| subdir = next(iter(Path(tdir).iterdir())) | |||||
| yield subdir | |||||
| finally: | |||||
| shutil.rmtree(tdir) | |||||
| @contextmanager | |||||
| def spec_as_local_tgz(spec: SpecPackage) -> Iterator[Path]: | |||||
| with spec.remote.make_local_dir(spec.name, spec.version) as clone_dir: | |||||
| out_tgz = clone_dir / 'sdist.tgz' | |||||
| check_call(['dds', 'sdist', 'create', f'--project-dir={clone_dir}', f'--out={out_tgz}']) | |||||
| yield out_tgz | |||||
| class Repository: | |||||
| def __init__(self, path: Path) -> None: | |||||
| self._path = path | |||||
| self._import_lock = Lock() | |||||
| @property | |||||
| def pkg_dir(self) -> Path: | |||||
| return self._path / 'pkg' | |||||
| @classmethod | |||||
| def create(cls, dirpath: Path, name: str) -> 'Repository': | |||||
| check_call(['dds', 'repoman', 'init', str(dirpath), f'--name={name}']) | |||||
| return Repository(dirpath) | |||||
| @classmethod | |||||
| def open(cls, dirpath: Path) -> 'Repository': | |||||
| return Repository(dirpath) | |||||
| def import_tgz(self, path: Path) -> None: | |||||
| check_call(['dds', 'repoman', 'import', str(self._path), str(path)]) | |||||
| def remove(self, name: str) -> None: | |||||
| check_call(['dds', 'repoman', 'remove', str(self._path), name]) | |||||
| def spec_import(self, spec: Path) -> None: | |||||
| all_specs = iter_spec(spec) | |||||
| want_import = (s for s in all_specs if self._shoule_import(s)) | |||||
| pool = ThreadPoolExecutor(10) | |||||
| futs = pool.map(self._get_and_import, want_import) | |||||
| for res in futs: | |||||
| pass | |||||
| def _shoule_import(self, spec: SpecPackage) -> bool: | |||||
| expect_file = self.pkg_dir / spec.name / str(spec.version) / 'sdist.tar.gz' | |||||
| return not expect_file.is_file() | |||||
| def _get_and_import(self, spec: SpecPackage) -> None: | |||||
| print(f'Import: {spec.name}@{spec.version}') | |||||
| with spec_as_local_tgz(spec) as tgz: | |||||
| with self._import_lock: | |||||
| self.import_tgz(tgz) | |||||
| class Arguments(Protocol): | |||||
| dir: Path | |||||
| spec: Path | |||||
| def main(argv: Sequence[str]) -> int: | |||||
| parser = argparse.ArgumentParser() | |||||
| parser.add_argument('--dir', '-d', help='Path to a repository to manage', required=True, type=Path) | |||||
| parser.add_argument( | |||||
| '--spec', | |||||
| metavar='<spec-path>', | |||||
| type=Path, | |||||
| required=True, | |||||
| help='Provide a JSON document specifying how to obtain an import some packages') | |||||
| args: Arguments = parser.parse_args(argv) | |||||
| repo = Repository.open(args.dir) | |||||
| repo.spec_import(args.spec) | |||||
| return 0 | |||||
| def start() -> NoReturn: | |||||
| sys.exit(main(sys.argv[1:])) | |||||
| if __name__ == "__main__": | |||||
| start() |