From 53e6588e9d4529033092c9adb43e317968b23712 Mon Sep 17 00:00:00 2001 From: Victor Romero Date: Mon, 7 Dec 2020 09:10:23 -0800 Subject: [PATCH] [vcpkg] Add SemVer and Date versioning schemes (#14889) * [vcpkg] Add semver versioning scheme * Remove unnecessary code * Fix SemVer comparison and add sorting test * Add date scheme * PR comments * Use a different column for date and semver schemes. * Use locale agnostic conversion to long * Add tests for version scheme change * Validate version strings before parsing * Format * Improve error messages * PR comments * PR comments pt. 2 --- scripts/generateBaseline.py | 50 +++ toolsrc/include/vcpkg/versions.h | 44 +++ toolsrc/src/vcpkg-test/dependencies.cpp | 456 ++++++++++++++++++++++++ toolsrc/src/vcpkg/dependencies.cpp | 62 ++-- toolsrc/src/vcpkg/versions.cpp | 217 +++++++++++ 5 files changed, 803 insertions(+), 26 deletions(-) create mode 100644 scripts/generateBaseline.py diff --git a/scripts/generateBaseline.py b/scripts/generateBaseline.py new file mode 100644 index 0000000000..45c424a7df --- /dev/null +++ b/scripts/generateBaseline.py @@ -0,0 +1,50 @@ +import os +import json +import subprocess +import sys + +SCRIPT_DIRECTORY = os.path.dirname(os.path.abspath(__file__)) + + +def generate_baseline(ports_path, output_filepath): + port_names = [item for item in os.listdir( + ports_path) if os.path.isdir(os.path.join(ports_path, item))] + port_names.sort() + + total = len(port_names) + baseline_versions = {} + for counter, port_name in enumerate(port_names): + vcpkg_exe = os.path.join(SCRIPT_DIRECTORY, '../vcpkg') + print(f'[{counter + 1}/{total}] Getting package info for {port_name}') + output = subprocess.run( + [vcpkg_exe, 'x-package-info', '--x-json', port_name], + capture_output=True, + encoding='utf-8') + + if output.returncode == 0: + package_info = json.loads(output.stdout) + port_info = package_info['results'][port_name] + + version = {} + for scheme in ['version-string', 'version-semver', 'version-date', 'version']: + if scheme in port_info: + version[scheme] = package_info['results'][port_name][scheme] + break + version['port-version'] = 0 + if 'port-version' in port_info: + version['port-version'] = port_info['port-version'] + baseline_versions[port_name] = version + else: + print(f'x-package-info --x-json {port_name} failed: ', output.stdout.strip(), file=sys.stderr) + + output = {} + output['default'] = baseline_versions + + with open(output_filepath, 'r') as output_file: + json.dump(baseline_versions, output_file) + sys.exit(0) + + +if __name__ == '__main__': + generate_baseline( + ports_path=f'{SCRIPT_DIRECTORY}/../ports', output_filepath='baseline.json') diff --git a/toolsrc/include/vcpkg/versions.h b/toolsrc/include/vcpkg/versions.h index 09df153666..19b5546eaf 100644 --- a/toolsrc/include/vcpkg/versions.h +++ b/toolsrc/include/vcpkg/versions.h @@ -6,6 +6,14 @@ namespace vcpkg::Versions { using Version = VersionT; + enum class VerComp + { + unk, + lt, + eq, + gt, + }; + enum class Scheme { Relaxed, @@ -32,6 +40,42 @@ namespace vcpkg::Versions std::size_t operator()(const VersionSpec& key) const; }; + struct RelaxedVersion + { + std::string original_string; + std::vector version; + + static ExpectedS from_string(const std::string& str); + }; + + struct SemanticVersion + { + std::string original_string; + std::string version_string; + std::string prerelease_string; + + std::vector version; + std::vector identifiers; + + static ExpectedS from_string(const std::string& str); + }; + + struct DateVersion + { + std::string original_string; + std::string version_string; + std::string identifiers_string; + + std::vector identifiers; + + static ExpectedS from_string(const std::string& str); + }; + + VerComp compare(const std::string& a, const std::string& b, Scheme scheme); + VerComp compare(const RelaxedVersion& a, const RelaxedVersion& b); + VerComp compare(const SemanticVersion& a, const SemanticVersion& b); + VerComp compare(const DateVersion& a, const DateVersion& b); + struct Constraint { enum class Type diff --git a/toolsrc/src/vcpkg-test/dependencies.cpp b/toolsrc/src/vcpkg-test/dependencies.cpp index bcc2f14a07..2aae7fffac 100644 --- a/toolsrc/src/vcpkg-test/dependencies.cpp +++ b/toolsrc/src/vcpkg-test/dependencies.cpp @@ -121,6 +121,42 @@ static void check_name_and_version(const Dependencies::InstallPlanAction& ipa, } } +static void check_semver_version(const ExpectedS& maybe_version, + const std::string& version_string, + const std::string& prerelease_string, + uint64_t major, + uint64_t minor, + uint64_t patch, + const std::vector& identifiers) +{ + auto actual_version = unwrap(maybe_version); + CHECK(actual_version.version_string == version_string); + CHECK(actual_version.prerelease_string == prerelease_string); + REQUIRE(actual_version.version.size() == 3); + CHECK(actual_version.version[0] == major); + CHECK(actual_version.version[1] == minor); + CHECK(actual_version.version[2] == patch); + CHECK(actual_version.identifiers == identifiers); +} + +static void check_relaxed_version(const ExpectedS& maybe_version, + const std::vector& version) +{ + auto actual_version = unwrap(maybe_version); + CHECK(actual_version.version == version); +} + +static void check_date_version(const ExpectedS& maybe_version, + const std::string& version_string, + const std::string& identifiers_string, + const std::vector& identifiers) +{ + auto actual_version = unwrap(maybe_version); + CHECK(actual_version.version_string == version_string); + CHECK(actual_version.identifiers_string == identifiers_string); + CHECK(actual_version.identifiers == identifiers); +} + static const PackageSpec& toplevel_spec() { static const PackageSpec ret("toplevel-spec", Test::X86_WINDOWS); @@ -506,6 +542,426 @@ TEST_CASE ("version install diamond relaxed", "[versionplan]") check_name_and_version(install_plan.install_actions[2], "a", {"3", 0}); } +TEST_CASE ("version parse semver", "[versionplan]") +{ + auto version_basic = Versions::SemanticVersion::from_string("1.2.3"); + check_semver_version(version_basic, "1.2.3", "", 1, 2, 3, {}); + + auto version_simple_tag = Versions::SemanticVersion::from_string("1.0.0-alpha"); + check_semver_version(version_simple_tag, "1.0.0", "alpha", 1, 0, 0, {"alpha"}); + + auto version_alphanumeric_tag = Versions::SemanticVersion::from_string("1.0.0-0alpha0"); + check_semver_version(version_alphanumeric_tag, "1.0.0", "0alpha0", 1, 0, 0, {"0alpha0"}); + + auto version_complex_tag = Versions::SemanticVersion::from_string("1.0.0-alpha.1.0.0"); + check_semver_version(version_complex_tag, "1.0.0", "alpha.1.0.0", 1, 0, 0, {"alpha", "1", "0", "0"}); + + auto version_complexer_tag = Versions::SemanticVersion::from_string("1.0.0-alpha.1.x.y.z.0-alpha.0-beta.l-a-s-t"); + check_semver_version(version_complexer_tag, + "1.0.0", + "alpha.1.x.y.z.0-alpha.0-beta.l-a-s-t", + 1, + 0, + 0, + {"alpha", "1", "x", "y", "z", "0-alpha", "0-beta", "l-a-s-t"}); + + auto version_ridiculous_tag = Versions::SemanticVersion::from_string("1.0.0----------------------------------"); + check_semver_version(version_ridiculous_tag, + "1.0.0", + "---------------------------------", + 1, + 0, + 0, + {"---------------------------------"}); + + auto version_build_tag = Versions::SemanticVersion::from_string("1.0.0+build"); + check_semver_version(version_build_tag, "1.0.0", "", 1, 0, 0, {}); + + auto version_prerelease_build_tag = Versions::SemanticVersion::from_string("1.0.0-alpha+build"); + check_semver_version(version_prerelease_build_tag, "1.0.0", "alpha", 1, 0, 0, {"alpha"}); + + auto version_invalid_incomplete = Versions::SemanticVersion::from_string("1.0-alpha"); + CHECK(!version_invalid_incomplete.has_value()); + + auto version_invalid_leading_zeroes = Versions::SemanticVersion::from_string("1.02.03-alpha+build"); + CHECK(!version_invalid_leading_zeroes.has_value()); + + auto version_invalid_leading_zeroes_in_tag = Versions::SemanticVersion::from_string("1.0.0-01"); + CHECK(!version_invalid_leading_zeroes_in_tag.has_value()); + + auto version_invalid_characters = Versions::SemanticVersion::from_string("1.0.0-alpha#2"); + CHECK(!version_invalid_characters.has_value()); +} + +TEST_CASE ("version parse relaxed", "[versionplan]") +{ + auto version_basic = Versions::RelaxedVersion::from_string("1.2.3"); + check_relaxed_version(version_basic, {1, 2, 3}); + + auto version_short = Versions::RelaxedVersion::from_string("1"); + check_relaxed_version(version_short, {1}); + + auto version_long = + Versions::RelaxedVersion::from_string("1.20.300.4000.50000.6000000.70000000.80000000.18446744073709551610"); + check_relaxed_version(version_long, {1, 20, 300, 4000, 50000, 6000000, 70000000, 80000000, 18446744073709551610}); + + auto version_invalid_characters = Versions::RelaxedVersion::from_string("1.a.0"); + CHECK(!version_invalid_characters.has_value()); + + auto version_invalid_identifiers_2 = Versions::RelaxedVersion::from_string("1.1a.2"); + CHECK(!version_invalid_identifiers_2.has_value()); + + auto version_invalid_leading_zeroes = Versions::RelaxedVersion::from_string("01.002.003"); + CHECK(!version_invalid_leading_zeroes.has_value()); +} + +TEST_CASE ("version parse date", "[versionplan]") +{ + auto version_basic = Versions::DateVersion::from_string("2020-12-25"); + check_date_version(version_basic, "2020-12-25", "", {}); + + auto version_identifiers = Versions::DateVersion::from_string("2020-12-25.1.2.3"); + check_date_version(version_identifiers, "2020-12-25", "1.2.3", {1, 2, 3}); + + auto version_invalid_date = Versions::DateVersion::from_string("2020-1-1"); + CHECK(!version_invalid_date.has_value()); + + auto version_invalid_identifiers = Versions::DateVersion::from_string("2020-01-01.alpha"); + CHECK(!version_invalid_identifiers.has_value()); + + auto version_invalid_identifiers_2 = Versions::DateVersion::from_string("2020-01-01.2a"); + CHECK(!version_invalid_identifiers_2.has_value()); + + auto version_invalid_leading_zeroes = Versions::DateVersion::from_string("2020-01-01.01"); + CHECK(!version_invalid_leading_zeroes.has_value()); +} + +TEST_CASE ("version sort semver", "[versionplan]") +{ + std::vector versions{unwrap(Versions::SemanticVersion::from_string("1.0.0")), + unwrap(Versions::SemanticVersion::from_string("0.0.0")), + unwrap(Versions::SemanticVersion::from_string("1.1.0")), + unwrap(Versions::SemanticVersion::from_string("2.0.0")), + unwrap(Versions::SemanticVersion::from_string("1.1.1")), + unwrap(Versions::SemanticVersion::from_string("1.0.1")), + unwrap(Versions::SemanticVersion::from_string("1.0.0-alpha.1")), + unwrap(Versions::SemanticVersion::from_string("1.0.0-beta")), + unwrap(Versions::SemanticVersion::from_string("1.0.0-alpha")), + unwrap(Versions::SemanticVersion::from_string("1.0.0-alpha.beta")), + unwrap(Versions::SemanticVersion::from_string("1.0.0-rc")), + unwrap(Versions::SemanticVersion::from_string("1.0.0-beta.2")), + unwrap(Versions::SemanticVersion::from_string("1.0.0-beta.20")), + unwrap(Versions::SemanticVersion::from_string("1.0.0-beta.3")), + unwrap(Versions::SemanticVersion::from_string("1.0.0-1")), + unwrap(Versions::SemanticVersion::from_string("1.0.0-0alpha"))}; + + std::sort(std::begin(versions), std::end(versions), [](const auto& lhs, const auto& rhs) -> bool { + return Versions::compare(lhs, rhs) == Versions::VerComp::lt; + }); + + CHECK(versions[0].original_string == "0.0.0"); + CHECK(versions[1].original_string == "1.0.0-1"); + CHECK(versions[2].original_string == "1.0.0-0alpha"); + CHECK(versions[3].original_string == "1.0.0-alpha"); + CHECK(versions[4].original_string == "1.0.0-alpha.1"); + CHECK(versions[5].original_string == "1.0.0-alpha.beta"); + CHECK(versions[6].original_string == "1.0.0-beta"); + CHECK(versions[7].original_string == "1.0.0-beta.2"); + CHECK(versions[8].original_string == "1.0.0-beta.3"); + CHECK(versions[9].original_string == "1.0.0-beta.20"); + CHECK(versions[10].original_string == "1.0.0-rc"); + CHECK(versions[11].original_string == "1.0.0"); + CHECK(versions[12].original_string == "1.0.1"); + CHECK(versions[13].original_string == "1.1.0"); + CHECK(versions[14].original_string == "1.1.1"); + CHECK(versions[15].original_string == "2.0.0"); +} + +TEST_CASE ("version sort relaxed", "[versionplan]") +{ + std::vector versions{unwrap(Versions::RelaxedVersion::from_string("1.0.0")), + unwrap(Versions::RelaxedVersion::from_string("1.0")), + unwrap(Versions::RelaxedVersion::from_string("1")), + unwrap(Versions::RelaxedVersion::from_string("2")), + unwrap(Versions::RelaxedVersion::from_string("1.1")), + unwrap(Versions::RelaxedVersion::from_string("1.10.1")), + unwrap(Versions::RelaxedVersion::from_string("1.0.1")), + unwrap(Versions::RelaxedVersion::from_string("1.0.0.1")), + unwrap(Versions::RelaxedVersion::from_string("1.0.0.2"))}; + + std::sort(std::begin(versions), std::end(versions), [](const auto& lhs, const auto& rhs) -> bool { + return Versions::compare(lhs, rhs) == Versions::VerComp::lt; + }); + + CHECK(versions[0].original_string == "1"); + CHECK(versions[1].original_string == "1.0"); + CHECK(versions[2].original_string == "1.0.0"); + CHECK(versions[3].original_string == "1.0.0.1"); + CHECK(versions[4].original_string == "1.0.0.2"); + CHECK(versions[5].original_string == "1.0.1"); + CHECK(versions[6].original_string == "1.1"); + CHECK(versions[7].original_string == "1.10.1"); + CHECK(versions[8].original_string == "2"); +} + +TEST_CASE ("version sort date", "[versionplan]") +{ + std::vector versions{unwrap(Versions::DateVersion::from_string("2021-01-01.2")), + unwrap(Versions::DateVersion::from_string("2021-01-01.1")), + unwrap(Versions::DateVersion::from_string("2021-01-01.1.1")), + unwrap(Versions::DateVersion::from_string("2021-01-01.1.0")), + unwrap(Versions::DateVersion::from_string("2021-01-01")), + unwrap(Versions::DateVersion::from_string("2021-01-01")), + unwrap(Versions::DateVersion::from_string("2020-12-25")), + unwrap(Versions::DateVersion::from_string("2020-12-31")), + unwrap(Versions::DateVersion::from_string("2021-01-01.10"))}; + + std::sort(std::begin(versions), std::end(versions), [](const auto& lhs, const auto& rhs) -> bool { + return Versions::compare(lhs, rhs) == Versions::VerComp::lt; + }); + + CHECK(versions[0].original_string == "2020-12-25"); + CHECK(versions[1].original_string == "2020-12-31"); + CHECK(versions[2].original_string == "2021-01-01"); + CHECK(versions[3].original_string == "2021-01-01"); + CHECK(versions[4].original_string == "2021-01-01.1"); + CHECK(versions[5].original_string == "2021-01-01.1.0"); + CHECK(versions[6].original_string == "2021-01-01.1.1"); + CHECK(versions[7].original_string == "2021-01-01.2"); + CHECK(versions[8].original_string == "2021-01-01.10"); +} + +TEST_CASE ("version install simple semver", "[versionplan]") +{ + MockBaselineProvider bp; + bp.v["a"] = {"2.0.0", 0}; + + MockVersionedPortfileProvider vp; + vp.emplace("a", {"2.0.0", 0}, Scheme::Semver); + vp.emplace("a", {"3.0.0", 0}, Scheme::Semver); + + MockCMakeVarProvider var_provider; + + auto install_plan = unwrap(Dependencies::create_versioned_install_plan( + vp, + bp, + var_provider, + { + Dependency{"a", {}, {}, {Constraint::Type::Minimum, "3.0.0", 0}}, + }, + {}, + toplevel_spec())); + + REQUIRE(install_plan.size() == 1); + check_name_and_version(install_plan.install_actions[0], "a", {"3.0.0", 0}); +} + +TEST_CASE ("version install transitive semver", "[versionplan]") +{ + MockBaselineProvider bp; + bp.v["a"] = {"2.0.0", 0}; + bp.v["b"] = {"2.0.0", 0}; + + MockVersionedPortfileProvider vp; + vp.emplace("a", {"2.0.0", 0}, Scheme::Semver); + vp.emplace("a", {"3.0.0", 0}, Scheme::Semver).source_control_file->core_paragraph->dependencies = { + Dependency{"b", {}, {}, DependencyConstraint{Constraint::Type::Minimum, "3.0.0"}}, + }; + vp.emplace("b", {"2.0.0", 0}, Scheme::Semver); + vp.emplace("b", {"3.0.0", 0}, Scheme::Semver); + + MockCMakeVarProvider var_provider; + + auto install_plan = unwrap(Dependencies::create_versioned_install_plan( + vp, + bp, + var_provider, + { + Dependency{"a", {}, {}, {Constraint::Type::Minimum, "3.0.0", 0}}, + }, + {}, + toplevel_spec())); + + REQUIRE(install_plan.size() == 2); + check_name_and_version(install_plan.install_actions[0], "b", {"3.0.0", 0}); + check_name_and_version(install_plan.install_actions[1], "a", {"3.0.0", 0}); +} + +TEST_CASE ("version install diamond semver", "[versionplan]") +{ + MockBaselineProvider bp; + bp.v["a"] = {"2.0.0", 0}; + bp.v["b"] = {"3.0.0", 0}; + + MockVersionedPortfileProvider vp; + vp.emplace("a", {"2.0.0", 0}, Scheme::Semver); + vp.emplace("a", {"3.0.0", 0}, Scheme::Semver).source_control_file->core_paragraph->dependencies = { + Dependency{"b", {}, {}, DependencyConstraint{Constraint::Type::Minimum, "2.0.0", 1}}, + Dependency{"c", {}, {}, DependencyConstraint{Constraint::Type::Minimum, "5.0.0", 1}}, + }; + vp.emplace("b", {"2.0.0", 1}, Scheme::Semver); + vp.emplace("b", {"3.0.0", 0}, Scheme::Semver).source_control_file->core_paragraph->dependencies = { + Dependency{"c", {}, {}, DependencyConstraint{Constraint::Type::Minimum, "9.0.0", 2}}, + }; + vp.emplace("c", {"5.0.0", 1}, Scheme::Semver); + vp.emplace("c", {"9.0.0", 2}, Scheme::Semver); + + MockCMakeVarProvider var_provider; + + auto install_plan = unwrap(Dependencies::create_versioned_install_plan( + vp, + bp, + var_provider, + { + Dependency{"a", {}, {}, {Constraint::Type::Minimum, "3.0.0", 0}}, + Dependency{"b", {}, {}, {Constraint::Type::Minimum, "2.0.0", 1}}, + }, + {}, + toplevel_spec())); + + REQUIRE(install_plan.size() == 3); + check_name_and_version(install_plan.install_actions[0], "c", {"9.0.0", 2}); + check_name_and_version(install_plan.install_actions[1], "b", {"3.0.0", 0}); + check_name_and_version(install_plan.install_actions[2], "a", {"3.0.0", 0}); +} + +TEST_CASE ("version install simple date", "[versionplan]") +{ + MockBaselineProvider bp; + bp.v["a"] = {"2020-02-01", 0}; + + MockVersionedPortfileProvider vp; + vp.emplace("a", {"2020-02-01", 0}, Scheme::Date); + vp.emplace("a", {"2020-03-01", 0}, Scheme::Date); + + MockCMakeVarProvider var_provider; + + auto install_plan = unwrap(Dependencies::create_versioned_install_plan( + vp, + bp, + var_provider, + { + Dependency{"a", {}, {}, {Constraint::Type::Minimum, "2020-03-01", 0}}, + }, + {}, + toplevel_spec())); + + REQUIRE(install_plan.size() == 1); + check_name_and_version(install_plan.install_actions[0], "a", {"2020-03-01", 0}); +} + +TEST_CASE ("version install transitive date", "[versionplan]") +{ + MockBaselineProvider bp; + bp.v["a"] = {"2020-01-01.2", 0}; + bp.v["b"] = {"2020-01-01.3", 0}; + + MockVersionedPortfileProvider vp; + vp.emplace("a", {"2020-01-01.2", 0}, Scheme::Date); + vp.emplace("a", {"2020-01-01.3", 0}, Scheme::Date).source_control_file->core_paragraph->dependencies = { + Dependency{"b", {}, {}, DependencyConstraint{Constraint::Type::Minimum, "2020-01-01.3"}}, + }; + vp.emplace("b", {"2020-01-01.2", 0}, Scheme::Date); + vp.emplace("b", {"2020-01-01.3", 0}, Scheme::Date); + + MockCMakeVarProvider var_provider; + + auto install_plan = unwrap(Dependencies::create_versioned_install_plan( + vp, + bp, + var_provider, + { + Dependency{"a", {}, {}, {Constraint::Type::Minimum, "2020-01-01.3", 0}}, + }, + {}, + toplevel_spec())); + + REQUIRE(install_plan.size() == 2); + check_name_and_version(install_plan.install_actions[0], "b", {"2020-01-01.3", 0}); + check_name_and_version(install_plan.install_actions[1], "a", {"2020-01-01.3", 0}); +} + +TEST_CASE ("version install diamond date", "[versionplan]") +{ + MockBaselineProvider bp; + bp.v["a"] = {"2020-01-02", 0}; + bp.v["b"] = {"2020-01-03", 0}; + + MockVersionedPortfileProvider vp; + vp.emplace("a", {"2020-01-02", 0}, Scheme::Date); + vp.emplace("a", {"2020-01-03", 0}, Scheme::Date).source_control_file->core_paragraph->dependencies = { + Dependency{"b", {}, {}, DependencyConstraint{Constraint::Type::Minimum, "2020-01-02", 1}}, + Dependency{"c", {}, {}, DependencyConstraint{Constraint::Type::Minimum, "2020-01-05", 1}}, + }; + vp.emplace("b", {"2020-01-02", 1}, Scheme::Date); + vp.emplace("b", {"2020-01-03", 0}, Scheme::Date).source_control_file->core_paragraph->dependencies = { + Dependency{"c", {}, {}, DependencyConstraint{Constraint::Type::Minimum, "2020-01-09", 2}}, + }; + vp.emplace("c", {"2020-01-05", 1}, Scheme::Date); + vp.emplace("c", {"2020-01-09", 2}, Scheme::Date); + + MockCMakeVarProvider var_provider; + + auto install_plan = unwrap(Dependencies::create_versioned_install_plan( + vp, + bp, + var_provider, + { + Dependency{"a", {}, {}, {Constraint::Type::Minimum, "2020-01-03", 0}}, + Dependency{"b", {}, {}, {Constraint::Type::Minimum, "2020-01-02", 1}}, + }, + {}, + toplevel_spec())); + + REQUIRE(install_plan.size() == 3); + check_name_and_version(install_plan.install_actions[0], "c", {"2020-01-09", 2}); + check_name_and_version(install_plan.install_actions[1], "b", {"2020-01-03", 0}); + check_name_and_version(install_plan.install_actions[2], "a", {"2020-01-03", 0}); +} + +TEST_CASE ("version install scheme failure", "[versionplan]") +{ + MockVersionedPortfileProvider vp; + vp.emplace("a", {"1.0.0", 0}, Scheme::Semver); + vp.emplace("a", {"1.0.1", 0}, Scheme::Relaxed); + vp.emplace("a", {"1.0.2", 0}, Scheme::Semver); + + MockCMakeVarProvider var_provider; + + SECTION ("lower baseline") + { + MockBaselineProvider bp; + bp.v["a"] = {"1.0.0", 0}; + + auto install_plan = Dependencies::create_versioned_install_plan( + vp, + bp, + var_provider, + {Dependency{"a", {}, {}, {Constraint::Type::Minimum, "1.0.1", 0}}}, + {}, + toplevel_spec()); + + REQUIRE(!install_plan.error().empty()); + CHECK(install_plan.error() == "Version conflict on a@1.0.1: baseline required 1.0.0"); + } + SECTION ("higher baseline") + { + MockBaselineProvider bp; + bp.v["a"] = {"1.0.2", 0}; + + auto install_plan = Dependencies::create_versioned_install_plan( + vp, + bp, + var_provider, + {Dependency{"a", {}, {}, {Constraint::Type::Minimum, "1.0.1", 0}}}, + {}, + toplevel_spec()); + + REQUIRE(!install_plan.error().empty()); + CHECK(install_plan.error() == "Version conflict on a@1.0.1: baseline required 1.0.2"); + } +} + TEST_CASE ("version install scheme change in port version", "[versionplan]") { MockVersionedPortfileProvider vp; diff --git a/toolsrc/src/vcpkg/dependencies.cpp b/toolsrc/src/vcpkg/dependencies.cpp index 7d69becdf5..e7768f2bfd 100644 --- a/toolsrc/src/vcpkg/dependencies.cpp +++ b/toolsrc/src/vcpkg/dependencies.cpp @@ -1215,6 +1215,8 @@ namespace vcpkg::Dependencies std::map vermap; std::map exacts; Optional> relaxed; + Optional> semver; + Optional> date; std::set features; bool default_features = true; @@ -1271,6 +1273,30 @@ namespace vcpkg::Dependencies vsi = relaxed.get()->get(); } } + else if (scheme == Versions::Scheme::Semver) + { + if (auto p = semver.get()) + { + vsi = p->get(); + } + else + { + semver = std::make_unique(); + vsi = semver.get()->get(); + } + } + else if (scheme == Versions::Scheme::Date) + { + if (auto p = date.get()) + { + vsi = p->get(); + } + else + { + date = std::make_unique(); + vsi = date.get()->get(); + } + } else { // not implemented @@ -1287,40 +1313,24 @@ namespace vcpkg::Dependencies return it == vermap.end() ? nullptr : it->second; } - enum class VerComp - { - unk, - lt, - eq, - gt, - }; + using Versions::VerComp; + static VerComp compare_versions(Versions::Scheme sa, const Versions::Version& a, Versions::Scheme sb, const Versions::Version& b) { if (sa != sb) return VerComp::unk; - switch (sa) + + if (a.text() != b.text()) { - case Versions::Scheme::String: - { - if (a.text() != b.text()) return VerComp::unk; - if (a.port_version() < b.port_version()) return VerComp::lt; - if (a.port_version() > b.port_version()) return VerComp::gt; - return VerComp::eq; - } - case Versions::Scheme::Relaxed: - { - auto i1 = atoi(a.text().c_str()); - auto i2 = atoi(b.text().c_str()); - if (i1 < i2) return VerComp::lt; - if (i1 > i2) return VerComp::gt; - if (a.port_version() < b.port_version()) return VerComp::lt; - if (a.port_version() > b.port_version()) return VerComp::gt; - return VerComp::eq; - } - default: Checks::unreachable(VCPKG_LINE_INFO); + auto result = Versions::compare(a.text(), b.text(), sa); + if (result != VerComp::eq) return result; } + + if (a.port_version() < b.port_version()) return VerComp::lt; + if (a.port_version() > b.port_version()) return VerComp::gt; + return VerComp::eq; } bool VersionedPackageGraph::VersionSchemeInfo::is_less_than(const Versions::Version& new_ver) const diff --git a/toolsrc/src/vcpkg/versions.cpp b/toolsrc/src/vcpkg/versions.cpp index 7f19813ec0..5ea2a8182e 100644 --- a/toolsrc/src/vcpkg/versions.cpp +++ b/toolsrc/src/vcpkg/versions.cpp @@ -1,7 +1,29 @@ +#include + #include +#include + namespace vcpkg::Versions { + namespace + { + Optional as_numeric(StringView str) + { + uint64_t res = 0; + size_t digits = 0; + for (auto&& ch : str) + { + uint64_t digit_value = static_cast(ch) - static_cast('0'); + if (digit_value > 9) return nullopt; + if (res > std::numeric_limits::max() / 10 - digit_value) return nullopt; + ++digits; + res = res * 10 + digit_value; + } + return res; + } + } + VersionSpec::VersionSpec(const std::string& port_name, const VersionT& version) : port_name(port_name), version(version) { @@ -27,4 +49,199 @@ namespace vcpkg::Versions return hash()(key.port_name) ^ (hash()(key.version.to_string()) >> 1); } + + ExpectedS RelaxedVersion::from_string(const std::string& str) + { + std::regex relaxed_scheme_match("^(0|[1-9][0-9]*)(\\.(0|[1-9][0-9]*))*"); + + if (!std::regex_match(str, relaxed_scheme_match)) + { + return Strings::format( + "Error: String `%s` must only contain dot-separated numeric values without leading zeroes.", str); + } + + return RelaxedVersion{str, Util::fmap(Strings::split(str, '.'), [](auto&& strval) -> uint64_t { + return as_numeric(strval).value_or_exit(VCPKG_LINE_INFO); + })}; + } + + ExpectedS SemanticVersion::from_string(const std::string& str) + { + // Suggested regex by semver.org + std::regex semver_scheme_match("^(0|[1-9][0-9]*)\\.(0|[1-9][0-9]*)\\.(0|[1-9][0-9]*)" + "(?:-((?:0|[1-9][0-9]*|[0-9]*[a-zA-Z-][0-9a-zA-Z-]*)(?:\\.(?:0|[1-9][0-9]*|[0-9]" + "*[a-zA-Z-][0-9a-zA-Z-]*))*))?" + "(?:\\+([0-9a-zA-Z-]+(?:\\.[0-9a-zA-Z-]+)*))?$"); + + if (!std::regex_match(str, semver_scheme_match)) + { + return Strings::format( + "Error: String `%s` is not a valid Semantic Version string, consult https://semver.org", str); + } + + SemanticVersion ret; + ret.original_string = str; + ret.version_string = str; + + auto build_found = ret.version_string.find('+'); + if (build_found != std::string::npos) + { + ret.version_string.resize(build_found); + } + + auto prerelease_found = ret.version_string.find('-'); + if (prerelease_found != std::string::npos) + { + ret.prerelease_string = ret.version_string.substr(prerelease_found + 1); + ret.identifiers = Strings::split(ret.prerelease_string, '.'); + ret.version_string.resize(prerelease_found); + } + + std::regex version_match("(0|[1-9][0-9]*)(\\.(0|[1-9][0-9]*)){2}"); + if (!std::regex_match(ret.version_string, version_match)) + { + return Strings::format("Error: String `%s` does not follow the required MAJOR.MINOR.PATCH format.", + ret.version_string); + } + + auto parts = Strings::split(ret.version_string, '.'); + ret.version = Util::fmap( + parts, [](auto&& strval) -> uint64_t { return as_numeric(strval).value_or_exit(VCPKG_LINE_INFO); }); + + return ret; + } + + ExpectedS DateVersion::from_string(const std::string& str) + { + std::regex date_scheme_match("([0-9]{4}-[0-9]{2}-[0-9]{2})(\\.(0|[1-9][0-9]*))*"); + if (!std::regex_match(str, date_scheme_match)) + { + return Strings::format("Error: String `%s` is not a valid date version." + "Date section must follow the format YYYY-MM-DD and disambiguators must be " + "dot-separated positive integer values without leading zeroes.", + str); + } + + DateVersion ret; + ret.original_string = str; + ret.version_string = str; + + auto identifiers_found = ret.version_string.find('.'); + if (identifiers_found != std::string::npos) + { + ret.identifiers_string = ret.version_string.substr(identifiers_found + 1); + ret.identifiers = Util::fmap(Strings::split(ret.identifiers_string, '.'), [](auto&& strval) -> uint64_t { + return as_numeric(strval).value_or_exit(VCPKG_LINE_INFO); + }); + ret.version_string.resize(identifiers_found); + } + + return ret; + } + + VerComp compare(const std::string& a, const std::string& b, Scheme scheme) + { + if (scheme == Scheme::String) + { + return (a == b) ? VerComp::eq : VerComp::unk; + } + if (scheme == Scheme::Semver) + { + return compare(SemanticVersion::from_string(a).value_or_exit(VCPKG_LINE_INFO), + SemanticVersion::from_string(b).value_or_exit(VCPKG_LINE_INFO)); + } + if (scheme == Scheme::Relaxed) + { + return compare(RelaxedVersion::from_string(a).value_or_exit(VCPKG_LINE_INFO), + RelaxedVersion::from_string(b).value_or_exit(VCPKG_LINE_INFO)); + } + if (scheme == Scheme::Date) + { + return compare(DateVersion::from_string(a).value_or_exit(VCPKG_LINE_INFO), + DateVersion::from_string(b).value_or_exit(VCPKG_LINE_INFO)); + } + Checks::unreachable(VCPKG_LINE_INFO); + } + + VerComp compare(const RelaxedVersion& a, const RelaxedVersion& b) + { + if (a.original_string == b.original_string) return VerComp::eq; + + if (a.version < b.version) return VerComp::lt; + if (a.version > b.version) return VerComp::gt; + Checks::unreachable(VCPKG_LINE_INFO); + } + + VerComp compare(const SemanticVersion& a, const SemanticVersion& b) + { + if (a.version_string == b.version_string) + { + if (a.prerelease_string == b.prerelease_string) return VerComp::eq; + if (a.prerelease_string.empty()) return VerComp::gt; + if (b.prerelease_string.empty()) return VerComp::lt; + } + + // Compare version elements left-to-right. + if (a.version < b.version) return VerComp::lt; + if (a.version > b.version) return VerComp::gt; + + // Compare identifiers left-to-right. + auto count = std::min(a.identifiers.size(), b.identifiers.size()); + for (size_t i = 0; i < count; ++i) + { + auto&& iden_a = a.identifiers[i]; + auto&& iden_b = b.identifiers[i]; + + auto a_numeric = as_numeric(iden_a); + auto b_numeric = as_numeric(iden_b); + + // Numeric identifiers always have lower precedence than non-numeric identifiers. + if (a_numeric.has_value() && !b_numeric.has_value()) return VerComp::lt; + if (!a_numeric.has_value() && b_numeric.has_value()) return VerComp::gt; + + // Identifiers consisting of only digits are compared numerically. + if (a_numeric.has_value() && b_numeric.has_value()) + { + auto a_value = a_numeric.value_or_exit(VCPKG_LINE_INFO); + auto b_value = b_numeric.value_or_exit(VCPKG_LINE_INFO); + + if (a_value < b_value) return VerComp::lt; + if (a_value > b_value) return VerComp::gt; + continue; + } + + // Identifiers with letters or hyphens are compared lexically in ASCII sort order. + auto strcmp_result = std::strcmp(iden_a.c_str(), iden_b.c_str()); + if (strcmp_result < 0) return VerComp::lt; + if (strcmp_result > 0) return VerComp::gt; + } + + // A larger set of pre-release fields has a higher precedence than a smaller set, if all of the preceding + // identifiers are equal. + if (a.identifiers.size() < b.identifiers.size()) return VerComp::lt; + if (a.identifiers.size() > b.identifiers.size()) return VerComp::gt; + + // This should be unreachable since direct string comparisons of version_string and prerelease_string should + // handle this case. If we ever land here, then there's a bug in the the parsing on + // SemanticVersion::from_string(). + Checks::unreachable(VCPKG_LINE_INFO); + } + + VerComp compare(const Versions::DateVersion& a, const Versions::DateVersion& b) + { + if (a.version_string == b.version_string) + { + if (a.identifiers_string == b.identifiers_string) return VerComp::eq; + if (a.identifiers_string.empty() && !b.identifiers_string.empty()) return VerComp::lt; + if (!a.identifiers_string.empty() && b.identifiers_string.empty()) return VerComp::gt; + } + + // The date parts in our scheme is lexicographically sortable. + if (a.version_string < b.version_string) return VerComp::lt; + if (a.version_string > b.version_string) return VerComp::gt; + if (a.identifiers < b.identifiers) return VerComp::lt; + if (a.identifiers > b.identifiers) return VerComp::gt; + + Checks::unreachable(VCPKG_LINE_INFO); + } } \ No newline at end of file