@@ -328,6 +328,10 @@ struct cli_catalog { | |||
} | |||
} | |||
void print_remote_info(std::monostate) { | |||
std::cout << "THIS ENTRY IS MISSING REMOTE INFORMATION!\n"; | |||
} | |||
int run() { | |||
auto pk_id = dds::package_id::parse(ident.Get()); | |||
auto cat = cat_path.open(); |
@@ -1,9 +1,12 @@ | |||
#include "./catalog.hpp" | |||
#include "./import.hpp" | |||
#include <dds/dym.hpp> | |||
#include <dds/error/errors.hpp> | |||
#include <dds/solve/solve.hpp> | |||
#include <neo/assert.hpp> | |||
#include <neo/sqlite3/exec.hpp> | |||
#include <neo/sqlite3/iter_tuples.hpp> | |||
#include <neo/sqlite3/single.hpp> | |||
@@ -131,96 +134,6 @@ std::vector<dds::glob> parse_glob_list(const nlohmann::json& data, std::string_v | |||
return ret; | |||
} | |||
std::optional<dds::repo_transform::copy_move> parse_copy_move_transform(nlohmann::json copy) { | |||
if (copy.is_null()) { | |||
return std::nullopt; | |||
} | |||
check_json(copy.is_object(), "'transform[.]/{copy,move}' must be an object"); | |||
auto from = copy["from"]; | |||
auto to = copy["to"]; | |||
check_json(from.is_string(), | |||
"'transform[.]/{copy,move}/from' must be present and must be a string"); | |||
check_json(to.is_string(), | |||
"'transform[.]/{copy,move}/to' must be present and must be a string"); | |||
dds::repo_transform::copy_move operation; | |||
operation.from = fs::path(std::string(from)); | |||
operation.to = fs::path(std::string(to)); | |||
if (operation.from.is_absolute()) { | |||
throw_user_error<errc::invalid_catalog_json>( | |||
"The 'from' filepath for a copy/move operation [{}] is an absolute path. These paths " | |||
"*must* be relative paths only.", | |||
operation.from.string()); | |||
} | |||
if (operation.to.is_absolute()) { | |||
throw_user_error<errc::invalid_catalog_json>( | |||
"The 'to' filepath for a copy/move operation [{}] is an absolute path. These paths " | |||
"*must* be relative paths only.", | |||
operation.to.string()); | |||
} | |||
operation.include = parse_glob_list(copy["include"], "transform[.]/{copy,move}/include"); | |||
operation.exclude = parse_glob_list(copy["exclude"], "transform[.]/{copy,move}/exclude"); | |||
auto strip_comps = copy["strip_components"]; | |||
if (!strip_comps.is_null()) { | |||
check_json(strip_comps.is_number() || int(strip_comps) < 0, | |||
"transform[.]/{copy,move}/strip_components must be a positive integer"); | |||
operation.strip_components = int(strip_comps); | |||
} | |||
return operation; | |||
} | |||
dds::repo_transform parse_transform(nlohmann::json data) { | |||
assert(data.is_object()); | |||
dds::repo_transform transform; | |||
transform.copy = parse_copy_move_transform(data["copy"]); | |||
transform.move = parse_copy_move_transform(data["move"]); | |||
return transform; | |||
} | |||
nlohmann::json transform_to_json(const dds::repo_transform::copy_move& tr) { | |||
auto obj = nlohmann::json::object(); | |||
obj["from"] = tr.from.string(); | |||
obj["to"] = tr.to.string(); | |||
obj["include"] = ranges::views::all(tr.include) | ranges::views::transform(&dds::glob::string); | |||
obj["exclude"] = ranges::views::all(tr.exclude) | ranges::views::transform(&dds::glob::string); | |||
return obj; | |||
} | |||
nlohmann::json transform_to_json(const struct dds::repo_transform::remove& rm) { | |||
auto obj = nlohmann::json::object(); | |||
obj["path"] = rm.path.string(); | |||
obj["only_matching"] | |||
= ranges::views::all(rm.only_matching) | ranges::views::transform(&dds::glob::string); | |||
return obj; | |||
} | |||
nlohmann::json transform_to_json(const dds::repo_transform& tr) { | |||
auto obj = nlohmann::json::object(); | |||
if (tr.copy) { | |||
obj["copy"] = transform_to_json(*tr.copy); | |||
} | |||
if (tr.move) { | |||
obj["move"] = transform_to_json(*tr.move); | |||
} | |||
if (tr.remove) { | |||
obj["remove"] = transform_to_json(*tr.remove); | |||
} | |||
return obj; | |||
} | |||
std::string transform_to_json(const std::vector<dds::repo_transform>& trs) { | |||
auto arr = nlohmann::json::array(); | |||
for (auto& tr : trs) { | |||
arr.push_back(transform_to_json(tr)); | |||
} | |||
return to_string(arr); | |||
} | |||
} // namespace | |||
catalog catalog::open(const std::string& db_path) { | |||
@@ -243,6 +156,15 @@ catalog catalog::open(const std::string& db_path) { | |||
catalog::catalog(sqlite3::database db) | |||
: _db(std::move(db)) {} | |||
void catalog::_store_pkg(const package_info& pkg, std::monostate) { | |||
neo_assert_always( | |||
invariant, | |||
false, | |||
"There was an attempt to insert a package listing into the database where that package " | |||
"listing does not have a remote listing. If you see this message, it is a dds bug.", | |||
pkg.ident.to_string()); | |||
} | |||
void catalog::_store_pkg(const package_info& pkg, const git_remote_listing& git) { | |||
auto lm_usage = git.auto_lib.value_or(lm::usage{}); | |||
sqlite3::exec( // | |||
@@ -275,8 +197,9 @@ void catalog::_store_pkg(const package_info& pkg, const git_remote_listing& git) | |||
git.ref, | |||
lm_usage.name, | |||
lm_usage.namespace_, | |||
pkg.description, | |||
transform_to_json(pkg.transforms))); | |||
pkg.description | |||
//, transform_to_json(pkg.transforms)) | |||
)); | |||
} | |||
void catalog::store(const package_info& pkg) { | |||
@@ -378,9 +301,10 @@ std::optional<package_info> catalog::get(const package_id& pk_id) const noexcept | |||
check_json(tr_json.is_array(), | |||
fmt::format("Database record for {} has an invalid 'repo_transform' field", | |||
pkg_id)); | |||
for (const auto& el : tr_json) { | |||
info.transforms.push_back(parse_transform(el)); | |||
} | |||
/// XXX: | |||
// for (const auto& el : tr_json) { | |||
// info.transforms.push_back(parse_transform(el)); | |||
// } | |||
} | |||
return info; | |||
} | |||
@@ -436,86 +360,10 @@ std::vector<dependency> catalog::dependencies_of(const package_id& pkg) const no | |||
} | |||
void catalog::import_json_str(std::string_view content) { | |||
using nlohmann::json; | |||
auto root = json::parse(content); | |||
check_json(root.is_object(), "Root of JSON must be an object (key-value mapping)"); | |||
auto version = root["version"]; | |||
check_json(version.is_number_integer(), "/version must be an integral value"); | |||
check_json(version <= 1, "/version is too new. We don't know how to parse this."); | |||
auto packages = root["packages"]; | |||
check_json(packages.is_object(), "/packages must be an object"); | |||
auto pkgs = parse_packages_json(content); | |||
sqlite3::transaction_guard tr{_db}; | |||
for (const auto& [pkg_name_, versions_map] : packages.items()) { | |||
std::string pkg_name = pkg_name_; | |||
check_json(versions_map.is_object(), | |||
fmt::format("/packages/{} must be an object", pkg_name)); | |||
for (const auto& [version_, pkg_info] : versions_map.items()) { | |||
auto version = semver::version::parse(version_); | |||
check_json(pkg_info.is_object(), | |||
fmt::format("/packages/{}/{} must be an object", pkg_name, version_)); | |||
package_info info{{pkg_name, version}, {}, {}, {}, {}}; | |||
auto deps = pkg_info["depends"]; | |||
if (!deps.is_null()) { | |||
check_json(deps.is_object(), | |||
fmt::format("/packages/{}/{}/depends must be an object", | |||
pkg_name, | |||
version_)); | |||
for (const auto& [dep_name, dep_version] : deps.items()) { | |||
check_json(dep_version.is_string(), | |||
fmt::format("/packages/{}/{}/depends/{} must be a string", | |||
pkg_name, | |||
version_, | |||
dep_name)); | |||
auto range = semver::range::parse(std::string(dep_version)); | |||
info.deps.push_back({ | |||
std::string(dep_name), | |||
{range.low(), range.high()}, | |||
}); | |||
} | |||
} | |||
auto git_remote = pkg_info["git"]; | |||
if (!git_remote.is_null()) { | |||
check_json(git_remote.is_object(), "`git` must be an object"); | |||
std::string url = git_remote["url"]; | |||
std::string ref = git_remote["ref"]; | |||
auto lm_usage = git_remote["auto-lib"]; | |||
std::optional<lm::usage> autolib; | |||
if (!lm_usage.is_null()) { | |||
autolib = lm::split_usage_string(std::string(lm_usage)); | |||
} | |||
info.remote = git_remote_listing{url, ref, autolib}; | |||
} else { | |||
throw_user_error<errc::no_catalog_remote_info>("No remote info for /packages/{}/{}", | |||
pkg_name, | |||
version_); | |||
} | |||
auto transforms = pkg_info["transform"]; | |||
if (!transforms.is_null()) { | |||
check_json(transforms.is_array(), "`transform` must be an array of objects"); | |||
for (nlohmann::json const& el : transforms) { | |||
check_json(el.is_object(), "Each element of `transform` must be an object"); | |||
info.transforms.emplace_back(parse_transform(el)); | |||
} | |||
} | |||
auto desc_ = pkg_info["description"]; | |||
if (!desc_.is_null()) { | |||
check_json(desc_.is_string(), "`description` must be a string"); | |||
info.description = desc_; | |||
} | |||
store(info); | |||
} | |||
for (const auto& pkg : pkgs) { | |||
store(pkg); | |||
} | |||
} |
@@ -1,11 +1,12 @@ | |||
#pragma once | |||
#include <dds/catalog/git.hpp> | |||
#include <dds/deps.hpp> | |||
#include <dds/package/id.hpp> | |||
#include <dds/util/fs.hpp> | |||
#include <dds/util/glob.hpp> | |||
#include "./package_info.hpp" | |||
#include <neo/sqlite3/database.hpp> | |||
#include <neo/sqlite3/statement.hpp> | |||
#include <neo/sqlite3/statement_cache.hpp> | |||
@@ -17,36 +18,6 @@ | |||
namespace dds { | |||
struct repo_transform { | |||
struct copy_move { | |||
fs::path from; | |||
fs::path to; | |||
int strip_components = 0; | |||
std::vector<dds::glob> include; | |||
std::vector<dds::glob> exclude; | |||
}; | |||
struct remove { | |||
fs::path path; | |||
std::vector<dds::glob> only_matching; | |||
}; | |||
std::optional<copy_move> copy; | |||
std::optional<copy_move> move; | |||
std::optional<remove> remove; | |||
}; | |||
struct package_info { | |||
package_id ident; | |||
std::vector<dependency> deps; | |||
std::string description; | |||
std::variant<git_remote_listing> remote; | |||
std::vector<repo_transform> transforms; | |||
}; | |||
class catalog { | |||
neo::sqlite3::database _db; | |||
mutable neo::sqlite3::statement_cache _stmt_cache{_db}; | |||
@@ -55,6 +26,7 @@ class catalog { | |||
catalog(const catalog&) = delete; | |||
void _store_pkg(const package_info&, const git_remote_listing&); | |||
void _store_pkg(const package_info&, std::monostate); | |||
public: | |||
catalog(catalog&&) = default; |
@@ -20,6 +20,7 @@ TEST_CASE_METHOD(catalog_test_case, "Store a simple package") { | |||
{}, | |||
"example", | |||
dds::git_remote_listing{"http://example.com", "master", std::nullopt}, | |||
{}, | |||
}); | |||
auto pkgs = db.by_name("foo"); | |||
@@ -39,6 +40,7 @@ TEST_CASE_METHOD(catalog_test_case, "Store a simple package") { | |||
{}, | |||
"example", | |||
dds::git_remote_listing{"http://example.com", "develop", std::nullopt}, | |||
{}, | |||
})); | |||
// The previous pkg_id is still a valid lookup key | |||
info = db.get(pkgs[0]); | |||
@@ -55,6 +57,7 @@ TEST_CASE_METHOD(catalog_test_case, "Package requirements") { | |||
}, | |||
"example", | |||
dds::git_remote_listing{"http://example.com", "master", std::nullopt}, | |||
{}, | |||
}); | |||
auto pkgs = db.by_name("foo"); | |||
REQUIRE(pkgs.size() == 1); |
@@ -2,8 +2,8 @@ | |||
#include <dds/catalog/catalog.hpp> | |||
#include <dds/error/errors.hpp> | |||
#include <dds/proc.hpp> | |||
#include <neo/assert.hpp> | |||
#include <nlohmann/json.hpp> | |||
#include <range/v3/algorithm/all_of.hpp> | |||
#include <range/v3/algorithm/any_of.hpp> | |||
@@ -15,129 +15,27 @@ using namespace dds; | |||
namespace { | |||
enum operation { move, copy }; | |||
void apply_copy(const dds::repo_transform::copy_move& copy, path_ref root, operation op) { | |||
auto copy_src = fs::weakly_canonical(root / copy.from); | |||
auto copy_dest = fs::weakly_canonical(root / copy.to); | |||
if (fs::relative(copy_src, root).generic_string().find("../") == 0) { | |||
throw std::runtime_error( | |||
fmt::format("A copy_src ends up copying from outside the root. (Relative path was " | |||
"[{}], resolved path was [{}])", | |||
copy.from.string(), | |||
copy_src.string())); | |||
} | |||
if (fs::relative(copy_dest, root).generic_string().find("../") == 0) { | |||
throw std::runtime_error( | |||
fmt::format("A copy_dest ends up copying from outside the root. (Relative path was " | |||
"[{}], resolved path was [{}])", | |||
copy.from.string(), | |||
copy_dest.string())); | |||
} | |||
if (fs::is_regular_file(copy_src)) { | |||
// Just copying a single file? Okay. | |||
if (op == move) { | |||
safe_rename(copy_src, copy_dest); | |||
} else { | |||
fs::copy_file(copy_src, copy_dest, fs::copy_options::overwrite_existing); | |||
} | |||
return; | |||
} | |||
auto f_iter = fs::recursive_directory_iterator(copy_src); | |||
for (auto item : f_iter) { | |||
auto relpath = fs::relative(item, copy_src); | |||
auto matches_glob = [&](auto glob) { return glob.match(relpath.string()); }; | |||
auto included = ranges::all_of(copy.include, matches_glob); | |||
auto excluded = ranges::any_of(copy.exclude, matches_glob); | |||
if (!included || excluded) { | |||
continue; | |||
} | |||
auto n_components = ranges::distance(relpath); | |||
if (n_components <= copy.strip_components) { | |||
continue; | |||
} | |||
auto it = relpath.begin(); | |||
std::advance(it, copy.strip_components); | |||
relpath = ranges::accumulate(it, relpath.end(), fs::path(), std::divides<>()); | |||
auto dest = copy_dest / relpath; | |||
fs::create_directories(dest.parent_path()); | |||
if (item.is_directory()) { | |||
fs::create_directories(dest); | |||
} else { | |||
if (op == move) { | |||
safe_rename(item, dest); | |||
} else { | |||
fs::copy_file(item, dest, fs::copy_options::overwrite_existing); | |||
} | |||
} | |||
} | |||
} | |||
void apply_remove(const struct dds::repo_transform::remove& rm, path_ref root) { | |||
const auto item = fs::weakly_canonical(root / rm.path); | |||
if (fs::relative(item, root).generic_string().find("../") == 0) { | |||
throw std::runtime_error(fmt::format( | |||
"A 'remove' ends up removing files from outside the root. (Relative path was " | |||
"[{}], resolved path was [{}])", | |||
rm.path.string(), | |||
item.string())); | |||
} | |||
if (!rm.only_matching.empty()) { | |||
if (!fs::is_directory(item)) { | |||
throw std::runtime_error( | |||
fmt::format("A 'remove' item has an 'only_matching' pattern list, but the named " | |||
"path is not a directory [{}]", | |||
item.string())); | |||
} | |||
for (auto glob : rm.only_matching) { | |||
for (auto rm_item : glob.scan_from(item)) { | |||
fs::remove_all(rm_item); | |||
} | |||
} | |||
} else { | |||
fs::remove_all(item); | |||
} | |||
if (fs::is_directory(item)) { | |||
} | |||
} | |||
void apply_transform(const dds::repo_transform& transform, path_ref root) { | |||
if (transform.copy) { | |||
apply_copy(*transform.copy, root, copy); | |||
} | |||
if (transform.move) { | |||
apply_copy(*transform.move, root, move); | |||
} | |||
if (transform.remove) { | |||
apply_remove(*transform.remove, root); | |||
} | |||
temporary_sdist do_pull_sdist(const package_info& listing, std::monostate) { | |||
neo_assert_always( | |||
invariant, | |||
false, | |||
"A package listing in the catalog has no defined remote from which to pull. This " | |||
"shouldn't happen in normal usage. This will occur if the database has been " | |||
"manually altered, or if DDS has a bug.", | |||
listing.ident.to_string()); | |||
} | |||
temporary_sdist do_pull_sdist(const package_info& listing, const git_remote_listing& git) { | |||
auto tmpdir = dds::temporary_dir::create(); | |||
using namespace std::literals; | |||
spdlog::info("Cloning Git repository: {} [{}] ...", git.url, git.ref); | |||
auto command = {"git"s, | |||
"clone"s, | |||
"--depth=1"s, | |||
"--branch"s, | |||
git.ref, | |||
git.url, | |||
tmpdir.path().generic_string()}; | |||
auto git_res = run_proc(command); | |||
if (!git_res.okay()) { | |||
throw_external_error<errc::git_clone_failure>( | |||
"Git clone operation failed [Git command: {}] [Exitted {}]:\n{}", | |||
quote_command(command), | |||
git_res.retc, | |||
git_res.output); | |||
} | |||
git.clone(tmpdir.path()); | |||
/// XXX: | |||
// for (const auto& tr : listing.transforms) { | |||
// tr.apply_to(tmpdir.path()); | |||
// } | |||
spdlog::info("Create sdist from clone ..."); | |||
if (git.auto_lib.has_value()) { | |||
spdlog::info("Generating library data automatically"); | |||
@@ -157,10 +55,6 @@ temporary_sdist do_pull_sdist(const package_info& listing, const git_remote_list | |||
lib_strm << nlohmann::to_string(lib_json); | |||
} | |||
for (const auto& tr : listing.transforms) { | |||
apply_transform(tr, tmpdir.path()); | |||
} | |||
sdist_params params; | |||
params.project_dir = tmpdir.path(); | |||
auto sd_tmp_dir = dds::temporary_dir::create(); |
@@ -0,0 +1,241 @@ | |||
#include "./import.hpp" | |||
#include <dds/error/errors.hpp> | |||
#include <json5/parse_data.hpp> | |||
#include <neo/assert.hpp> | |||
#include <semester/decomp.hpp> | |||
#include <spdlog/fmt/fmt.h> | |||
#include <optional> | |||
using namespace dds; | |||
template <typename... Args> | |||
struct any_key { | |||
semester::try_seq<Args...> _seq; | |||
std::string_view& _key; | |||
any_key(std::string_view& key_var, Args&&... args) | |||
: _seq(NEO_FWD(args)...) | |||
, _key{key_var} {} | |||
template <typename Data> | |||
semester::dc_result_t operator()(std::string_view key, Data&& dat) const { | |||
_key = key; | |||
return _seq.invoke(dat); | |||
} | |||
}; | |||
template <typename... Args> | |||
any_key(std::string_view, Args&&...) -> any_key<Args&&...>; | |||
namespace { | |||
semester::dc_result_t reject(std::string s) { return semester::dc_reject_t{s}; } | |||
semester::dc_result_t pass = semester::dc_pass; | |||
semester::dc_result_t accept = semester::dc_accept; | |||
using require_obj = semester::require_type<json5::data::mapping_type>; | |||
auto reject_unknown_key(std::string_view path) { | |||
return [path = std::string(path)](auto key, auto&&) { // | |||
return reject(fmt::format("{}: unknown key '{}'", path, key)); | |||
}; | |||
}; | |||
std::vector<dependency> parse_deps_json_v1(const json5::data& deps, std::string_view path) { | |||
std::vector<dependency> acc_deps; | |||
std::string_view dep_name; | |||
std::string_view dep_version_range_str; | |||
using namespace semester::decompose_ops; | |||
auto result = semester::decompose( // | |||
deps, | |||
mapping{any_key{ | |||
dep_name, | |||
[&](auto&& range_str) { | |||
if (!range_str.is_string()) { | |||
throw_user_error< | |||
errc::invalid_catalog_json>("{}/{} should be a string version range", | |||
path, | |||
dep_name); | |||
} | |||
try { | |||
auto rng = semver::range::parse_restricted(range_str.as_string()); | |||
acc_deps.push_back(dependency{std::string{dep_name}, {rng.low(), rng.high()}}); | |||
return accept; | |||
} catch (const semver::invalid_range&) { | |||
throw_user_error<errc::invalid_version_range_string>( | |||
"Invalid version range string '{}' at {}/{}", | |||
range_str.as_string(), | |||
path, | |||
dep_name); | |||
} | |||
}, | |||
}}); | |||
neo_assert(invariant, | |||
std::holds_alternative<semester::dc_accept_t>(result), | |||
"Parsing dependency object did not accept??"); | |||
return acc_deps; | |||
} | |||
package_info parse_pkg_json_v1(std::string_view name, | |||
semver::version version, | |||
std::string_view path, | |||
const json5::data& pkg) { | |||
using namespace semester::decompose_ops; | |||
package_info ret; | |||
ret.ident = package_id{std::string{name}, version}; | |||
auto result = semester::decompose( // | |||
pkg, | |||
mapping{if_key{"description", | |||
require_type<std::string>{ | |||
fmt::format("{}/description should be a string", path)}, | |||
put_into{ret.description}}, | |||
if_key{"depends", | |||
require_obj{fmt::format("{}/depends must be a JSON object", path)}, | |||
[&](auto&& dep_obj) { | |||
ret.deps = parse_deps_json_v1(dep_obj, fmt::format("{}/depends", path)); | |||
return accept; | |||
}}, | |||
if_key{ | |||
"git", | |||
require_obj{fmt::format("{}/git must be a JSON object", path)}, | |||
[&](auto&& git_obj) { | |||
git_remote_listing git_remote; | |||
auto r = semester::decompose( | |||
git_obj, | |||
mapping{ | |||
if_key{"url", put_into{git_remote.url}}, | |||
if_key{"ref", put_into{git_remote.ref}}, | |||
if_key{"auto-lib", | |||
require_type<std::string>{ | |||
fmt::format("{}/git/auto-lib must be a string", path)}, | |||
[&](auto&& al) { | |||
git_remote.auto_lib | |||
= lm::split_usage_string(al.as_string()); | |||
return accept; | |||
}}, | |||
reject_unknown_key(std::string(path) + "/git"), | |||
}); | |||
if (git_remote.url.empty() || git_remote.ref.empty()) { | |||
throw_user_error<errc::invalid_catalog_json>( | |||
"{}/git requires both 'url' and 'ref' non-empty string properties", | |||
path); | |||
} | |||
ret.remote = git_remote; | |||
return r; | |||
}, | |||
}, | |||
reject_unknown_key(path)}); | |||
if (std::holds_alternative<std::monostate>(ret.remote)) { | |||
throw_user_error< | |||
errc::invalid_catalog_json>("{}: Requires a remote listing (e.g. a 'git' proprety).", | |||
path); | |||
} | |||
return ret; | |||
} | |||
std::vector<package_info> parse_json_v1(const json5::data& data) { | |||
using namespace semester::decompose_ops; | |||
auto packages_it = data.as_object().find("packages"); | |||
if (packages_it == data.as_object().end() || !packages_it->second.is_object()) { | |||
throw_user_error<errc::invalid_catalog_json>( | |||
"Root JSON object requires a 'packages' property"); | |||
} | |||
std::vector<package_info> acc_pkgs; | |||
std::string_view pkg_name; | |||
std::string_view pkg_version_str; | |||
auto result = semester::decompose( | |||
data, | |||
mapping{ | |||
// Ignore the "version" key at this level | |||
if_key{"version", just_accept}, | |||
if_key{ | |||
"packages", | |||
mapping{any_key{ | |||
pkg_name, | |||
[&](auto&& entry) { | |||
if (!entry.is_object()) { | |||
return reject( | |||
fmt::format("/packages/{} must be a JSON object", pkg_name)); | |||
} | |||
return pass; | |||
}, | |||
mapping{any_key{ | |||
pkg_version_str, | |||
[&](auto&& pkg_def) { | |||
semver::version version; | |||
try { | |||
version = semver::version::parse(pkg_version_str); | |||
} catch (const semver::invalid_version& e) { | |||
throw_user_error<errc::invalid_catalog_json>( | |||
"/packages/{} version string '{}' is invalid: {}", | |||
pkg_name, | |||
pkg_version_str, | |||
e.what()); | |||
} | |||
if (!pkg_def.is_object()) { | |||
return reject(fmt::format("/packages/{}/{} must be a JSON object")); | |||
} | |||
auto pkg = parse_pkg_json_v1(pkg_name, | |||
version, | |||
fmt::format("/packages/{}/{}", | |||
pkg_name, | |||
pkg_version_str), | |||
pkg_def); | |||
acc_pkgs.emplace_back(std::move(pkg)); | |||
return accept; | |||
}, | |||
}}, | |||
}}, | |||
}, | |||
reject_unknown_key("/"), | |||
}); | |||
auto rej = std::get_if<semester::dc_reject_t>(&result); | |||
if (rej) { | |||
throw_user_error<errc::invalid_catalog_json>(rej->message); | |||
} | |||
return acc_pkgs; | |||
} | |||
} // namespace | |||
std::vector<package_info> dds::parse_packages_json(std::string_view content) { | |||
json5::data data; | |||
try { | |||
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(); | |||
if (version == 1.0) { | |||
return parse_json_v1(data); | |||
} else { | |||
throw_user_error<errc::invalid_catalog_json>("Unknown catalog JSON version '{}'", version); | |||
} | |||
} |
@@ -0,0 +1,9 @@ | |||
#pragma once | |||
#include "./package_info.hpp" | |||
namespace dds { | |||
std::vector<package_info> parse_packages_json(std::string_view); | |||
} // namespace dds |
@@ -0,0 +1,79 @@ | |||
#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: 1, 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: 1}", | |||
// Bad version | |||
"{version: 1.7, packages: {}}", | |||
"{version: [], packages: {}}", | |||
"{version: null, packages: {}}", | |||
// 'packages' should be an object | |||
"{version: 1, packages: []}", | |||
"{version: 1, packages: null}", | |||
"{version: 1, packages: 4}", | |||
"{version: 1, packages: 'lol'}", | |||
// Objects in 'packages' should be objects | |||
"{version:1, packages:{foo:null}}", | |||
"{version:1, packages:{foo:[]}}", | |||
"{version:1, packages:{foo:9}}", | |||
"{version:1, packages:{foo:'lol'}}", | |||
// Objects in 'packages' shuold have version strings | |||
"{version:1, packages:{foo:{'lol':{}}}}", | |||
"{version:1, packages:{foo:{'1.2':{}}}}", | |||
}; | |||
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:1, packages:{}}", | |||
// No versions for 'foo' is weird, but okay | |||
"{version:1, packages:{foo:{}}}", | |||
}; | |||
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: 1, | |||
packages: { | |||
foo: { | |||
'1.2.3': { | |||
git: { | |||
url: 'foo', | |||
ref: 'fasdf' | |||
} | |||
} | |||
} | |||
} | |||
})"); | |||
CHECK(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)); | |||
} |
@@ -0,0 +1,27 @@ | |||
#pragma once | |||
#include "./remote/git.hpp" | |||
#include <dds/deps.hpp> | |||
#include <dds/package/id.hpp> | |||
#include <dds/util/fs_transform.hpp> | |||
#include <dds/util/glob.hpp> | |||
#include <optional> | |||
#include <string> | |||
#include <variant> | |||
#include <vector> | |||
namespace dds { | |||
struct package_info { | |||
package_id ident; | |||
std::vector<dependency> deps; | |||
std::string description; | |||
std::variant<std::monostate, git_remote_listing> remote; | |||
std::vector<fs_transformation> transforms; | |||
}; | |||
} // namespace dds |
@@ -0,0 +1,18 @@ | |||
#include "./git.hpp" | |||
#include <dds/error/errors.hpp> | |||
#include <dds/proc.hpp> | |||
void dds::git_remote_listing::clone(dds::path_ref dest) const { | |||
fs::remove_all(dest); | |||
using namespace std::literals; | |||
auto command = {"git"s, "clone"s, "--depth=1"s, "--branch"s, ref, url, dest.generic_string()}; | |||
auto git_res = run_proc(command); | |||
if (!git_res.okay()) { | |||
throw_external_error<errc::git_clone_failure>( | |||
"Git clone operation failed [Git command: {}] [Exitted {}]:\n{}", | |||
quote_command(command), | |||
git_res.retc, | |||
git_res.output); | |||
} | |||
} |
@@ -0,0 +1,21 @@ | |||
#pragma once | |||
#include <dds/catalog/get.hpp> | |||
#include <dds/util/fs.hpp> | |||
#include <libman/package.hpp> | |||
#include <optional> | |||
#include <string> | |||
namespace dds { | |||
struct git_remote_listing { | |||
std::string url; | |||
std::string ref; | |||
std::optional<lm::usage> auto_lib; | |||
void clone(path_ref path) const; | |||
}; | |||
} // namespace dds |
@@ -0,0 +1,37 @@ | |||
#pragma once | |||
#include "./fs.hpp" | |||
#include "./glob.hpp" | |||
#include <optional> | |||
#include <variant> | |||
namespace dds { | |||
class 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; | |||
}; | |||
std::optional<struct copy> copy; | |||
std::optional<struct move> move; | |||
std::optional<remove> remove; | |||
void apply_to(path_ref root) const; | |||
}; | |||
} // namespace dds |