diff options
| author | Victor Romero <romerosanchezv@gmail.com> | 2020-11-27 05:44:21 -0800 |
|---|---|---|
| committer | GitHub <noreply@github.com> | 2020-11-27 05:44:21 -0800 |
| commit | 6c9cda1635859571de5c964bbacdece824045305 (patch) | |
| tree | 1bcc929af8e7e32314966ed39a08e43e64f6473f | |
| parent | 62fe6ffbbbae9149fb8c48cde2a34b809e2a3008 (diff) | |
| download | vcpkg-6c9cda1635859571de5c964bbacdece824045305.tar.gz vcpkg-6c9cda1635859571de5c964bbacdece824045305.zip | |
[vcpkg] Implement VersionedPortfileProvider and BaselineProvider (#14123)
* WIP: Get versions from database files
* Fix formatting
* Provider inherits ResourceBase
* Correct versions JSON file location
* Fix formatting
* Fix formatting
* Fix include in versions.h
* Fetch port versions using git tree object
* Undo changes to x-history
* Remove unnecesary moves
Co-authored-by: nicole mazzuca <mazzucan@outlook.com>
* Extract Git manipulation code
* [WIP] Review comments
* [WIP] Review comments pt. 2
* [WIP] Review comments / fix formatting
* Generate baseline.json
* Extract deserializers from registries source file
* BaselineProvider initial implementation
* Modify gitignore
* Update .gitignore again
* Use JSON deserializer for versions db
* Lazy load baseline file
* Fetch baseline.json from baseline commit
* More git abstractions
* Clean up code
* Path helpers
* Formatting
* Move data into impl object
* Use implementation object for VersionedPortfileProvider
* Reuse cloned instance for checkouts
* Code cleanup and formatting
* Fix returning dangling reference
* Prepare to remove files in port_versions/
* Remove files in port_versions/
* Update .gitignore
* Some PR review comments
* Use StringView
* More StringView conversions
* More refactoring
* Make some implementation members private
* Functions for parsing baseline and version files
* Hide deserializers implementation
* Check for `versions` feature flag in registries.
Co-authored-by: Robert Schumacher <roschuma@microsoft.com>
Co-authored-by: nicole mazzuca <mazzucan@outlook.com>
| -rw-r--r-- | .gitignore | 2 | ||||
| -rw-r--r-- | scripts/generatePortVersionsDb.py | 71 | ||||
| -rw-r--r-- | toolsrc/include/vcpkg/base/jsonreader.h | 3 | ||||
| -rw-r--r-- | toolsrc/include/vcpkg/portfileprovider.h | 45 | ||||
| -rw-r--r-- | toolsrc/include/vcpkg/vcpkgpaths.h | 27 | ||||
| -rw-r--r-- | toolsrc/include/vcpkg/versiondeserializers.h | 37 | ||||
| -rw-r--r-- | toolsrc/include/vcpkg/versions.h | 21 | ||||
| -rw-r--r-- | toolsrc/src/vcpkg/portfileprovider.cpp | 190 | ||||
| -rw-r--r-- | toolsrc/src/vcpkg/registries.cpp | 83 | ||||
| -rw-r--r-- | toolsrc/src/vcpkg/vcpkgpaths.cpp | 193 | ||||
| -rw-r--r-- | toolsrc/src/vcpkg/versiondeserializers.cpp | 210 | ||||
| -rw-r--r-- | toolsrc/src/vcpkg/versions.cpp | 34 | ||||
| -rw-r--r-- | toolsrc/windows-bootstrap/vcpkg.vcxproj | 3 |
13 files changed, 832 insertions, 87 deletions
diff --git a/.gitignore b/.gitignore index ed388f4a1..d12fd1363 100644 --- a/.gitignore +++ b/.gitignore @@ -295,6 +295,8 @@ __pycache__/ /toolsrc/windows-bootstrap/msbuild.x86.release/ /toolsrc/windows-bootstrap/msbuild.x64.debug/ /toolsrc/windows-bootstrap/msbuild.x64.release/ +#ignore db +/port_versions/ #ignore custom triplets /triplets/* #add vcpkg-designed triplets back in diff --git a/scripts/generatePortVersionsDb.py b/scripts/generatePortVersionsDb.py index cefd61e1c..e3c338c64 100644 --- a/scripts/generatePortVersionsDb.py +++ b/scripts/generatePortVersionsDb.py @@ -4,6 +4,7 @@ import sys import subprocess
import json
import time
+import shutil
from subprocess import CalledProcessError
from json.decoder import JSONDecodeError
@@ -14,9 +15,9 @@ 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')
+ output = subprocess.run(['git', '-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)
@@ -25,47 +26,89 @@ def get_current_git_ref(): 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))]
+
+ # Assume each directory in ${VCPKG_ROOT}/ports is a different port
+ port_names = [item for item in os.listdir(
+ ports_path) if os.path.isdir(os.path.join(ports_path, item))]
+ port_names.sort()
total_count = len(port_names)
+
+ # Dictionary to collect the latest version of each port as baseline
+ baseline_objects = {}
+ baseline_objects['default'] = {}
+
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):
+ if not os.path.exists(output_filepath):
output = subprocess.run(
- [os.path.join(SCRIPT_DIRECTORY, '../vcpkg'), 'x-history', port_name, '--x-json'],
+ [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)
+
+ # Put latest version in baseline dictionary
+ latest_version = versions_object["versions"][0]
+ baseline_objects['default'][port_name] = {
+ "version-string": latest_version["version-string"],
+ "port-version": latest_version["port-version"]
+ }
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)
+ print(
+ f'Malformed 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)
+ 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')
+ print(
+ f'Processed {counter} out of {total_count}. Elapsed time: {elapsed_time:.2f} seconds')
+
+ # Generate baseline.json
+ baseline_file_path = os.path.join(db_path, 'baseline.json')
+ with open(baseline_file_path, 'w') as baseline_output_file:
+ json.dump(baseline_objects, baseline_output_file)
+
+ # Generate timestamp
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')
-
+ 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 (os.path.exists(db_path)):
+ try:
+ shutil.rmtree(db_path)
+ except OSError as e:
+ print(f'Could not delete folder: {db_path}.\nError: {e.strerror}')
+
+ 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'))
+ main(ports_path=os.path.join(SCRIPT_DIRECTORY, '../ports'),
+ db_path=os.path.join(SCRIPT_DIRECTORY, '../port_versions'))
diff --git a/toolsrc/include/vcpkg/base/jsonreader.h b/toolsrc/include/vcpkg/base/jsonreader.h index cdd0299d2..02c1936e2 100644 --- a/toolsrc/include/vcpkg/base/jsonreader.h +++ b/toolsrc/include/vcpkg/base/jsonreader.h @@ -23,7 +23,7 @@ namespace vcpkg::Json Optional<Type> visit(Reader&, const Value&); Optional<Type> visit(Reader&, const Object&); - protected: + public: virtual Optional<Type> visit_null(Reader&); virtual Optional<Type> visit_boolean(Reader&, bool); virtual Optional<Type> visit_integer(Reader& r, int64_t i); @@ -33,6 +33,7 @@ namespace vcpkg::Json virtual Optional<Type> visit_object(Reader&, const Object&); virtual View<StringView> valid_fields() const; + protected: IDeserializer() = default; IDeserializer(const IDeserializer&) = default; IDeserializer& operator=(const IDeserializer&) = default; diff --git a/toolsrc/include/vcpkg/portfileprovider.h b/toolsrc/include/vcpkg/portfileprovider.h index c127aed40..610ecb735 100644 --- a/toolsrc/include/vcpkg/portfileprovider.h +++ b/toolsrc/include/vcpkg/portfileprovider.h @@ -6,6 +6,7 @@ #include <vcpkg/base/util.h> #include <vcpkg/sourceparagraph.h> +#include <vcpkg/versions.h> namespace vcpkg::PortFileProvider { @@ -36,4 +37,48 @@ namespace vcpkg::PortFileProvider std::vector<fs::path> overlay_ports; mutable std::unordered_map<std::string, SourceControlFileLocation> cache; }; + + struct IVersionedPortfileProvider + { + virtual const std::vector<vcpkg::Versions::VersionSpec>& get_port_versions(StringView port_name) const = 0; + + virtual ExpectedS<const SourceControlFileLocation&> get_control_file( + const vcpkg::Versions::VersionSpec& version_spec) const = 0; + }; + + struct IBaselineProvider + { + virtual Optional<VersionT> get_baseline_version(StringView port_name) const = 0; + }; + + namespace details + { + struct BaselineProviderImpl; + struct VersionedPortfileProviderImpl; + } + + struct VersionedPortfileProvider : IVersionedPortfileProvider, Util::ResourceBase + { + explicit VersionedPortfileProvider(const vcpkg::VcpkgPaths& paths); + ~VersionedPortfileProvider(); + + const std::vector<vcpkg::Versions::VersionSpec>& get_port_versions(StringView port_name) const override; + + ExpectedS<const SourceControlFileLocation&> get_control_file( + const vcpkg::Versions::VersionSpec& version_spec) const override; + + private: + std::unique_ptr<details::VersionedPortfileProviderImpl> m_impl; + }; + + struct BaselineProvider : IBaselineProvider, Util::ResourceBase + { + explicit BaselineProvider(const vcpkg::VcpkgPaths& paths, const std::string& baseline); + ~BaselineProvider(); + + Optional<VersionT> get_baseline_version(StringView port_name) const override; + + private: + std::unique_ptr<details::BaselineProviderImpl> m_impl; + }; } diff --git a/toolsrc/include/vcpkg/vcpkgpaths.h b/toolsrc/include/vcpkg/vcpkgpaths.h index def874e7c..c85eff0ca 100644 --- a/toolsrc/include/vcpkg/vcpkgpaths.h +++ b/toolsrc/include/vcpkg/vcpkgpaths.h @@ -102,11 +102,23 @@ namespace vcpkg fs::path vcpkg_dir_info; fs::path vcpkg_dir_updates; + fs::path baselines_dot_git_dir; + fs::path baselines_work_tree; + fs::path baselines_output; + + fs::path versions_dot_git_dir; + fs::path versions_work_tree; + fs::path versions_output; + fs::path ports_cmake; const fs::path& get_tool_exe(const std::string& tool) const; const std::string& get_tool_version(const std::string& tool) const; + // Git manipulation + fs::path git_checkout_baseline(Files::Filesystem& filesystem, StringView commit_sha) const; + fs::path git_checkout_port(Files::Filesystem& filesystem, StringView port_name, StringView git_tree) const; + Optional<const Json::Object&> get_manifest() const; Optional<const fs::path&> get_manifest_path() const; const Configuration& get_configuration() const; @@ -133,5 +145,20 @@ namespace vcpkg private: std::unique_ptr<details::VcpkgPathsImpl> m_pimpl; + + static void git_checkout_subpath(const VcpkgPaths& paths, + StringView commit_sha, + const fs::path& subpath, + const fs::path& local_repo, + const fs::path& destination, + const fs::path& dot_git_dir, + const fs::path& work_tree); + + static void git_checkout_object(const VcpkgPaths& paths, + StringView git_object, + const fs::path& local_repo, + const fs::path& destination, + const fs::path& dot_git_dir, + const fs::path& work_tree); }; } diff --git a/toolsrc/include/vcpkg/versiondeserializers.h b/toolsrc/include/vcpkg/versiondeserializers.h new file mode 100644 index 000000000..f5ffda101 --- /dev/null +++ b/toolsrc/include/vcpkg/versiondeserializers.h @@ -0,0 +1,37 @@ +#pragma once + +#include <vcpkg/base/fwd/stringview.h> + +#include <vcpkg/base/jsonreader.h> +#include <vcpkg/base/stringliteral.h> + +#include <vcpkg/versions.h> +#include <vcpkg/versiont.h> + +namespace vcpkg +{ + struct VersionDbEntry + { + VersionT version; + Versions::Scheme scheme; + std::string git_tree; + + VersionDbEntry(const std::string& version_string, + int port_version, + Versions::Scheme scheme, + const std::string& git_tree) + : version(VersionT(version_string, port_version)), scheme(scheme), git_tree(git_tree) + { + } + }; + + Json::IDeserializer<VersionT>& get_versiont_deserializer_instance(); + + ExpectedS<std::map<std::string, VersionT, std::less<>>> parse_baseline_file(Files::Filesystem& fs, + StringView baseline_name, + const fs::path& baseline_file_path); + + ExpectedS<std::vector<VersionDbEntry>> parse_versions_file(Files::Filesystem& fs, + StringView port_name, + const fs::path& versions_file_path); +}
\ No newline at end of file diff --git a/toolsrc/include/vcpkg/versions.h b/toolsrc/include/vcpkg/versions.h index 55586ddcc..7d5b573c2 100644 --- a/toolsrc/include/vcpkg/versions.h +++ b/toolsrc/include/vcpkg/versions.h @@ -1,5 +1,7 @@ #pragma once +#include <vcpkg/versiont.h> + namespace vcpkg::Versions { enum class Scheme @@ -10,6 +12,25 @@ namespace vcpkg::Versions String }; + struct VersionSpec + { + std::string port_name; + VersionT version; + Scheme scheme; + + VersionSpec(const std::string& port_name, const VersionT& version, Scheme scheme); + + VersionSpec(const std::string& port_name, const std::string& version_string, int port_version, Scheme scheme); + + friend bool operator==(const VersionSpec& lhs, const VersionSpec& rhs); + friend bool operator!=(const VersionSpec& lhs, const VersionSpec& rhs); + }; + + struct VersionSpecHasher + { + std::size_t operator()(const VersionSpec& key) const; + }; + struct Constraint { enum class Type diff --git a/toolsrc/src/vcpkg/portfileprovider.cpp b/toolsrc/src/vcpkg/portfileprovider.cpp index 9c09a23e4..e3053bfa6 100644 --- a/toolsrc/src/vcpkg/portfileprovider.cpp +++ b/toolsrc/src/vcpkg/portfileprovider.cpp @@ -1,3 +1,6 @@ + + +#include <vcpkg/base/json.h> #include <vcpkg/base/system.debug.h> #include <vcpkg/configuration.h> @@ -7,6 +10,33 @@ #include <vcpkg/sourceparagraph.h> #include <vcpkg/vcpkgcmdarguments.h> #include <vcpkg/vcpkgpaths.h> +#include <vcpkg/versiondeserializers.h> + +#include <regex> + +using namespace vcpkg; +using namespace Versions; + +namespace +{ + Optional<fs::path> get_versions_json_path(const VcpkgPaths& paths, StringView port_name) + { + const auto port_versions_dir_path = paths.root / fs::u8path("port_versions"); + const auto subpath = Strings::concat(port_name.substr(0, 1), "-/", port_name, ".json"); + const auto json_path = port_versions_dir_path / subpath; + if (paths.get_filesystem().exists(json_path)) + { + return json_path; + } + return nullopt; + } + + Optional<fs::path> get_baseline_json_path(const VcpkgPaths& paths, StringView baseline_commit_sha) + { + const auto baseline_json = paths.git_checkout_baseline(paths.get_filesystem(), baseline_commit_sha); + return paths.get_filesystem().exists(baseline_json) ? make_optional(baseline_json) : nullopt; + } +} namespace vcpkg::PortFileProvider { @@ -27,7 +57,7 @@ namespace vcpkg::PortFileProvider return Util::fmap(ports, [](auto&& kvpair) -> const SourceControlFileLocation* { return &kvpair.second; }); } - PathsPortFileProvider::PathsPortFileProvider(const vcpkg::VcpkgPaths& paths_, + PathsPortFileProvider::PathsPortFileProvider(const VcpkgPaths& paths_, const std::vector<std::string>& overlay_ports_) : paths(paths_) { @@ -80,7 +110,7 @@ namespace vcpkg::PortFileProvider } else { - vcpkg::print_error_message(maybe_scf.error()); + print_error_message(maybe_scf.error()); Checks::exit_with_message( VCPKG_LINE_INFO, "Error: Failed to load port %s from %s", spec, fs::u8string(ports_dir)); } @@ -106,7 +136,7 @@ namespace vcpkg::PortFileProvider } else { - vcpkg::print_error_message(found_scf.error()); + print_error_message(found_scf.error()); Checks::exit_with_message( VCPKG_LINE_INFO, "Error: Failed to load port %s from %s", spec, fs::u8string(ports_dir)); } @@ -148,7 +178,7 @@ namespace vcpkg::PortFileProvider } else { - vcpkg::print_error_message(found_scf.error()); + print_error_message(found_scf.error()); Checks::exit_with_message( VCPKG_LINE_INFO, "Error: Failed to load port %s from %s", spec, fs::u8string(port_directory)); } @@ -224,7 +254,7 @@ namespace vcpkg::PortFileProvider } else { - vcpkg::print_error_message(maybe_scf.error()); + print_error_message(maybe_scf.error()); Checks::exit_with_message( VCPKG_LINE_INFO, "Error: Failed to load port from %s", fs::u8string(ports_dir)); } @@ -257,4 +287,154 @@ namespace vcpkg::PortFileProvider return ret; } + + namespace details + { + struct BaselineProviderImpl + { + BaselineProviderImpl(const VcpkgPaths& paths, const std::string& baseline) + : paths(paths), baseline(baseline) + { + } + ~BaselineProviderImpl() { } + + const std::map<std::string, VersionT, std::less<>>& get_baseline_cache() const + { + return baseline_cache.get_lazy([&]() -> auto { + auto maybe_baseline_file = get_baseline_json_path(paths, baseline); + Checks::check_exit(VCPKG_LINE_INFO, maybe_baseline_file.has_value(), "Couldn't find baseline.json"); + auto baseline_file = maybe_baseline_file.value_or_exit(VCPKG_LINE_INFO); + + auto maybe_baselines_map = parse_baseline_file(paths.get_filesystem(), "default", baseline_file); + Checks::check_exit(VCPKG_LINE_INFO, + maybe_baselines_map.has_value(), + "Error: Couldn't parse baseline `%s` from `%s`", + "default", + fs::u8string(baseline_file)); + auto baselines_map = *maybe_baselines_map.get(); + return std::move(baselines_map); + }); + } + + private: + const VcpkgPaths& paths; + const std::string baseline; + Lazy<std::map<std::string, VersionT, std::less<>>> baseline_cache; + }; + + struct VersionedPortfileProviderImpl + { + std::map<std::string, std::vector<VersionSpec>> versions_cache; + std::unordered_map<VersionSpec, std::string, VersionSpecHasher> git_tree_cache; + std::unordered_map<VersionSpec, SourceControlFileLocation, VersionSpecHasher> control_cache; + + VersionedPortfileProviderImpl(const VcpkgPaths& paths) : paths(paths) { } + ~VersionedPortfileProviderImpl() { } + + const VcpkgPaths& get_paths() const { return paths; } + Files::Filesystem& get_filesystem() const { return paths.get_filesystem(); } + + private: + const VcpkgPaths& paths; + }; + } + + VersionedPortfileProvider::VersionedPortfileProvider(const VcpkgPaths& paths) + : m_impl(std::make_unique<details::VersionedPortfileProviderImpl>(paths)) + { + } + VersionedPortfileProvider::~VersionedPortfileProvider() { } + + const std::vector<VersionSpec>& VersionedPortfileProvider::get_port_versions(StringView port_name) const + { + auto cache_it = m_impl->versions_cache.find(port_name.to_string()); + if (cache_it != m_impl->versions_cache.end()) + { + return cache_it->second; + } + + auto maybe_versions_file_path = get_versions_json_path(m_impl->get_paths(), port_name); + Checks::check_exit(VCPKG_LINE_INFO, + maybe_versions_file_path.has_value(), + "Error: Couldn't find a versions database file: %s.json.", + port_name); + auto versions_file_path = maybe_versions_file_path.value_or_exit(VCPKG_LINE_INFO); + + auto maybe_version_entries = parse_versions_file(m_impl->get_filesystem(), port_name, versions_file_path); + Checks::check_exit(VCPKG_LINE_INFO, + maybe_version_entries.has_value(), + "Error: Couldn't parse versions from file: %s", + fs::u8string(versions_file_path)); + auto version_entries = maybe_version_entries.value_or_exit(VCPKG_LINE_INFO); + + auto port = port_name.to_string(); + for (auto&& version_entry : version_entries) + { + VersionSpec spec(port, version_entry.version, version_entry.scheme); + m_impl->versions_cache[port].push_back(spec); + m_impl->git_tree_cache.emplace(std::move(spec), std::move(version_entry.git_tree)); + } + return m_impl->versions_cache.at(port); + } + + ExpectedS<const SourceControlFileLocation&> VersionedPortfileProvider::get_control_file( + const VersionSpec& version_spec) const + { + auto cache_it = m_impl->control_cache.find(version_spec); + if (cache_it != m_impl->control_cache.end()) + { + return cache_it->second; + } + + // Pre-populate versions cache. + get_port_versions(version_spec.port_name); + + auto git_tree_cache_it = m_impl->git_tree_cache.find(version_spec); + if (git_tree_cache_it == m_impl->git_tree_cache.end()) + { + return Strings::concat("No git object SHA for entry %s at version %s.", + version_spec.port_name, + version_spec.version.to_string()); + } + + const std::string git_tree = git_tree_cache_it->second; + auto port_directory = + m_impl->get_paths().git_checkout_port(m_impl->get_filesystem(), version_spec.port_name, git_tree); + + auto maybe_control_file = Paragraphs::try_load_port(m_impl->get_filesystem(), port_directory); + if (auto scf = maybe_control_file.get()) + { + if (scf->get()->core_paragraph->name == version_spec.port_name) + { + return m_impl->control_cache + .emplace(version_spec, SourceControlFileLocation{std::move(*scf), std::move(port_directory)}) + .first->second; + } + return Strings::format("Error: Failed to load port from %s: names did not match: '%s' != '%s'", + fs::u8string(port_directory), + version_spec.port_name, + scf->get()->core_paragraph->name); + } + + print_error_message(maybe_control_file.error()); + return Strings::format( + "Error: Failed to load port %s from %s", version_spec.port_name, fs::u8string(port_directory)); + } + + BaselineProvider::BaselineProvider(const VcpkgPaths& paths, const std::string& baseline) + : m_impl(std::make_unique<details::BaselineProviderImpl>(paths, baseline)) + { + } + BaselineProvider::~BaselineProvider() { } + + Optional<VersionT> BaselineProvider::get_baseline_version(StringView port_name) const + { + const auto& cache = m_impl->get_baseline_cache(); + auto it = cache.find(port_name.to_string()); + if (it != cache.end()) + { + return it->second; + } + return nullopt; + } } diff --git a/toolsrc/src/vcpkg/registries.cpp b/toolsrc/src/vcpkg/registries.cpp index e481685d9..7794ed9b0 100644 --- a/toolsrc/src/vcpkg/registries.cpp +++ b/toolsrc/src/vcpkg/registries.cpp @@ -5,7 +5,9 @@ #include <vcpkg/configurationdeserializer.h> #include <vcpkg/registries.h> +#include <vcpkg/vcpkgcmdarguments.h> #include <vcpkg/vcpkgpaths.h> +#include <vcpkg/versiondeserializers.h> #include <vcpkg/versiont.h> #include <map> @@ -70,32 +72,6 @@ namespace } }; - struct VersionTDeserializer final : Json::IDeserializer<VersionT> - { - StringView type_name() const override { return "a version object"; } - View<StringView> valid_fields() const override - { - static const StringView t[] = {"version-string", "port-version"}; - return t; - } - - Optional<VersionT> visit_object(Json::Reader& r, const Json::Object& obj) override - { - std::string version; - int port_version = 0; - - r.required_object_field(type_name(), obj, "version-string", version, version_deserializer); - r.optional_object_field(obj, "port-version", port_version, Json::NaturalNumberDeserializer::instance); - - return VersionT{std::move(version), port_version}; - } - - static Json::StringDeserializer version_deserializer; - static VersionTDeserializer instance; - }; - Json::StringDeserializer VersionTDeserializer::version_deserializer{"version"}; - VersionTDeserializer VersionTDeserializer::instance; - struct FilesystemVersionEntryDeserializer final : Json::IDeserializer<std::pair<VersionT, fs::path>> { StringView type_name() const override { return "a version entry object"; } @@ -109,7 +85,7 @@ namespace { fs::path registry_path; - auto version = VersionTDeserializer::instance.visit_object(r, obj); + auto version = get_versiont_deserializer_instance().visit_object(r, obj); r.required_object_field( "version entry", obj, "registry-path", registry_path, Json::PathDeserializer::instance); @@ -162,30 +138,6 @@ namespace const fs::path& registry_root; }; - struct BaselineDeserializer final : Json::IDeserializer<std::map<std::string, VersionT, std::less<>>> - { - StringView type_name() const override { return "a baseline object"; } - - Optional<type> visit_object(Json::Reader& r, const Json::Object& obj) override - { - std::map<std::string, VersionT, std::less<>> result; - - for (auto pr : obj) - { - const auto& version_value = pr.second; - VersionT version; - r.visit_in_key(version_value, pr.first, version, VersionTDeserializer::instance); - - result.emplace(pr.first.to_string(), std::move(version)); - } - - return std::move(result); - } - - static BaselineDeserializer instance; - }; - BaselineDeserializer BaselineDeserializer::instance; - struct FilesystemRegistry final : RegistryImpl { std::unique_ptr<RegistryEntry> get_port_entry(const VcpkgPaths& paths, StringView port_name) const override @@ -267,6 +219,12 @@ namespace Optional<VersionT> get_baseline_version(const VcpkgPaths& paths, StringView port_name) const override { + if (!paths.get_feature_flags().versions) + { + Checks::check_exit(VCPKG_LINE_INFO, + "This invocation failed because the `versions` feature flag is not enabled."); + } + const auto& baseline_cache = baseline.get([this, &paths] { return load_baseline_versions(paths); }); auto it = baseline_cache.find(port_name); if (it != baseline_cache.end()) @@ -310,26 +268,17 @@ namespace Checks::exit_with_message(VCPKG_LINE_INFO, "Error: `baseline.json` does not have a top-level object."); } - const auto& obj = value.first.object(); - auto baseline_value = obj.get("default"); - if (!baseline_value) - { - Checks::exit_with_message( - VCPKG_LINE_INFO, "Error: `baseline.json` does not contain the baseline \"%s\"", "default"); - } - - Json::Reader r; - std::map<std::string, VersionT, std::less<>> result; - r.visit_in_key(*baseline_value, "default", result, BaselineDeserializer::instance); - - if (r.errors().empty()) + auto maybe_baseline_versions = parse_baseline_file(paths.get_filesystem(), "default", baseline_file); + if (auto baseline_versions = maybe_baseline_versions.get()) { - return result; + return std::move(*baseline_versions); } else { - Checks::exit_with_message( - VCPKG_LINE_INFO, "Error: failed to parse `baseline.json`:\n%s", Strings::join("\n", r.errors())); + Checks::exit_with_message(VCPKG_LINE_INFO, + "Error: failed to parse `%s`:\n%s", + fs::u8string(baseline_file), + maybe_baseline_versions.error()); } } diff --git a/toolsrc/src/vcpkg/vcpkgpaths.cpp b/toolsrc/src/vcpkg/vcpkgpaths.cpp index a3ab21629..5b33b8db4 100644 --- a/toolsrc/src/vcpkg/vcpkgpaths.cpp +++ b/toolsrc/src/vcpkg/vcpkgpaths.cpp @@ -71,6 +71,15 @@ namespace return result; } + System::CmdLineBuilder git_cmd_builder(const VcpkgPaths& paths, + const fs::path& dot_git_dir, + const fs::path& work_tree) + { + return System::CmdLineBuilder() + .path_arg(paths.get_tool_exe(Tools::GIT)) + .string_arg(Strings::concat("--git-dir=", fs::u8string(dot_git_dir))) + .string_arg(Strings::concat("--work-tree=", fs::u8string(work_tree))); + } } // unnamed namespace namespace vcpkg @@ -353,6 +362,18 @@ If you wish to silence this error and use classic mode, you can: vcpkg_dir_info = vcpkg_dir / fs::u8path("info"); vcpkg_dir_updates = vcpkg_dir / fs::u8path("updates"); + // Versioning paths + const auto versioning_tmp = buildtrees / fs::u8path("versioning_tmp"); + const auto versioning_output = buildtrees / fs::u8path("versioning"); + + baselines_dot_git_dir = versioning_tmp / fs::u8path(".baselines.git"); + baselines_work_tree = versioning_tmp / fs::u8path("baselines-worktree"); + baselines_output = versioning_output / fs::u8path("baselines"); + + versions_dot_git_dir = versioning_tmp / fs::u8path(".versions.git"); + versions_work_tree = versioning_tmp / fs::u8path("versions-worktree"); + versions_output = versioning_output / fs::u8path("versions"); + ports_cmake = filesystem.canonical(VCPKG_LINE_INFO, scripts / fs::u8path("ports.cmake")); for (auto&& overlay_triplets_dir : args.overlay_triplets) @@ -456,6 +477,178 @@ If you wish to silence this error and use classic mode, you can: return m_pimpl->m_tool_cache->get_tool_version(*this, tool); } + void VcpkgPaths::git_checkout_subpath(const VcpkgPaths& paths, + StringView commit_sha, + const fs::path& subpath, + const fs::path& local_repo, + const fs::path& destination, + const fs::path& dot_git_dir, + const fs::path& work_tree) + { + Files::Filesystem& fs = paths.get_filesystem(); + fs.remove_all(work_tree, VCPKG_LINE_INFO); + fs.remove_all(destination, VCPKG_LINE_INFO); + fs.remove_all(dot_git_dir, VCPKG_LINE_INFO); + + // All git commands are run with: --git-dir={dot_git_dir} --work-tree={work_tree_temp} + // git clone --no-checkout --local {vcpkg_root} {dot_git_dir} + System::CmdLineBuilder clone_cmd_builder = git_cmd_builder(paths, dot_git_dir, work_tree) + .string_arg("clone") + .string_arg("--no-checkout") + .string_arg("--local") + .path_arg(local_repo) + .path_arg(dot_git_dir); + const auto clone_output = System::cmd_execute_and_capture_output(clone_cmd_builder.extract()); + Checks::check_exit(VCPKG_LINE_INFO, + clone_output.exit_code == 0, + "Failed to clone temporary vcpkg instance.\n%s\n", + clone_output.output); + + // git checkout {commit-sha} -- {subpath} + System::CmdLineBuilder checkout_cmd_builder = git_cmd_builder(paths, dot_git_dir, work_tree) + .string_arg("checkout") + .string_arg(commit_sha) + .string_arg("--") + .path_arg(subpath); + const auto checkout_output = System::cmd_execute_and_capture_output(checkout_cmd_builder.extract()); + Checks::check_exit(VCPKG_LINE_INFO, + checkout_output.exit_code == 0, + "Error: Failed to checkout %s:%s\n%s\n", + commit_sha, + fs::u8string(subpath), + checkout_output.output); + + const fs::path checked_out_path = work_tree / subpath; + const auto& containing_folder = destination.parent_path(); + if (!fs.exists(containing_folder)) + { + fs.create_directories(containing_folder, VCPKG_LINE_INFO); + } + + std::error_code ec; + fs.rename_or_copy(checked_out_path, destination, ".tmp", ec); + fs.remove_all(work_tree, VCPKG_LINE_INFO); + fs.remove_all(dot_git_dir, VCPKG_LINE_INFO); + if (ec) + { + System::printf(System::Color::error, + "Error: Couldn't move checked out files from %s to destination %s", + fs::u8string(checked_out_path), + fs::u8string(destination)); + Checks::exit_fail(VCPKG_LINE_INFO); + } + } + + void VcpkgPaths::git_checkout_object(const VcpkgPaths& paths, + StringView git_object, + const fs::path& local_repo, + const fs::path& destination, + const fs::path& dot_git_dir, + const fs::path& work_tree) + { + Files::Filesystem& fs = paths.get_filesystem(); + fs.remove_all(work_tree, VCPKG_LINE_INFO); + fs.remove_all(destination, VCPKG_LINE_INFO); + + if (!fs.exists(dot_git_dir)) + { + // All git commands are run with: --git-dir={dot_git_dir} --work-tree={work_tree_temp} + // git clone --no-checkout --local {vcpkg_root} {dot_git_dir} + System::CmdLineBuilder clone_cmd_builder = git_cmd_builder(paths, dot_git_dir, work_tree) + .string_arg("clone") + .string_arg("--no-checkout") + .string_arg("--local") + .path_arg(local_repo) + .path_arg(dot_git_dir); + const auto clone_output = System::cmd_execute_and_capture_output(clone_cmd_builder.extract()); + Checks::check_exit(VCPKG_LINE_INFO, + clone_output.exit_code == 0, + "Failed to clone temporary vcpkg instance.\n%s\n", + clone_output.output); + } + else + { + System::CmdLineBuilder fetch_cmd_builder = + git_cmd_builder(paths, dot_git_dir, work_tree).string_arg("fetch"); + const auto fetch_output = System::cmd_execute_and_capture_output(fetch_cmd_builder.extract()); + Checks::check_exit(VCPKG_LINE_INFO, + fetch_output.exit_code == 0, + "Failed to update refs on temporary vcpkg repository.\n%s\n", + fetch_output.output); + } + + if (!fs.exists(work_tree)) + { + fs.create_directories(work_tree, VCPKG_LINE_INFO); + } + + // git checkout {tree_object} . + System::CmdLineBuilder checkout_cmd_builder = git_cmd_builder(paths, dot_git_dir, work_tree) + .string_arg("checkout") + .string_arg(git_object) + .string_arg("."); + const auto checkout_output = System::cmd_execute_and_capture_output(checkout_cmd_builder.extract()); + Checks::check_exit(VCPKG_LINE_INFO, checkout_output.exit_code == 0, "Failed to checkout %s", git_object); + + const auto& containing_folder = destination.parent_path(); + if (!fs.exists(containing_folder)) + { + fs.create_directories(containing_folder, VCPKG_LINE_INFO); + } + + std::error_code ec; + fs.rename_or_copy(work_tree, destination, ".tmp", ec); + fs.remove_all(work_tree, VCPKG_LINE_INFO); + if (ec) + { + System::printf(System::Color::error, + "Error: Couldn't move checked out files from %s to destination %s", + fs::u8string(work_tree), + fs::u8string(destination)); + Checks::exit_fail(VCPKG_LINE_INFO); + } + } + + fs::path VcpkgPaths::git_checkout_baseline(Files::Filesystem& fs, StringView commit_sha) const + { + const fs::path local_repo = this->root; + const fs::path destination = this->baselines_output / fs::u8path(commit_sha) / fs::u8path("baseline.json"); + const fs::path baseline_subpath = fs::u8path("port_versions") / fs::u8path("baseline.json"); + + if (!fs.exists(destination)) + { + git_checkout_subpath(*this, + commit_sha, + baseline_subpath, + local_repo, + destination, + this->baselines_dot_git_dir, + this->baselines_work_tree); + } + return destination; + } + + fs::path VcpkgPaths::git_checkout_port(Files::Filesystem& fs, StringView port_name, StringView git_tree) const + { + /* Clone a new vcpkg repository instance using the local instance as base. + * + * The `--git-dir` directory will store all the Git metadata files, + * and the `--work-tree` is the directory where files will be checked out. + * + * Since we are checking a git tree object, all files will be checked out to the root of `work-tree`. + * Because of that, it makes sense to use the git hash as the name for the directory. + */ + const fs::path local_repo = this->root; + const fs::path destination = this->versions_output / fs::u8path(git_tree) / fs::u8path(port_name); + + if (!fs.exists(destination / "CONTROL") && !fs.exists(destination / "vcpkg.json")) + { + git_checkout_object( + *this, git_tree, local_repo, destination, this->versions_dot_git_dir, this->versions_work_tree); + } + return destination; + } + Optional<const Json::Object&> VcpkgPaths::get_manifest() const { if (auto p = m_pimpl->m_manifest_doc.get()) diff --git a/toolsrc/src/vcpkg/versiondeserializers.cpp b/toolsrc/src/vcpkg/versiondeserializers.cpp new file mode 100644 index 000000000..276b70e4f --- /dev/null +++ b/toolsrc/src/vcpkg/versiondeserializers.cpp @@ -0,0 +1,210 @@ +#include <vcpkg/versiondeserializers.h> + +using namespace vcpkg; +using namespace vcpkg::Versions; + +namespace +{ + struct VersionDbEntryDeserializer final : Json::IDeserializer<VersionDbEntry> + { + static constexpr StringLiteral VERSION_RELAXED = "version"; + static constexpr StringLiteral VERSION_SEMVER = "version-semver"; + static constexpr StringLiteral VERSION_STRING = "version-string"; + static constexpr StringLiteral VERSION_DATE = "version-date"; + static constexpr StringLiteral PORT_VERSION = "port-version"; + static constexpr StringLiteral GIT_TREE = "git-tree"; + + StringView type_name() const override { return "a version database entry"; } + View<StringView> valid_fields() const override + { + static const StringView t[] = { + VERSION_RELAXED, VERSION_SEMVER, VERSION_STRING, VERSION_DATE, PORT_VERSION, GIT_TREE}; + return t; + } + + Optional<VersionDbEntry> visit_object(Json::Reader& r, const Json::Object& obj) override + { + std::string version; + int port_version = 0; + std::string git_tree; + Versions::Scheme version_scheme = Versions::Scheme::String; + + // Code copy-and-paste'd from sourceparagraph.cpp + static Json::StringDeserializer version_exact_deserializer{"an exact version string"}; + static Json::StringDeserializer version_relaxed_deserializer{"a relaxed version string"}; + static Json::StringDeserializer version_semver_deserializer{"a semantic version string"}; + static Json::StringDeserializer version_date_deserializer{"a date version string"}; + static Json::StringDeserializer git_tree_deserializer("a git object SHA"); + + bool has_exact = r.optional_object_field(obj, VERSION_STRING, version, version_exact_deserializer); + bool has_relax = r.optional_object_field(obj, VERSION_RELAXED, version, version_relaxed_deserializer); + bool has_semver = r.optional_object_field(obj, VERSION_SEMVER, version, version_semver_deserializer); + bool has_date = r.optional_object_field(obj, VERSION_DATE, version, version_date_deserializer); + int num_versions = (int)has_exact + (int)has_relax + (int)has_semver + (int)has_date; + if (num_versions == 0) + { + r.add_generic_error(type_name(), "expected a versioning field (example: ", VERSION_STRING, ")"); + } + else if (num_versions > 1) + { + r.add_generic_error(type_name(), "expected only one versioning field"); + } + else + { + if (has_exact) + version_scheme = Versions::Scheme::String; + else if (has_relax) + version_scheme = Versions::Scheme::Relaxed; + else if (has_semver) + version_scheme = Versions::Scheme::Semver; + else if (has_date) + version_scheme = Versions::Scheme::Date; + else + Checks::unreachable(VCPKG_LINE_INFO); + } + r.optional_object_field(obj, PORT_VERSION, port_version, Json::NaturalNumberDeserializer::instance); + r.required_object_field(type_name(), obj, GIT_TREE, git_tree, git_tree_deserializer); + + return VersionDbEntry(version, port_version, version_scheme, git_tree); + } + + static VersionDbEntryDeserializer instance; + }; + + struct VersionDbEntryArrayDeserializer final : Json::IDeserializer<std::vector<VersionDbEntry>> + { + virtual StringView type_name() const override { return "an array of versions"; } + + virtual Optional<std::vector<VersionDbEntry>> visit_array(Json::Reader& r, const Json::Array& arr) override + { + return r.array_elements(arr, VersionDbEntryDeserializer::instance); + } + + static VersionDbEntryArrayDeserializer instance; + }; + + VersionDbEntryDeserializer VersionDbEntryDeserializer::instance; + VersionDbEntryArrayDeserializer VersionDbEntryArrayDeserializer::instance; + + struct BaselineDeserializer final : Json::IDeserializer<std::map<std::string, VersionT, std::less<>>> + { + StringView type_name() const override { return "a baseline object"; } + + Optional<type> visit_object(Json::Reader& r, const Json::Object& obj) override + { + std::map<std::string, VersionT, std::less<>> result; + + for (auto&& pr : obj) + { + const auto& version_value = pr.second; + VersionT version; + r.visit_in_key(version_value, pr.first, version, get_versiont_deserializer_instance()); + + result.emplace(pr.first.to_string(), std::move(version)); + } + + return std::move(result); + } + + static BaselineDeserializer instance; + }; + BaselineDeserializer BaselineDeserializer::instance; + + struct VersionTDeserializer final : Json::IDeserializer<VersionT> + { + StringView type_name() const override { return "a version object"; } + View<StringView> valid_fields() const override + { + static const StringView t[] = {"version-string", "port-version"}; + return t; + } + + Optional<VersionT> visit_object(Json::Reader& r, const Json::Object& obj) override + { + std::string version; + int port_version = 0; + + r.required_object_field(type_name(), obj, "version-string", version, version_deserializer); + r.optional_object_field(obj, "port-version", port_version, Json::NaturalNumberDeserializer::instance); + + return VersionT{std::move(version), port_version}; + } + + static Json::StringDeserializer version_deserializer; + static VersionTDeserializer instance; + }; + Json::StringDeserializer VersionTDeserializer::version_deserializer{"version"}; + VersionTDeserializer VersionTDeserializer::instance; +} + +namespace vcpkg +{ + Json::IDeserializer<VersionT>& get_versiont_deserializer_instance() { return VersionTDeserializer::instance; } + + ExpectedS<std::map<std::string, VersionT, std::less<>>> parse_baseline_file(Files::Filesystem& fs, + StringView baseline_name, + const fs::path& baseline_file_path) + { + if (!fs.exists(baseline_file_path)) + { + return Strings::format("Couldn't find `%s`", fs::u8string(baseline_file_path)); + } + + auto value = Json::parse_file(VCPKG_LINE_INFO, fs, baseline_file_path); + if (!value.first.is_object()) + { + return Strings::format("Error: `%s` does not have a top-level object.", fs::u8string(baseline_file_path)); + } + + const auto& obj = value.first.object(); + auto baseline_value = obj.get(baseline_name); + if (!baseline_value) + { + return Strings::format( + "Error: `%s` does not contain the baseline \"%s\"", fs::u8string(baseline_file_path), baseline_name); + } + + Json::Reader r; + std::map<std::string, VersionT, std::less<>> result; + r.visit_in_key(*baseline_value, baseline_name, result, BaselineDeserializer::instance); + if (!r.errors().empty()) + { + return Strings::format( + "Error: failed to parse `%s`:\n%s", fs::u8string(baseline_file_path), Strings::join("\n", r.errors())); + } + return result; + } + + ExpectedS<std::vector<VersionDbEntry>> parse_versions_file(Files::Filesystem& fs, + StringView port_name, + const fs::path& versions_file_path) + { + (void)port_name; + if (!fs.exists(versions_file_path)) + { + return Strings::format("Couldn't find the versions database file: %s", fs::u8string(versions_file_path)); + } + + auto versions_json = Json::parse_file(VCPKG_LINE_INFO, fs, versions_file_path); + if (!versions_json.first.is_object()) + { + return Strings::format("Error: `%s` does not have a top level object.", fs::u8string(versions_file_path)); + } + + const auto& versions_object = versions_json.first.object(); + auto maybe_versions_array = versions_object.get("versions"); + if (!maybe_versions_array || !maybe_versions_array->is_array()) + { + return Strings::format("Error: `%s` does not contain a versions array.", fs::u8string(versions_file_path)); + } + + std::vector<VersionDbEntry> db_entries; + // Avoid warning treated as error. + if (maybe_versions_array != nullptr) + { + Json::Reader r; + r.visit_in_key(*maybe_versions_array, "versions", db_entries, VersionDbEntryArrayDeserializer::instance); + } + return db_entries; + } +} diff --git a/toolsrc/src/vcpkg/versions.cpp b/toolsrc/src/vcpkg/versions.cpp new file mode 100644 index 000000000..ac1829712 --- /dev/null +++ b/toolsrc/src/vcpkg/versions.cpp @@ -0,0 +1,34 @@ +#include <vcpkg/versions.h> + +namespace vcpkg::Versions +{ + VersionSpec::VersionSpec(const std::string& port_name, const VersionT& version, Scheme scheme) + : port_name(port_name), version(version), scheme(scheme) + { + } + + VersionSpec::VersionSpec(const std::string& port_name, + const std::string& version_string, + int port_version, + Scheme scheme) + : port_name(port_name), version(version_string, port_version), scheme(scheme) + { + } + + bool operator==(const VersionSpec& lhs, const VersionSpec& rhs) + { + return std::tie(lhs.port_name, lhs.version, lhs.scheme) == std::tie(rhs.port_name, rhs.version, rhs.scheme); + } + + bool operator!=(const VersionSpec& lhs, const VersionSpec& rhs) { return !(lhs == rhs); } + + std::size_t VersionSpecHasher::operator()(const VersionSpec& key) const + { + using std::hash; + using std::size_t; + using std::string; + + return ((hash<string>()(key.port_name) ^ (hash<string>()(key.version.to_string()) << 1)) >> 1) ^ + (hash<int>()(static_cast<int>(key.scheme)) << 1); + } +}
\ No newline at end of file diff --git a/toolsrc/windows-bootstrap/vcpkg.vcxproj b/toolsrc/windows-bootstrap/vcpkg.vcxproj index 5f897bc67..16de6e9c4 100644 --- a/toolsrc/windows-bootstrap/vcpkg.vcxproj +++ b/toolsrc/windows-bootstrap/vcpkg.vcxproj @@ -267,6 +267,7 @@ <ClInclude Include="..\include\vcpkg\vcpkgcmdarguments.h" />
<ClInclude Include="..\include\vcpkg\vcpkglib.h" />
<ClInclude Include="..\include\vcpkg\vcpkgpaths.h" />
+ <ClInclude Include="..\include\vcpkg\versiondeserializers.h" />
<ClInclude Include="..\include\vcpkg\versions.h" />
<ClInclude Include="..\include\vcpkg\versiont.h" />
<ClInclude Include="..\include\vcpkg\visualstudio.h" />
@@ -355,6 +356,8 @@ <ClCompile Include="..\src\vcpkg\vcpkgcmdarguments.cpp" />
<ClCompile Include="..\src\vcpkg\vcpkglib.cpp" />
<ClCompile Include="..\src\vcpkg\vcpkgpaths.cpp" />
+ <ClCompile Include="..\src\vcpkg\versiondeserializers.cpp" />
+ <ClCompile Include="..\src\vcpkg\versions.cpp" />
<ClCompile Include="..\src\vcpkg\versiont.cpp" />
<ClCompile Include="..\src\vcpkg\visualstudio.cpp" />
<ClCompile Include="..\src\vcpkg.cpp" />
|
