[vcpkg] Implement versions db generator (#13777)

* [vcpkg] Add script to generate ports versions history

* [vcpkg] Fix formatting

* Fetch port versions from commit ID

* Use global --x-json switch

* Use --no-checkout when cloning secondary instance

* Clone from local repository instead of from GitHub

* Use CmdLineBuilder to build git commands

* Use CmdLineBuilder and reduce repeated code

* Fetch version at baseline and code cleanup

* Guess version scheme from old CONTROL files

* Rename version db generator script

* Simplify x-history json output

* Use CONTROL/manifest parsers on x-history

* Use git-tree instaed of commit-id

* Remove 'ports' field from root object

* Clean up code

* More code cleanup

* Improve port version detection

* Improve generator logging

* Do not ignore parsing errors in CONTROL files

* PR review comments in Python script

* Fix subprocess.run() calls

* Make `canonicalize()` return error instead of terminating

* [vcpkg] Add tests for new test_parse_control_file paths

* Remove unnecessary std::move() calls

* Fix formatting

* Python formatting

Co-authored-by: Robert Schumacher <roschuma@microsoft.com>
This commit is contained in:
Victor Romero 2020-10-26 10:24:30 -07:00 committed by GitHub
parent ac2ddd5f05
commit e9f8cc67a5
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
12 changed files with 539 additions and 107 deletions

View File

@ -0,0 +1,71 @@
import os
import os.path
import sys
import subprocess
import json
import time
from subprocess import CalledProcessError
from json.decoder import JSONDecodeError
from pathlib import Path
SCRIPT_DIRECTORY = os.path.dirname(os.path.abspath(__file__))
def get_current_git_ref():
output = subprocess.run(['git.exe', '-C', SCRIPT_DIRECTORY, 'rev-parse', '--verify', 'HEAD'],
capture_output=True,
encoding='utf-8')
if output.returncode == 0:
return output.stdout.strip()
print(f"Failed to get git ref:", output.stderr.strip(), file=sys.stderr)
return None
def generate_port_versions_db(ports_path, db_path, revision):
start_time = time.time()
port_names = [item for item in os.listdir(ports_path) if os.path.isdir(os.path.join(ports_path, item))]
total_count = len(port_names)
for counter, port_name in enumerate(port_names):
containing_dir = os.path.join(db_path, f'{port_name[0]}-')
os.makedirs(containing_dir, exist_ok=True)
output_filepath = os.path.join(containing_dir, f'{port_name}.json')
if not os.path.exists(output_filepath):
output = subprocess.run(
[os.path.join(SCRIPT_DIRECTORY, '../vcpkg'), 'x-history', port_name, '--x-json'],
capture_output=True, encoding='utf-8')
if output.returncode == 0:
try:
versions_object = json.loads(output.stdout)
with open(output_filepath, 'w') as output_file:
json.dump(versions_object, output_file)
except JSONDecodeError:
print(f'Maformed JSON from vcpkg x-history {port_name}: ', output.stdout.strip(), file=sys.stderr)
else:
print(f'x-history {port_name} failed: ', output.stdout.strip(), file=sys.stderr)
# This should be replaced by a progress bar
if counter > 0 and counter % 100 == 0:
elapsed_time = time.time() - start_time
print(f'Processed {counter} out of {total_count}. Elapsed time: {elapsed_time:.2f} seconds')
rev_file = os.path.join(db_path, revision)
Path(rev_file).touch()
elapsed_time = time.time() - start_time
print(f'Processed {total_count} total ports. Elapsed time: {elapsed_time:.2f} seconds')
def main(ports_path, db_path):
revision = get_current_git_ref()
if not revision:
print('Couldn\'t fetch current Git revision', file=sys.stderr)
sys.exit(1)
rev_file = os.path.join(db_path, revision)
if os.path.exists(rev_file):
print(f'Database files already exist for commit {revision}')
sys.exit(0)
generate_port_versions_db(ports_path=ports_path, db_path=db_path, revision=revision)
if __name__ == "__main__":
main(ports_path=os.path.join(SCRIPT_DIRECTORY, '../ports'),
db_path=os.path.join(SCRIPT_DIRECTORY, '../port_versions'))

View File

@ -17,12 +17,18 @@ namespace vcpkg::Paragraphs
ExpectedS<Paragraph> parse_single_paragraph(const std::string& str, const std::string& origin); ExpectedS<Paragraph> parse_single_paragraph(const std::string& str, const std::string& origin);
ExpectedS<Paragraph> get_single_paragraph(const Files::Filesystem& fs, const fs::path& control_path); ExpectedS<Paragraph> get_single_paragraph(const Files::Filesystem& fs, const fs::path& control_path);
ExpectedS<std::vector<Paragraph>> get_paragraphs(const Files::Filesystem& fs, const fs::path& control_path); ExpectedS<std::vector<Paragraph>> get_paragraphs(const Files::Filesystem& fs, const fs::path& control_path);
ExpectedS<std::vector<Paragraph>> get_paragraphs_text(const std::string& text, const std::string& origin);
ExpectedS<std::vector<Paragraph>> parse_paragraphs(const std::string& str, const std::string& origin); ExpectedS<std::vector<Paragraph>> parse_paragraphs(const std::string& str, const std::string& origin);
bool is_port_directory(const Files::Filesystem& fs, const fs::path& path); bool is_port_directory(const Files::Filesystem& fs, const fs::path& path);
Parse::ParseExpected<SourceControlFile> try_load_port(const Files::Filesystem& fs, const fs::path& path); Parse::ParseExpected<SourceControlFile> try_load_port(const Files::Filesystem& fs, const fs::path& path);
Parse::ParseExpected<SourceControlFile> try_load_port_text(const std::string& text,
const std::string& origin,
bool is_manifest);
ExpectedS<BinaryControlFile> try_load_cached_package(const VcpkgPaths& paths, const PackageSpec& spec); ExpectedS<BinaryControlFile> try_load_cached_package(const VcpkgPaths& paths, const PackageSpec& spec);

View File

@ -91,11 +91,14 @@ namespace vcpkg
return ret; return ret;
} }
static Parse::ParseExpected<SourceControlFile> parse_manifest_object(const std::string& origin,
const Json::Object& object);
static Parse::ParseExpected<SourceControlFile> parse_manifest_file(const fs::path& path_to_manifest, static Parse::ParseExpected<SourceControlFile> parse_manifest_file(const fs::path& path_to_manifest,
const Json::Object& object); const Json::Object& object);
static Parse::ParseExpected<SourceControlFile> parse_control_file( static Parse::ParseExpected<SourceControlFile> parse_control_file(
const fs::path& path_to_control, std::vector<Parse::Paragraph>&& control_paragraphs); const std::string& origin, std::vector<Parse::Paragraph>&& control_paragraphs);
// Always non-null in non-error cases // Always non-null in non-error cases
std::unique_ptr<SourceParagraph> core_paragraph; std::unique_ptr<SourceParagraph> core_paragraph;

View File

@ -1,14 +1,12 @@
#pragma once #pragma once
#include <vcpkg/fwd/vcpkgpaths.h>
namespace vcpkg::Versions namespace vcpkg::Versions
{ {
enum class Scheme enum class Scheme
{ {
String,
Relaxed, Relaxed,
Semver, Semver,
Date Date,
String
}; };
} }

View File

@ -75,7 +75,7 @@ Build-Depends: bzip
)", )",
"<testdata>"); "<testdata>");
REQUIRE(pghs.has_value()); REQUIRE(pghs.has_value());
auto maybe_scf = SourceControlFile::parse_control_file(fs::path(), std::move(*pghs.get())); auto maybe_scf = SourceControlFile::parse_control_file(fs::u8string(fs::path()), std::move(*pghs.get()));
REQUIRE(maybe_scf.has_value()); REQUIRE(maybe_scf.has_value());
SourceControlFileLocation scfl{std::move(*maybe_scf.get()), fs::path()}; SourceControlFileLocation scfl{std::move(*maybe_scf.get()), fs::path()};
@ -255,7 +255,7 @@ Description: a spiffy compression library wrapper
)", )",
"<testdata>"); "<testdata>");
REQUIRE(pghs.has_value()); REQUIRE(pghs.has_value());
auto maybe_scf = SourceControlFile::parse_control_file(fs::path(), std::move(*pghs.get())); auto maybe_scf = SourceControlFile::parse_control_file(fs::u8string(fs::path()), std::move(*pghs.get()));
REQUIRE(maybe_scf.has_value()); REQUIRE(maybe_scf.has_value());
SourceControlFileLocation scfl{std::move(*maybe_scf.get()), fs::path()}; SourceControlFileLocation scfl{std::move(*maybe_scf.get()), fs::path()};
plan.install_actions.push_back(Dependencies::InstallPlanAction()); plan.install_actions.push_back(Dependencies::InstallPlanAction());
@ -278,7 +278,7 @@ Description: a spiffy compression library wrapper
)", )",
"<testdata>"); "<testdata>");
REQUIRE(pghs2.has_value()); REQUIRE(pghs2.has_value());
auto maybe_scf2 = SourceControlFile::parse_control_file(fs::path(), std::move(*pghs2.get())); auto maybe_scf2 = SourceControlFile::parse_control_file(fs::u8string(fs::path()), std::move(*pghs2.get()));
REQUIRE(maybe_scf2.has_value()); REQUIRE(maybe_scf2.has_value());
SourceControlFileLocation scfl2{std::move(*maybe_scf2.get()), fs::path()}; SourceControlFileLocation scfl2{std::move(*maybe_scf2.get()), fs::path()};
plan.install_actions.push_back(Dependencies::InstallPlanAction()); plan.install_actions.push_back(Dependencies::InstallPlanAction());

View File

@ -331,9 +331,9 @@ TEST_CASE ("Serialize all the ports", "[manifests]")
auto pghs = Paragraphs::parse_paragraphs(contents, fs::u8string(control)); auto pghs = Paragraphs::parse_paragraphs(contents, fs::u8string(control));
REQUIRE(pghs); REQUIRE(pghs);
scfs.push_back(std::move( scfs.push_back(std::move(*SourceControlFile::parse_control_file(
*SourceControlFile::parse_control_file(control, std::move(pghs).value_or_exit(VCPKG_LINE_INFO)) fs::u8string(control), std::move(pghs).value_or_exit(VCPKG_LINE_INFO))
.value_or_exit(VCPKG_LINE_INFO))); .value_or_exit(VCPKG_LINE_INFO)));
} }
else if (fs.exists(manifest)) else if (fs.exists(manifest))
{ {

View File

@ -51,6 +51,38 @@ TEST_CASE ("SourceParagraph construct minimum", "[paragraph]")
REQUIRE(pgh.core_paragraph->dependencies.size() == 0); REQUIRE(pgh.core_paragraph->dependencies.size() == 0);
} }
TEST_CASE ("SourceParagraph construct invalid", "[paragraph]")
{
auto m_pgh = test_parse_control_file({{
{"Source", "zlib"},
{"Version", "1.2.8"},
{"Build-Depends", "1.2.8"},
}});
REQUIRE(!m_pgh.has_value());
m_pgh = test_parse_control_file({{
{"Source", "zlib"},
{"Version", "1.2.8"},
{"Default-Features", "1.2.8"},
}});
REQUIRE(!m_pgh.has_value());
m_pgh = test_parse_control_file({
{
{"Source", "zlib"},
{"Version", "1.2.8"},
},
{
{"Feature", "a"},
{"Build-Depends", "1.2.8"},
},
});
REQUIRE(!m_pgh.has_value());
}
TEST_CASE ("SourceParagraph construct maximum", "[paragraph]") TEST_CASE ("SourceParagraph construct maximum", "[paragraph]")
{ {
auto m_pgh = test_parse_control_file({{ auto m_pgh = test_parse_control_file({{
@ -76,6 +108,24 @@ TEST_CASE ("SourceParagraph construct maximum", "[paragraph]")
REQUIRE(pgh.core_paragraph->default_features[0] == "df"); REQUIRE(pgh.core_paragraph->default_features[0] == "df");
} }
TEST_CASE ("SourceParagraph construct feature", "[paragraph]")
{
auto m_pgh = test_parse_control_file({
{
{"Source", "s"},
{"Version", "v"},
},
{{"Feature", "f"}, {"Description", "d2"}, {"Build-Depends", "bd2"}},
});
REQUIRE(m_pgh.has_value());
auto& pgh = **m_pgh.get();
REQUIRE(pgh.feature_paragraphs.size() == 1);
REQUIRE(pgh.feature_paragraphs[0]->name == "f");
REQUIRE(pgh.feature_paragraphs[0]->description == std::vector<std::string>{"d2"});
REQUIRE(pgh.feature_paragraphs[0]->dependencies.size() == 1);
}
TEST_CASE ("SourceParagraph two dependencies", "[paragraph]") TEST_CASE ("SourceParagraph two dependencies", "[paragraph]")
{ {
auto m_pgh = test_parse_control_file({{ auto m_pgh = test_parse_control_file({{

View File

@ -80,8 +80,8 @@ namespace
paragraphs.error()); paragraphs.error());
return {}; return {};
} }
auto scf_res = auto scf_res = SourceControlFile::parse_control_file(fs::u8string(control_path),
SourceControlFile::parse_control_file(control_path, std::move(paragraphs).value_or_exit(VCPKG_LINE_INFO)); std::move(paragraphs).value_or_exit(VCPKG_LINE_INFO));
if (!scf_res) if (!scf_res)
{ {
System::printf(System::Color::error, "Failed to parse control file: %s\n", control_path_string); System::printf(System::Color::error, "Failed to parse control file: %s\n", control_path_string);

View File

@ -1,77 +1,257 @@
#include <vcpkg/base/json.h>
#include <vcpkg/base/system.print.h> #include <vcpkg/base/system.print.h>
#include <vcpkg/base/system.process.h> #include <vcpkg/base/system.process.h>
#include <vcpkg/base/util.h> #include <vcpkg/base/util.h>
#include <vcpkg/commands.porthistory.h> #include <vcpkg/commands.porthistory.h>
#include <vcpkg/help.h> #include <vcpkg/help.h>
#include <vcpkg/paragraphs.h>
#include <vcpkg/tools.h> #include <vcpkg/tools.h>
#include <vcpkg/vcpkgcmdarguments.h> #include <vcpkg/vcpkgcmdarguments.h>
#include <vcpkg/vcpkgpaths.h> #include <vcpkg/vcpkgpaths.h>
#include <vcpkg/versions.h>
namespace vcpkg::Commands::PortHistory namespace vcpkg::Commands::PortHistory
{ {
struct PortControlVersion namespace
{ {
std::string commit_id; struct HistoryVersion
std::string version;
std::string date;
};
static System::ExitCodeAndOutput run_git_command(const VcpkgPaths& paths, const std::string& cmd)
{
const fs::path& git_exe = paths.get_tool_exe(Tools::GIT);
const fs::path dot_git_dir = paths.root / ".git";
const std::string full_cmd =
Strings::format(R"("%s" --git-dir="%s" %s)", fs::u8string(git_exe), fs::u8string(dot_git_dir), cmd);
auto output = System::cmd_execute_and_capture_output(full_cmd);
Checks::check_exit(VCPKG_LINE_INFO, output.exit_code == 0, "Failed to run command: %s", full_cmd);
return output;
}
static std::string get_version_from_commit(const VcpkgPaths& paths,
const std::string& commit_id,
const std::string& port_name)
{
const std::string cmd = Strings::format(R"(show %s:ports/%s/CONTROL)", commit_id, port_name);
auto output = run_git_command(paths, cmd);
const auto version = Strings::find_at_most_one_enclosed(output.output, "\nVersion: ", "\n");
const auto port_version = Strings::find_at_most_one_enclosed(output.output, "\nPort-Version: ", "\n");
Checks::check_exit(VCPKG_LINE_INFO, version.has_value(), "CONTROL file does not have a 'Version' field");
if (auto pv = port_version.get())
{ {
return Strings::format("%s#%s", version.get()->to_string(), pv->to_string()); std::string port_name;
std::string git_tree;
std::string commit_id;
std::string commit_date;
std::string version_string;
std::string version;
int port_version;
Versions::Scheme scheme;
};
const System::ExitCodeAndOutput run_git_command_inner(const VcpkgPaths& paths,
const fs::path& dot_git_directory,
const fs::path& working_directory,
const std::string& cmd)
{
const fs::path& git_exe = paths.get_tool_exe(Tools::GIT);
System::CmdLineBuilder builder;
builder.path_arg(git_exe)
.string_arg(Strings::concat("--git-dir=", fs::u8string(dot_git_directory)))
.string_arg(Strings::concat("--work-tree=", fs::u8string(working_directory)));
const std::string full_cmd = Strings::concat(builder.extract(), " ", cmd);
const auto output = System::cmd_execute_and_capture_output(full_cmd);
return output;
} }
return version.get()->to_string(); const System::ExitCodeAndOutput run_git_command(const VcpkgPaths& paths, const std::string& cmd)
}
static std::vector<PortControlVersion> read_versions_from_log(const VcpkgPaths& paths, const std::string& port_name)
{
const std::string cmd =
Strings::format(R"(log --format="%%H %%cd" --date=short --left-only -- ports/%s/.)", port_name);
auto output = run_git_command(paths, cmd);
auto commits = Util::fmap(
Strings::split(output.output, '\n'), [](const std::string& line) -> auto {
auto parts = Strings::split(line, ' ');
return std::make_pair(parts[0], parts[1]);
});
std::vector<PortControlVersion> ret;
std::string last_version;
for (auto&& commit_date_pair : commits)
{ {
const std::string version = get_version_from_commit(paths, commit_date_pair.first, port_name); const fs::path& work_dir = paths.root;
if (last_version != version) const fs::path dot_git_dir = paths.root / ".git";
return run_git_command_inner(paths, dot_git_dir, work_dir, cmd);
}
bool is_date(const std::string& version_string)
{
// The date regex is not complete, it matches strings that look like dates,
// e.g.: 2020-99-99.
//
// The regex has two capture groups:
// * Date: "^([0-9]{4,}[-][0-9]{2}[-][0-9]{2})", it matches strings that resemble YYYY-MM-DD.
// It does not validate that MM <= 12, or that DD is possible with the given MM.
// YYYY should be AT LEAST 4 digits, for some kind of "future proofing".
std::regex re("^([0-9]{4,}[-][0-9]{2}[-][0-9]{2})((?:[.|-][0-9a-zA-Z]+)*)$");
return std::regex_match(version_string, re);
}
bool is_date_without_tags(const std::string& version_string)
{
std::regex re("^([0-9]{4,}[-][0-9]{2}[-][0-9]{2})$");
return std::regex_match(version_string, re);
}
bool is_semver(const std::string& version_string)
{
// This is the "official" SemVer regex, taken from:
// https://semver.org/#is-there-a-suggested-regular-expression-regex-to-check-a-semver-string
std::regex re("^(0|[1-9]\\d*)\\.(0|[1-9]\\d*)\\.(0|[1-9]\\d*)(?:-((?:0|[1-9]\\d*|\\d*[a-zA-Z-][0-9a-zA-Z-]*"
")(?:\\.(?:0|["
"1-9]\\d*|\\d*[a-zA-Z-][0-9a-zA-Z-]*))*))?(?:\\+([0-9a-zA-Z-]+(?:\\.[0-9a-zA-Z-]+)*))?$");
return std::regex_match(version_string, re);
}
bool is_semver_relaxed(const std::string& version_string)
{
std::regex re("^(?:[0-9a-zA-Z]+)\\.(?:[0-9a-zA-Z]+)\\.(?:[0-9a-zA-Z]+)(?:[\\.|-|\\+][0-9a-zA-Z]+)*$");
return std::regex_match(version_string, re);
}
const Versions::Scheme guess_version_scheme(const std::string& version_string)
{
if (is_date(version_string))
{ {
ret.emplace_back(PortControlVersion{commit_date_pair.first, version, commit_date_pair.second}); return Versions::Scheme::Date;
last_version = version;
} }
if (is_semver(version_string) || is_semver_relaxed(version_string))
{
return Versions::Scheme::Relaxed;
}
return Versions::Scheme::String;
}
std::pair<std::string, int> clean_version_string(const std::string& version_string,
int port_version,
bool from_manifest)
{
// Manifest files and ports that use the `Port-Version` field are assumed to have a clean version string
// already.
if (from_manifest || port_version > 0)
{
return std::make_pair(version_string, port_version);
}
std::string clean_version = version_string;
int clean_port_version = 0;
const auto index = version_string.find_last_of('-');
if (index != std::string::npos)
{
// Very lazy check to keep date versions untouched
if (!is_date_without_tags(version_string))
{
auto maybe_port_version = version_string.substr(index + 1);
clean_version.resize(index);
try
{
clean_port_version = std::stoi(maybe_port_version);
}
catch (std::exception&)
{
// If not convertible to int consider last fragment as part of version string
clean_version = version_string;
}
}
}
return std::make_pair(clean_version, clean_port_version);
}
vcpkg::Optional<HistoryVersion> get_version_from_text(const std::string& text,
const std::string& git_tree,
const std::string& commit_id,
const std::string& commit_date,
const std::string& port_name,
bool is_manifest)
{
auto res = Paragraphs::try_load_port_text(text, Strings::concat(commit_id, ":", port_name), is_manifest);
if (const auto& maybe_scf = res.get())
{
if (const auto& scf = maybe_scf->get())
{
// TODO: Get clean version name and port version
const auto version_string = scf->core_paragraph->version;
const auto clean_version =
clean_version_string(version_string, scf->core_paragraph->port_version, is_manifest);
// SCF to HistoryVersion
return HistoryVersion{
port_name,
git_tree,
commit_id,
commit_date,
Strings::concat(clean_version.first, "#", std::to_string(clean_version.second)),
clean_version.first,
clean_version.second,
guess_version_scheme(clean_version.first)};
}
}
return nullopt;
}
vcpkg::Optional<HistoryVersion> get_version_from_commit(const VcpkgPaths& paths,
const std::string& commit_id,
const std::string& commit_date,
const std::string& port_name)
{
const std::string rev_parse_cmd = Strings::format("rev-parse %s:ports/%s", commit_id, port_name);
auto rev_parse_output = run_git_command(paths, rev_parse_cmd);
if (rev_parse_output.exit_code == 0)
{
// Remove newline character
const auto git_tree = Strings::trim(std::move(rev_parse_output.output));
// Do we have a manifest file?
const std::string manifest_cmd = Strings::format(R"(show %s:vcpkg.json)", git_tree, port_name);
auto manifest_output = run_git_command(paths, manifest_cmd);
if (manifest_output.exit_code == 0)
{
return get_version_from_text(
manifest_output.output, git_tree, commit_id, commit_date, port_name, true);
}
const std::string cmd = Strings::format(R"(show %s:CONTROL)", git_tree, commit_id, port_name);
auto control_output = run_git_command(paths, cmd);
if (control_output.exit_code == 0)
{
return get_version_from_text(
control_output.output, git_tree, commit_id, commit_date, port_name, false);
}
}
return nullopt;
}
std::vector<HistoryVersion> read_versions_from_log(const VcpkgPaths& paths, const std::string& port_name)
{
// log --format="%H %cd" --date=short --left-only -- ports/{port_name}/.
System::CmdLineBuilder builder;
builder.string_arg("log");
builder.string_arg("--format=%H %cd");
builder.string_arg("--date=short");
builder.string_arg("--left-only");
builder.string_arg("--"); // Begin pathspec
builder.string_arg(Strings::format("ports/%s/.", port_name));
const auto output = run_git_command(paths, builder.extract());
auto commits = Util::fmap(
Strings::split(output.output, '\n'), [](const std::string& line) -> auto {
auto parts = Strings::split(line, ' ');
return std::make_pair(parts[0], parts[1]);
});
std::vector<HistoryVersion> ret;
std::string last_version;
for (auto&& commit_date_pair : commits)
{
auto maybe_version =
get_version_from_commit(paths, commit_date_pair.first, commit_date_pair.second, port_name);
if (maybe_version.has_value())
{
const auto version = maybe_version.value_or_exit(VCPKG_LINE_INFO);
// Keep latest port with the current version string
if (last_version != version.version_string)
{
last_version = version.version_string;
ret.emplace_back(version);
}
}
// NOTE: Uncomment this code if you're looking for edge cases to patch in the generation.
// Otherwise, x-history simply skips "bad" versions, which is OK behavior.
// else
//{
// Checks::exit_with_message(VCPKG_LINE_INFO, "Failed to get version from %s:%s",
// commit_date_pair.first, port_name);
//}
}
return ret;
} }
return ret;
} }
const CommandStructure COMMAND_STRUCTURE = { const CommandStructure COMMAND_STRUCTURE = {
@ -84,13 +264,47 @@ namespace vcpkg::Commands::PortHistory
void perform_and_exit(const VcpkgCmdArguments& args, const VcpkgPaths& paths) void perform_and_exit(const VcpkgCmdArguments& args, const VcpkgPaths& paths)
{ {
(void)args.parse_arguments(COMMAND_STRUCTURE); const ParsedArguments options = args.parse_arguments(COMMAND_STRUCTURE);
std::string port_name = args.command_arguments.at(0); std::string port_name = args.command_arguments.at(0);
std::vector<PortControlVersion> versions = read_versions_from_log(paths, port_name); std::vector<HistoryVersion> versions = read_versions_from_log(paths, port_name);
System::print2(" version date vcpkg commit\n");
for (auto&& version : versions) if (args.output_json())
{ {
System::printf("%20.20s %s %s\n", version.version, version.date, version.commit_id); Json::Array versions_json;
for (auto&& version : versions)
{
Json::Object object;
object.insert("git-tree", Json::Value::string(version.git_tree));
switch (version.scheme)
{
case Versions::Scheme::Semver: // falls through
case Versions::Scheme::Relaxed:
object.insert("version", Json::Value::string(version.version));
break;
case Versions::Scheme::Date:
object.insert("version-date", Json::Value::string(version.version));
break;
case Versions::Scheme::String: // falls through
default: object.insert("version-string", Json::Value::string(version.version)); break;
}
object.insert("port-version", Json::Value::integer(version.port_version));
versions_json.push_back(std::move(object));
}
Json::Object root;
root.insert("versions", versions_json);
auto json_string = Json::stringify(root, vcpkg::Json::JsonStyle::with_spaces(2));
System::printf("%s\n", json_string);
}
else
{
System::print2(" version date vcpkg commit\n");
for (auto&& version : versions)
{
System::printf("%20.20s %s %s\n", version.version_string, version.commit_date, version.commit_id);
}
} }
Checks::exit_success(VCPKG_LINE_INFO); Checks::exit_success(VCPKG_LINE_INFO);
} }

View File

@ -241,6 +241,11 @@ namespace vcpkg::Paragraphs
return contents.error().message(); return contents.error().message();
} }
ExpectedS<std::vector<Paragraph>> get_paragraphs_text(const std::string& text, const std::string& origin)
{
return parse_paragraphs(text, origin);
}
ExpectedS<std::vector<Paragraph>> get_paragraphs(const Files::Filesystem& fs, const fs::path& control_path) ExpectedS<std::vector<Paragraph>> get_paragraphs(const Files::Filesystem& fs, const fs::path& control_path)
{ {
const Expected<std::string> contents = fs.read_contents(control_path); const Expected<std::string> contents = fs.read_contents(control_path);
@ -262,36 +267,73 @@ namespace vcpkg::Paragraphs
return fs.exists(path / fs::u8path("CONTROL")) || fs.exists(path / fs::u8path("vcpkg.json")); return fs.exists(path / fs::u8path("CONTROL")) || fs.exists(path / fs::u8path("vcpkg.json"));
} }
static ParseExpected<SourceControlFile> try_load_manifest(const Files::Filesystem& fs, static ParseExpected<SourceControlFile> try_load_manifest_object(
const std::string& port_name, const std::string& origin,
const fs::path& path_to_manifest, const ExpectedT<std::pair<vcpkg::Json::Value, vcpkg::Json::JsonStyle>, std::unique_ptr<Parse::IParseError>>&
std::error_code& ec) res)
{ {
auto error_info = std::make_unique<ParseControlErrorInfo>(); auto error_info = std::make_unique<ParseControlErrorInfo>();
auto res = Json::parse_file(fs, path_to_manifest, ec);
if (ec) return error_info;
if (auto val = res.get()) if (auto val = res.get())
{ {
if (val->first.is_object()) if (val->first.is_object())
{ {
return SourceControlFile::parse_manifest_file(path_to_manifest, val->first.object()); return SourceControlFile::parse_manifest_object(origin, val->first.object());
} }
else else
{ {
error_info->name = port_name; error_info->name = origin;
error_info->error = "Manifest files must have a top-level object"; error_info->error = "Manifest files must have a top-level object";
return error_info; return error_info;
} }
} }
else else
{ {
error_info->name = port_name; error_info->name = origin;
error_info->error = res.error()->format(); error_info->error = res.error()->format();
return error_info; return error_info;
} }
} }
static ParseExpected<SourceControlFile> try_load_manifest_text(const std::string& text, const std::string& origin)
{
auto res = Json::parse(text);
return try_load_manifest_object(origin, res);
}
static ParseExpected<SourceControlFile> try_load_manifest(const Files::Filesystem& fs,
const std::string& port_name,
const fs::path& path_to_manifest,
std::error_code& ec)
{
(void)port_name;
auto error_info = std::make_unique<ParseControlErrorInfo>();
auto res = Json::parse_file(fs, path_to_manifest, ec);
if (ec) return error_info;
return try_load_manifest_object(fs::u8string(path_to_manifest), res);
}
ParseExpected<SourceControlFile> try_load_port_text(const std::string& text,
const std::string& origin,
bool is_manifest)
{
if (is_manifest)
{
return try_load_manifest_text(text, origin);
}
ExpectedS<std::vector<Paragraph>> pghs = get_paragraphs_text(text, origin);
if (auto vector_pghs = pghs.get())
{
return SourceControlFile::parse_control_file(origin, std::move(*vector_pghs));
}
auto error_info = std::make_unique<ParseControlErrorInfo>();
error_info->name = fs::u8string(origin);
error_info->error = pghs.error();
return error_info;
}
ParseExpected<SourceControlFile> try_load_port(const Files::Filesystem& fs, const fs::path& path) ParseExpected<SourceControlFile> try_load_port(const Files::Filesystem& fs, const fs::path& path)
{ {
const auto path_to_manifest = path / fs::u8path("vcpkg.json"); const auto path_to_manifest = path / fs::u8path("vcpkg.json");
@ -318,7 +360,7 @@ namespace vcpkg::Paragraphs
ExpectedS<std::vector<Paragraph>> pghs = get_paragraphs(fs, path_to_control); ExpectedS<std::vector<Paragraph>> pghs = get_paragraphs(fs, path_to_control);
if (auto vector_pghs = pghs.get()) if (auto vector_pghs = pghs.get())
{ {
return SourceControlFile::parse_control_file(path_to_control, std::move(*vector_pghs)); return SourceControlFile::parse_control_file(fs::u8string(path_to_control), std::move(*vector_pghs));
} }
auto error_info = std::make_unique<ParseControlErrorInfo>(); auto error_info = std::make_unique<ParseControlErrorInfo>();
error_info->name = fs::u8string(path.filename()); error_info->name = fs::u8string(path.filename());

View File

@ -221,7 +221,7 @@ namespace vcpkg
fpgh.extra_info.sort_keys(); fpgh.extra_info.sort_keys();
} }
void operator()(SourceControlFile& scf) const [[nodiscard]] std::unique_ptr<ParseControlErrorInfo> operator()(SourceControlFile& scf) const
{ {
(*this)(*scf.core_paragraph); (*this)(*scf.core_paragraph);
std::for_each(scf.feature_paragraphs.begin(), scf.feature_paragraphs.end(), *this); std::for_each(scf.feature_paragraphs.begin(), scf.feature_paragraphs.end(), *this);
@ -231,20 +231,21 @@ namespace vcpkg
std::adjacent_find(scf.feature_paragraphs.begin(), scf.feature_paragraphs.end(), FeatureEqual{}); std::adjacent_find(scf.feature_paragraphs.begin(), scf.feature_paragraphs.end(), FeatureEqual{});
if (adjacent_equal != scf.feature_paragraphs.end()) if (adjacent_equal != scf.feature_paragraphs.end())
{ {
Checks::exit_with_message(VCPKG_LINE_INFO, auto error_info = std::make_unique<ParseControlErrorInfo>();
R"(Multiple features with the same name for port %s: %s error_info->name = scf.core_paragraph->name;
error_info->error = Strings::format(R"(Multiple features with the same name for port %s: %s
This is invalid; please make certain that features have distinct names.)", This is invalid; please make certain that features have distinct names.)",
scf.core_paragraph->name, scf.core_paragraph->name,
(*adjacent_equal)->name); (*adjacent_equal)->name);
return error_info;
} }
return nullptr;
} }
} canonicalize{}; } canonicalize{};
} }
static ParseExpected<SourceParagraph> parse_source_paragraph(const fs::path& path_to_control, Paragraph&& fields) static ParseExpected<SourceParagraph> parse_source_paragraph(const std::string& origin, Paragraph&& fields)
{ {
auto origin = fs::u8string(path_to_control);
ParagraphParser parser(std::move(fields)); ParagraphParser parser(std::move(fields));
auto spgh = std::make_unique<SourceParagraph>(); auto spgh = std::make_unique<SourceParagraph>();
@ -276,10 +277,35 @@ namespace vcpkg
TextRowCol textrowcol; TextRowCol textrowcol;
std::string buf; std::string buf;
parser.optional_field(SourceParagraphFields::BUILD_DEPENDS, {buf, textrowcol}); parser.optional_field(SourceParagraphFields::BUILD_DEPENDS, {buf, textrowcol});
spgh->dependencies = parse_dependencies_list(buf, origin, textrowcol).value_or_exit(VCPKG_LINE_INFO);
auto maybe_dependencies = parse_dependencies_list(buf, origin, textrowcol);
if (maybe_dependencies.has_value())
{
spgh->dependencies = maybe_dependencies.value_or_exit(VCPKG_LINE_INFO);
}
else
{
auto error_info = std::make_unique<ParseControlErrorInfo>();
error_info->name = origin;
error_info->error = maybe_dependencies.error();
return error_info;
}
buf.clear(); buf.clear();
parser.optional_field(SourceParagraphFields::DEFAULT_FEATURES, {buf, textrowcol}); parser.optional_field(SourceParagraphFields::DEFAULT_FEATURES, {buf, textrowcol});
spgh->default_features = parse_default_features_list(buf, origin, textrowcol).value_or_exit(VCPKG_LINE_INFO);
auto maybe_default_features = parse_default_features_list(buf, origin, textrowcol);
if (maybe_default_features.has_value())
{
spgh->default_features = maybe_default_features.value_or_exit(VCPKG_LINE_INFO);
}
else
{
auto error_info = std::make_unique<ParseControlErrorInfo>();
error_info->name = origin;
error_info->error = maybe_default_features.error();
return error_info;
}
auto supports_expr = parser.optional_field(SourceParagraphFields::SUPPORTS); auto supports_expr = parser.optional_field(SourceParagraphFields::SUPPORTS);
if (!supports_expr.empty()) if (!supports_expr.empty())
@ -304,9 +330,8 @@ namespace vcpkg
return spgh; return spgh;
} }
static ParseExpected<FeatureParagraph> parse_feature_paragraph(const fs::path& path_to_control, Paragraph&& fields) static ParseExpected<FeatureParagraph> parse_feature_paragraph(const std::string& origin, Paragraph&& fields)
{ {
auto origin = fs::u8string(path_to_control);
ParagraphParser parser(std::move(fields)); ParagraphParser parser(std::move(fields));
auto fpgh = std::make_unique<FeatureParagraph>(); auto fpgh = std::make_unique<FeatureParagraph>();
@ -315,9 +340,19 @@ namespace vcpkg
fpgh->description = Strings::split(parser.required_field(SourceParagraphFields::DESCRIPTION), '\n'); fpgh->description = Strings::split(parser.required_field(SourceParagraphFields::DESCRIPTION), '\n');
trim_all(fpgh->description); trim_all(fpgh->description);
fpgh->dependencies = auto maybe_dependencies =
parse_dependencies_list(parser.optional_field(SourceParagraphFields::BUILD_DEPENDS), origin) parse_dependencies_list(parser.optional_field(SourceParagraphFields::BUILD_DEPENDS), origin);
.value_or_exit(VCPKG_LINE_INFO); if (maybe_dependencies.has_value())
{
fpgh->dependencies = maybe_dependencies.value_or_exit(VCPKG_LINE_INFO);
}
else
{
auto error_info = std::make_unique<ParseControlErrorInfo>();
error_info->name = origin;
error_info->error = maybe_dependencies.error();
return error_info;
}
auto err = parser.error_info(fpgh->name.empty() ? origin : fpgh->name); auto err = parser.error_info(fpgh->name.empty() ? origin : fpgh->name);
if (err) if (err)
@ -327,18 +362,18 @@ namespace vcpkg
} }
ParseExpected<SourceControlFile> SourceControlFile::parse_control_file( ParseExpected<SourceControlFile> SourceControlFile::parse_control_file(
const fs::path& path_to_control, std::vector<Parse::Paragraph>&& control_paragraphs) const std::string& origin, std::vector<Parse::Paragraph>&& control_paragraphs)
{ {
if (control_paragraphs.size() == 0) if (control_paragraphs.size() == 0)
{ {
auto ret = std::make_unique<Parse::ParseControlErrorInfo>(); auto ret = std::make_unique<Parse::ParseControlErrorInfo>();
ret->name = fs::u8string(path_to_control); ret->name = origin;
return ret; return ret;
} }
auto control_file = std::make_unique<SourceControlFile>(); auto control_file = std::make_unique<SourceControlFile>();
auto maybe_source = parse_source_paragraph(path_to_control, std::move(control_paragraphs.front())); auto maybe_source = parse_source_paragraph(origin, std::move(control_paragraphs.front()));
if (const auto source = maybe_source.get()) if (const auto source = maybe_source.get())
control_file->core_paragraph = std::move(*source); control_file->core_paragraph = std::move(*source);
else else
@ -348,14 +383,17 @@ namespace vcpkg
for (auto&& feature_pgh : control_paragraphs) for (auto&& feature_pgh : control_paragraphs)
{ {
auto maybe_feature = parse_feature_paragraph(path_to_control, std::move(feature_pgh)); auto maybe_feature = parse_feature_paragraph(origin, std::move(feature_pgh));
if (const auto feature = maybe_feature.get()) if (const auto feature = maybe_feature.get())
control_file->feature_paragraphs.emplace_back(std::move(*feature)); control_file->feature_paragraphs.emplace_back(std::move(*feature));
else else
return std::move(maybe_feature).error(); return std::move(maybe_feature).error();
} }
canonicalize(*control_file); if (auto maybe_error = canonicalize(*control_file))
{
return std::move(maybe_error);
}
return control_file; return control_file;
} }
@ -854,7 +892,10 @@ namespace vcpkg
r.optional_object_field( r.optional_object_field(
obj, FEATURES, control_file->feature_paragraphs, FeaturesFieldDeserializer::instance); obj, FEATURES, control_file->feature_paragraphs, FeaturesFieldDeserializer::instance);
canonicalize(*control_file); if (auto maybe_error = canonicalize(*control_file))
{
Checks::exit_with_message(VCPKG_LINE_INFO, maybe_error->error);
}
return std::move(control_file); return std::move(control_file);
} }
@ -879,8 +920,8 @@ namespace vcpkg
constexpr StringLiteral ManifestDeserializer::DEFAULT_FEATURES; constexpr StringLiteral ManifestDeserializer::DEFAULT_FEATURES;
constexpr StringLiteral ManifestDeserializer::SUPPORTS; constexpr StringLiteral ManifestDeserializer::SUPPORTS;
Parse::ParseExpected<SourceControlFile> SourceControlFile::parse_manifest_file(const fs::path& path_to_manifest, Parse::ParseExpected<SourceControlFile> SourceControlFile::parse_manifest_object(const std::string& origin,
const Json::Object& manifest) const Json::Object& manifest)
{ {
Json::Reader reader; Json::Reader reader;
@ -889,7 +930,7 @@ namespace vcpkg
if (!reader.errors().empty()) if (!reader.errors().empty())
{ {
auto err = std::make_unique<ParseControlErrorInfo>(); auto err = std::make_unique<ParseControlErrorInfo>();
err->name = fs::u8string(path_to_manifest); err->name = origin;
err->other_errors = std::move(reader.errors()); err->other_errors = std::move(reader.errors());
return std::move(err); return std::move(err);
} }
@ -903,6 +944,12 @@ namespace vcpkg
} }
} }
Parse::ParseExpected<SourceControlFile> SourceControlFile::parse_manifest_file(const fs::path& path_to_manifest,
const Json::Object& manifest)
{
return parse_manifest_object(fs::u8string(path_to_manifest), manifest);
}
void print_error_message(Span<const std::unique_ptr<Parse::ParseControlErrorInfo>> error_info_list) void print_error_message(Span<const std::unique_ptr<Parse::ParseControlErrorInfo>> error_info_list)
{ {
Checks::check_exit(VCPKG_LINE_INFO, error_info_list.size() > 0); Checks::check_exit(VCPKG_LINE_INFO, error_info_list.size() > 0);

View File

@ -267,6 +267,7 @@
<ClInclude Include="..\include\vcpkg\vcpkgcmdarguments.h" /> <ClInclude Include="..\include\vcpkg\vcpkgcmdarguments.h" />
<ClInclude Include="..\include\vcpkg\vcpkglib.h" /> <ClInclude Include="..\include\vcpkg\vcpkglib.h" />
<ClInclude Include="..\include\vcpkg\vcpkgpaths.h" /> <ClInclude Include="..\include\vcpkg\vcpkgpaths.h" />
<ClInclude Include="..\include\vcpkg\versions.h" />
<ClInclude Include="..\include\vcpkg\versiont.h" /> <ClInclude Include="..\include\vcpkg\versiont.h" />
<ClInclude Include="..\include\vcpkg\visualstudio.h" /> <ClInclude Include="..\include\vcpkg\visualstudio.h" />
</ItemGroup> </ItemGroup>