[vcpkg-ci] Do not rebuild libraries that were previously successful or failed

This commit is contained in:
Robert Schumacher 2018-03-14 10:30:27 -07:00
parent 39c63d2276
commit eab1d5c531
14 changed files with 350 additions and 72 deletions

View File

@ -0,0 +1,21 @@
#pragma once
#include <map>
namespace vcpkg
{
template<class Key, class Value>
struct Cache
{
template<class F>
Value const& get_lazy(const Key& k, const F& f) const
{
auto it = m_cache.find(k);
if (it != m_cache.end()) return it->second;
return m_cache.emplace(k, f()).first->second;
}
private:
mutable std::map<Key, Value> m_cache;
};
}

View File

@ -24,10 +24,10 @@ namespace vcpkg::Util
namespace Sets
{
template<class Container>
bool contains(const Container& container, const ElementT<Container>& item)
template<class Container, class Key>
bool contains(const Container& container, const Key& item)
{
return container.find(item) != container.cend();
return container.find(item) != container.end();
}
}

View File

@ -204,4 +204,21 @@ namespace vcpkg::Build
};
BuildInfo read_build_info(const Files::Filesystem& fs, const fs::path& filepath);
struct AbiEntry
{
std::string key;
std::string value;
};
struct AbiTagAndFile
{
std::string tag;
fs::path tag_file;
};
Optional<AbiTagAndFile> compute_abi_tag(const VcpkgPaths& paths,
const BuildPackageConfig& config,
const PreBuildInfo& pre_build_info,
Span<const AbiEntry> dependency_abis);
}

View File

@ -7,6 +7,8 @@
#include <vcpkg/vcpkgpaths.h>
#include <array>
#include <map>
#include <vector>
namespace vcpkg::Commands
{
@ -23,7 +25,17 @@ namespace vcpkg::Commands
namespace CI
{
struct UnknownCIPortsResults
{
std::vector<PackageSpec> unknown;
std::map<PackageSpec, Build::BuildResult> known;
};
extern const CommandStructure COMMAND_STRUCTURE;
UnknownCIPortsResults find_unknown_ports_for_ci(const VcpkgPaths& paths,
const std::set<std::string>& exclusions,
const Dependencies::PortFileProvider& provider,
const std::vector<FeatureSpec>& fspecs);
void perform_and_exit(const VcpkgCmdArguments& args, const VcpkgPaths& paths, const Triplet& default_triplet);
}

View File

@ -45,7 +45,8 @@ namespace vcpkg::Dependencies
InstallPlanAction(const PackageSpec& spec,
const SourceControlFile& scf,
const std::set<std::string>& features,
const RequestType& request_type);
const RequestType& request_type,
std::vector<PackageSpec>&& dependencies);
std::string displayname() const;
@ -58,6 +59,8 @@ namespace vcpkg::Dependencies
RequestType request_type;
Build::BuildPackageOptions build_options;
std::set<std::string> feature_list;
std::vector<PackageSpec> computed_dependencies;
};
enum class RemovePlanType

View File

@ -37,6 +37,7 @@ namespace vcpkg::Install
std::string total_elapsed_time;
void print() const;
static std::string xunit_result(const PackageSpec& spec, Chrono::ElapsedTime time, Build::BuildResult code);
std::string xunit_results() const;
};

View File

@ -63,6 +63,8 @@ namespace vcpkg
std::unique_ptr<SourceParagraph> core_paragraph;
std::vector<std::unique_ptr<FeatureParagraph>> feature_paragraphs;
Optional<const FeatureParagraph&> find_feature(const std::string& featurename) const;
};
void print_error_message(Span<const std::unique_ptr<Parse::ParseControlErrorInfo>> error_info_list);

View File

@ -593,6 +593,87 @@ namespace UnitTest1
features_check(&install_plan[0], "a", {"core"}, Triplet::X64_WINDOWS);
}
TEST_METHOD(install_plan_action_dependencies)
{
std::vector<std::unique_ptr<StatusParagraph>> status_paragraphs;
// Add a port "a" which depends on the core of "b", which was already
// installed explicitly
PackageSpecMap spec_map(Triplet::X64_WINDOWS);
auto spec_c = spec_map.emplace("c");
auto spec_b = spec_map.emplace("b", "c");
spec_map.emplace("a", "b");
// Install "a" (without explicit feature specification)
auto install_specs = FullPackageSpec::from_string("a", Triplet::X64_WINDOWS);
auto install_plan = Dependencies::create_feature_install_plan(
spec_map.map,
FullPackageSpec::to_feature_specs({install_specs.value_or_exit(VCPKG_LINE_INFO)}),
StatusParagraphs(std::move(status_paragraphs)));
Assert::IsTrue(install_plan.size() == 3);
features_check(&install_plan[0], "c", {"core"}, Triplet::X64_WINDOWS);
features_check(&install_plan[1], "b", {"core"}, Triplet::X64_WINDOWS);
Assert::IsTrue(install_plan[1].install_action.get()->computed_dependencies ==
std::vector<PackageSpec>{spec_c});
features_check(&install_plan[2], "a", {"core"}, Triplet::X64_WINDOWS);
Assert::IsTrue(install_plan[2].install_action.get()->computed_dependencies ==
std::vector<PackageSpec>{spec_b});
}
TEST_METHOD(install_plan_action_dependencies_2)
{
std::vector<std::unique_ptr<StatusParagraph>> status_paragraphs;
// Add a port "a" which depends on the core of "b", which was already
// installed explicitly
PackageSpecMap spec_map(Triplet::X64_WINDOWS);
auto spec_c = spec_map.emplace("c");
auto spec_b = spec_map.emplace("b", "c");
spec_map.emplace("a", "c, b");
// Install "a" (without explicit feature specification)
auto install_specs = FullPackageSpec::from_string("a", Triplet::X64_WINDOWS);
auto install_plan = Dependencies::create_feature_install_plan(
spec_map.map,
FullPackageSpec::to_feature_specs({install_specs.value_or_exit(VCPKG_LINE_INFO)}),
StatusParagraphs(std::move(status_paragraphs)));
Assert::IsTrue(install_plan.size() == 3);
features_check(&install_plan[0], "c", {"core"}, Triplet::X64_WINDOWS);
features_check(&install_plan[1], "b", {"core"}, Triplet::X64_WINDOWS);
Assert::IsTrue(install_plan[1].install_action.get()->computed_dependencies ==
std::vector<PackageSpec>{spec_c});
features_check(&install_plan[2], "a", {"core"}, Triplet::X64_WINDOWS);
Assert::IsTrue(install_plan[2].install_action.get()->computed_dependencies ==
std::vector<PackageSpec>{spec_b, spec_c});
}
TEST_METHOD(install_plan_action_dependencies_3)
{
std::vector<std::unique_ptr<StatusParagraph>> status_paragraphs;
// Add a port "a" which depends on the core of "b", which was already
// installed explicitly
PackageSpecMap spec_map(Triplet::X64_WINDOWS);
spec_map.emplace("a", "", {{"0", ""}, {"1", "a[0]"}}, {"1"});
// Install "a" (without explicit feature specification)
auto install_specs = FullPackageSpec::from_string("a", Triplet::X64_WINDOWS);
auto install_plan = Dependencies::create_feature_install_plan(
spec_map.map,
FullPackageSpec::to_feature_specs({install_specs.value_or_exit(VCPKG_LINE_INFO)}),
StatusParagraphs(std::move(status_paragraphs)));
Assert::IsTrue(install_plan.size() == 1);
features_check(&install_plan[0], "a", {"1", "0", "core"}, Triplet::X64_WINDOWS);
Assert::IsTrue(install_plan[0].install_action.get()->computed_dependencies == std::vector<PackageSpec>{});
}
TEST_METHOD(upgrade_with_default_features_1)
{
std::vector<std::unique_ptr<StatusParagraph>> pghs;
@ -619,7 +700,6 @@ namespace UnitTest1
Assert::IsTrue(plan[0].remove_action.has_value());
Assert::AreEqual("a", plan[1].spec().name().c_str());
Assert::IsTrue(plan[1].install_action.has_value());
features_check(&plan[1], "a", {"core", "0"}, Triplet::X86_WINDOWS);
}
@ -651,11 +731,9 @@ namespace UnitTest1
Assert::IsTrue(plan[0].remove_action.has_value());
Assert::AreEqual("b", plan[1].spec().name().c_str());
Assert::IsTrue(plan[1].install_action.has_value());
features_check(&plan[1], "b", {"b0", "core"}, Triplet::X64_WINDOWS);
Assert::AreEqual("a", plan[2].spec().name().c_str());
Assert::IsTrue(plan[2].install_action.has_value());
features_check(&plan[2], "a", {"core"}, Triplet::X64_WINDOWS);
}

View File

@ -314,6 +314,8 @@ namespace vcpkg::System
// Flush stdout before launching external process
fflush(stdout);
auto timer = Chrono::ElapsedTimer::create_started();
#if defined(_WIN32)
const auto actual_cmd_line = Strings::format(R"###("%s 2>&1")###", cmd_line);
@ -335,8 +337,10 @@ namespace vcpkg::System
}
const auto ec = _pclose(pipe);
Debug::println("_pclose() returned %d", ec);
remove_byte_order_marks(&output);
Debug::println("_pclose() returned %d after %8d us", ec, (int)timer.microseconds());
return {ec, Strings::to_utf8(output)};
#else
const auto actual_cmd_line = Strings::format(R"###(%s 2>&1)###", cmd_line);
@ -359,7 +363,9 @@ namespace vcpkg::System
}
const auto ec = pclose(pipe);
Debug::println("pclose() returned %d", ec);
Debug::println("_pclose() returned %d after %8d us", ec, (int)timer.microseconds());
return {ec, output};
#endif
}

View File

@ -273,12 +273,10 @@ namespace vcpkg::Build
return filter_dependencies(config.scf.core_paragraph->depends, triplet);
}
auto it =
Util::find_if(config.scf.feature_paragraphs,
[&](std::unique_ptr<FeatureParagraph> const& fpgh) { return fpgh->name == feature; });
Checks::check_exit(VCPKG_LINE_INFO, it != config.scf.feature_paragraphs.end());
auto maybe_feature = config.scf.find_feature(feature);
Checks::check_exit(VCPKG_LINE_INFO, maybe_feature.has_value());
return filter_dependencies(it->get()->depends, triplet);
return filter_dependencies(maybe_feature.get()->depends, triplet);
});
auto dep_fspecs = FeatureSpec::from_strings_and_triplet(dep_strings, triplet);
@ -423,22 +421,10 @@ namespace vcpkg::Build
return result;
}
struct AbiEntry
{
std::string key;
std::string value;
};
struct AbiTagAndFile
{
std::string tag;
fs::path tag_file;
};
static Optional<AbiTagAndFile> compute_abi_tag(const VcpkgPaths& paths,
const BuildPackageConfig& config,
const PreBuildInfo& pre_build_info,
Span<const AbiEntry> dependency_abis)
Optional<AbiTagAndFile> compute_abi_tag(const VcpkgPaths& paths,
const BuildPackageConfig& config,
const PreBuildInfo& pre_build_info,
Span<const AbiEntry> dependency_abis)
{
if (!GlobalState::g_binary_caching) return nullopt;
@ -584,17 +570,17 @@ namespace vcpkg::Build
const auto pre_build_info = PreBuildInfo::from_triplet_file(paths, triplet);
auto abi_tag_and_file = compute_abi_tag(paths, config, pre_build_info, dependency_abis);
auto maybe_abi_tag_and_file = compute_abi_tag(paths, config, pre_build_info, dependency_abis);
std::unique_ptr<BinaryControlFile> bcf;
auto maybe_abi_tag_and_file = abi_tag_and_file.get();
auto abi_tag_and_file = maybe_abi_tag_and_file.get();
if (GlobalState::g_binary_caching && maybe_abi_tag_and_file)
if (GlobalState::g_binary_caching && abi_tag_and_file)
{
auto archives_root_dir = paths.root / "archives";
auto archive_name = maybe_abi_tag_and_file->tag + ".zip";
auto archive_subpath = fs::u8path(maybe_abi_tag_and_file->tag.substr(0, 2)) / archive_name;
auto archive_name = abi_tag_and_file->tag + ".zip";
auto archive_subpath = fs::u8path(abi_tag_and_file->tag.substr(0, 2)) / archive_name;
auto archive_path = archives_root_dir / archive_subpath;
auto archive_tombstone_path = archives_root_dir / "fail" / archive_subpath;
@ -617,12 +603,12 @@ namespace vcpkg::Build
System::println("Could not locate cached archive: %s", archive_path.u8string());
ExtendedBuildResult result = do_build_package_and_clean_buildtrees(
paths, pre_build_info, spec, abi_tag_and_file.value_or(AbiTagAndFile{}).tag, config, status_db);
paths, pre_build_info, spec, maybe_abi_tag_and_file.value_or(AbiTagAndFile{}).tag, config, status_db);
std::error_code ec;
fs.create_directories(paths.package_dir(spec) / "share" / spec.name(), ec);
auto abi_file_in_package = paths.package_dir(spec) / "share" / spec.name() / "vcpkg_abi_info.txt";
fs.copy_file(maybe_abi_tag_and_file->tag_file, abi_file_in_package, fs::stdfs::copy_options::none, ec);
fs.copy_file(abi_tag_and_file->tag_file, abi_file_in_package, fs::stdfs::copy_options::none, ec);
Checks::check_exit(VCPKG_LINE_INFO, !ec, "Could not copy into file: %s", abi_file_in_package.u8string());
if (result.code == BuildResult::SUCCEEDED)
@ -648,7 +634,7 @@ namespace vcpkg::Build
else
{
return do_build_package_and_clean_buildtrees(
paths, pre_build_info, spec, abi_tag_and_file.value_or(AbiTagAndFile{}).tag, config, status_db);
paths, pre_build_info, spec, maybe_abi_tag_and_file.value_or(AbiTagAndFile{}).tag, config, status_db);
}
}

View File

@ -1,5 +1,6 @@
#include "pch.h"
#include <vcpkg/base/cache.h>
#include <vcpkg/base/files.h>
#include <vcpkg/base/stringliteral.h>
#include <vcpkg/base/system.h>
@ -7,6 +8,7 @@
#include <vcpkg/build.h>
#include <vcpkg/commands.h>
#include <vcpkg/dependencies.h>
#include <vcpkg/globalstate.h>
#include <vcpkg/help.h>
#include <vcpkg/input.h>
#include <vcpkg/install.h>
@ -44,8 +46,120 @@ namespace vcpkg::Commands::CI
nullptr,
};
UnknownCIPortsResults find_unknown_ports_for_ci(const VcpkgPaths& paths,
const std::set<std::string>& exclusions,
const Dependencies::PortFileProvider& provider,
const std::vector<FeatureSpec>& fspecs)
{
UnknownCIPortsResults ret;
auto& fs = paths.get_filesystem();
std::map<PackageSpec, std::string> abi_tag_map;
std::set<PackageSpec> will_fail;
const Build::BuildPackageOptions install_plan_options = {Build::UseHeadVersion::NO,
Build::AllowDownloads::YES,
Build::CleanBuildtrees::YES,
Build::CleanPackages::YES};
vcpkg::Cache<Triplet, Build::PreBuildInfo> pre_build_info_cache;
auto action_plan = Dependencies::create_feature_install_plan(provider, fspecs, StatusParagraphs{});
for (auto&& action : action_plan)
{
if (auto p = action.install_action.get())
{
// determine abi tag
std::string abi;
if (auto scf = p->source_control_file.get())
{
auto triplet = p->spec.triplet();
const Build::BuildPackageConfig build_config{p->source_control_file.value_or_exit(VCPKG_LINE_INFO),
triplet,
paths.port_dir(p->spec),
install_plan_options,
p->feature_list};
auto dependency_abis =
Util::fmap(p->computed_dependencies, [&](const PackageSpec& spec) -> Build::AbiEntry {
auto it = abi_tag_map.find(spec);
if (it == abi_tag_map.end())
return {spec.name(), ""};
else
return {spec.name(), it->second};
});
const auto& pre_build_info = pre_build_info_cache.get_lazy(
triplet, [&]() { return Build::PreBuildInfo::from_triplet_file(paths, triplet); });
auto maybe_tag_and_file =
Build::compute_abi_tag(paths, build_config, pre_build_info, dependency_abis);
if (auto tag_and_file = maybe_tag_and_file.get())
{
abi = tag_and_file->tag;
abi_tag_map.emplace(p->spec, abi);
}
}
else if (auto ipv = p->installed_package.get())
{
abi = ipv->core->package.abi;
if (!abi.empty()) abi_tag_map.emplace(p->spec, abi);
}
std::string state;
auto archives_root_dir = paths.root / "archives";
auto archive_name = abi + ".zip";
auto archive_subpath = fs::u8path(abi.substr(0, 2)) / archive_name;
auto archive_path = archives_root_dir / archive_subpath;
auto archive_tombstone_path = archives_root_dir / "fail" / archive_subpath;
bool b_will_build = false;
if (Util::Sets::contains(exclusions, p->spec.name()))
{
ret.known.emplace(p->spec, BuildResult::EXCLUDED);
will_fail.emplace(p->spec);
}
else if (std::any_of(p->computed_dependencies.begin(),
p->computed_dependencies.end(),
[&](const PackageSpec& spec) { return Util::Sets::contains(will_fail, spec); }))
{
ret.known.emplace(p->spec, BuildResult::CASCADED_DUE_TO_MISSING_DEPENDENCIES);
will_fail.emplace(p->spec);
}
else if (fs.exists(archive_path))
{
state += "pass";
ret.known.emplace(p->spec, BuildResult::SUCCEEDED);
}
else if (fs.exists(archive_tombstone_path))
{
state += "fail";
ret.known.emplace(p->spec, BuildResult::BUILD_FAILED);
will_fail.emplace(p->spec);
}
else
{
ret.unknown.push_back(p->spec);
b_will_build = true;
}
System::println("%40s: %1s %8s: %s", p->spec, (b_will_build ? "*" : " "), state, abi);
}
}
return ret;
}
void perform_and_exit(const VcpkgCmdArguments& args, const VcpkgPaths& paths, const Triplet& default_triplet)
{
Checks::check_exit(
VCPKG_LINE_INFO, GlobalState::g_binary_caching, "The ci command requires binary caching to be enabled.");
const ParsedArguments options = args.parse_arguments(COMMAND_STRUCTURE);
std::set<std::string> exclusions_set;
@ -77,16 +191,21 @@ namespace vcpkg::Commands::CI
Build::CleanBuildtrees::YES,
Build::CleanPackages::YES};
std::vector<std::string> ports = Install::get_all_port_names(paths);
std::vector<std::map<PackageSpec, BuildResult>> all_known_results;
std::vector<std::string> all_ports = Install::get_all_port_names(paths);
std::vector<TripletAndSummary> results;
for (const Triplet& triplet : triplets)
{
Input::check_triplet(triplet, paths);
std::vector<PackageSpec> specs = PackageSpec::to_package_specs(ports, triplet);
// Install the default features for every package
const auto featurespecs = Util::fmap(specs, [](auto& spec) { return FeatureSpec(spec, ""); });
auto action_plan = Dependencies::create_feature_install_plan(paths_port_file, featurespecs, status_db);
std::vector<PackageSpec> specs = PackageSpec::to_package_specs(all_ports, triplet);
// Install the default features for every package
auto all_fspecs = Util::fmap(specs, [](auto& spec) { return FeatureSpec(spec, ""); });
auto split_specs = find_unknown_ports_for_ci(paths, exclusions_set, paths_port_file, all_fspecs);
auto fspecs = Util::fmap(split_specs.unknown, [](auto& spec) { return FeatureSpec(spec, ""); });
auto action_plan = Dependencies::create_feature_install_plan(paths_port_file, fspecs, status_db);
for (auto&& action : action_plan)
{
@ -107,7 +226,10 @@ namespace vcpkg::Commands::CI
else
{
auto summary = Install::perform(action_plan, Install::KeepGoing::YES, paths, status_db);
for (auto&& result : summary.results)
split_specs.known.erase(result.spec);
results.push_back({triplet, std::move(summary)});
all_known_results.emplace_back(std::move(split_specs.known));
}
}
@ -125,6 +247,14 @@ namespace vcpkg::Commands::CI
for (auto&& result : results)
xunit_doc += result.summary.xunit_results();
for (auto&& known_result : all_known_results)
{
for (auto&& result : known_result)
{
xunit_doc +=
Install::InstallSummary::xunit_result(result.first, Chrono::ElapsedTime{}, result.second);
}
}
xunit_doc += "</collection></assembly></assemblies>\n";
paths.get_filesystem().write_contents(fs::u8path(it_xunit->second), xunit_doc);

View File

@ -144,12 +144,14 @@ namespace vcpkg::Dependencies
InstallPlanAction::InstallPlanAction(const PackageSpec& spec,
const SourceControlFile& scf,
const std::set<std::string>& features,
const RequestType& request_type)
const RequestType& request_type,
std::vector<PackageSpec>&& dependencies)
: spec(spec)
, source_control_file(scf)
, plan_type(InstallPlanType::BUILD_AND_INSTALL)
, request_type(request_type)
, feature_list(features)
, computed_dependencies(std::move(dependencies))
{
}
@ -162,6 +164,7 @@ namespace vcpkg::Dependencies
, plan_type(InstallPlanType::ALREADY_INSTALLED)
, request_type(request_type)
, feature_list(features)
, computed_dependencies(installed_package.get()->dependencies())
{
}
@ -683,11 +686,17 @@ namespace vcpkg::Dependencies
// If it will be transiently uninstalled, we need to issue a full installation command
auto pscf = p_cluster->source_control_file.value_or_exit(VCPKG_LINE_INFO);
Checks::check_exit(VCPKG_LINE_INFO, pscf != nullptr);
auto dep_specs = Util::fmap(m_graph_plan->install_graph.adjacency_list(p_cluster),
[](ClusterPtr const& p) { return p->spec; });
Util::sort_unique_erase(dep_specs);
plan.emplace_back(InstallPlanAction{
p_cluster->spec,
*pscf,
p_cluster->to_install_features,
p_cluster->request_type,
std::move(dep_specs),
});
}
else

View File

@ -668,39 +668,42 @@ namespace vcpkg::Install
return nullptr;
}
std::string InstallSummary::xunit_result(const PackageSpec& spec, Chrono::ElapsedTime time, BuildResult code)
{
std::string inner_block;
const char* result_string = "";
switch (code)
{
case BuildResult::POST_BUILD_CHECKS_FAILED:
case BuildResult::FILE_CONFLICTS:
case BuildResult::BUILD_FAILED:
result_string = "Fail";
inner_block = Strings::format("<failure><message><![CDATA[%s]]></message></failure>", to_string(code));
break;
case BuildResult::EXCLUDED:
case BuildResult::CASCADED_DUE_TO_MISSING_DEPENDENCIES:
result_string = "Skip";
inner_block = Strings::format("<reason><![CDATA[%s]]></reason>", to_string(code));
break;
case BuildResult::SUCCEEDED: result_string = "Pass"; break;
default: Checks::exit_fail(VCPKG_LINE_INFO);
}
return Strings::format(R"(<test name="%s" method="%s" time="%lld" result="%s">%s</test>)"
"\n",
spec,
spec,
time.as<std::chrono::seconds>().count(),
result_string,
inner_block);
}
std::string InstallSummary::xunit_results() const
{
std::string xunit_doc;
for (auto&& result : results)
{
std::string inner_block;
const char* result_string = "";
switch (result.build_result.code)
{
case BuildResult::POST_BUILD_CHECKS_FAILED:
case BuildResult::FILE_CONFLICTS:
case BuildResult::BUILD_FAILED:
result_string = "Fail";
inner_block = Strings::format("<failure><message><![CDATA[%s]]></message></failure>",
to_string(result.build_result.code));
break;
case BuildResult::EXCLUDED:
case BuildResult::CASCADED_DUE_TO_MISSING_DEPENDENCIES:
result_string = "Skip";
inner_block =
Strings::format("<reason><![CDATA[%s]]></reason>", to_string(result.build_result.code));
break;
case BuildResult::SUCCEEDED: result_string = "Pass"; break;
default: Checks::exit_fail(VCPKG_LINE_INFO);
}
xunit_doc += Strings::format(R"(<test name="%s" method="%s" time="%lld" result="%s">%s</test>)"
"\n",
result.spec,
result.spec,
result.timing.as<std::chrono::seconds>().count(),
result_string,
inner_block);
xunit_doc += xunit_result(result.spec, result.timing, result.build_result.code);
}
return xunit_doc;
}

View File

@ -158,6 +158,16 @@ namespace vcpkg
return std::move(control_file);
}
Optional<const FeatureParagraph&> SourceControlFile::find_feature(const std::string& featurename) const
{
auto it = Util::find_if(feature_paragraphs,
[&](const std::unique_ptr<FeatureParagraph>& p) { return p->name == featurename; });
if (it != feature_paragraphs.end())
return **it;
else
return nullopt;
}
Dependency Dependency::parse_dependency(std::string name, std::string qualifier)
{
Dependency dep;