From 44a980b07e51cd698ff3229a63cbe27ad36ac271 Mon Sep 17 00:00:00 2001 From: Eduardo Dantas Date: Thu, 13 Aug 2026 18:47:47 -0300 Subject: [PATCH] build(cache): share vcpkg dependencies across CMake and Visual Studio (#1800) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Add shared vcpkg dependency cache for CMake and Visual Studio builds This commit adds a content addressed vcpkg dependency cache that can be reused across compatible Git worktrees independent forks CMake presets and Visual Studio Solution builds. The goal is to reduce repeated dependency installation and rebuild time while keeping build outputs isolated per worktree. The cache only shares dependency artifacts that are proven compatible through a complete dependency contract. Project build outputs such as object files PCH files PDB files generated sources intermediate files and executables remain local to each checkout. The shared cache is opt in. When the cache is not configured or when the resolver cannot prove that reuse is safe both CMake and Visual Studio fall back to local worktree specific vcpkg roots. Main behavior added • Shares vcpkg downloads globally • Shares vcpkg binary packages globally • Shares expanded vcpkg installed trees only when the dependency fingerprint matches • Shares vcpkg buildtrees and packages only when the dependency fingerprint matches • Keeps CMake build trees local to each worktree • Keeps Visual Studio output directories local to each worktree • Keeps generated files intermediate files executables PCH files PDB files and object files local • Allows compatible Git worktrees to reuse dependency artifacts • Allows compatible forks to reuse dependency artifacts • Allows compatible CMake presets and Visual Studio configurations to converge on the same dependency cache • Falls back to local vcpkg storage when shared cache safety cannot be verified CMake integration The new CMake shared cache module runs before project initialization and resolves the vcpkg dependency contract before the normal configure flow continues. The resolver validates the manifest registries overlays triplets features vcpkg revision compiler toolset SDK and cache implementation before selecting fingerprinted dependency roots. This prevents incompatible configurations from sharing the same expanded dependency tree. If the CMake version is too old for the shared cache module and shared cache variables are present the build warns and continues with build local vcpkg storage instead. CMake preset changes • Removes hardcoded VCPKG_INSTALLED_DIR values from presets • Lets the shared cache resolver select the installed root when safe • Keeps local fallback behavior available • Replaces the previous preset binary source override with clean packages and clean buildtrees options • Prevents Visual Studio CMake presets from inheriting Ninja compiler overrides where they should not • Keeps CMake build outputs under their configured build preset directories Visual Studio integration The Visual Studio bridge uses the same dependency resolver as CMake. It evaluates each Solution configuration through MSBuild, generates a machine local ignored SharedVcpkgCache props file and revalidates the complete contract before vcpkg manifest installation. Different Visual Studio configurations receive separate consumer fingerprints. They can still share an expanded dependency tree when their dependency inputs are identical, but their consumer contracts remain separate so MSBuild specific details cannot corrupt CMake or another Solution configuration. The Visual Studio project now imports vcpkg props more reliably, supports the generated shared cache props, and validates the generated contract through SharedVcpkgCache targets before dependency installation and build preparation. Visual Studio changes included • Adds SharedVcpkgCache targets • Adds generated SharedVcpkgCache props support • Adds vcpkg bootstrap property resolution from VCPKG ROOT • Keeps local fallback when the shared Solution cache is inactive • Validates manifest root target triplet host triplet configuration toolset SDK and install options before using the shared cache • Pins the Visual Studio instance used by vcpkg for the shared Solution contract • Requires the dependency CMake tool used by the generated contract • Revalidates the generated contract before vcpkg manifest install • Uses fingerprinted installed buildtrees and packages roots for compatible Solution builds • Keeps Solution outputs local Shared cache safety boundaries The shared cache is intentionally conservative. It only activates when the environment and contract can be fully validated. Safety rules enforced • The shared pool must be on a verified local fixed filesystem • The shared pool must live outside every registered checkout • UNC paths are rejected for the shared cache root • Reparse points and symlink escapes are rejected in sensitive paths • Mutable dependency roots are isolated by full contract fingerprint • vcpkg and shared cache operation locks protect mutable cache operations • Existing authenticated VCPKG BINARY SOURCES values are preserved • Existing authenticated binary source values are never printed • Existing authenticated binary source values are never persisted by the helper • Cleanup fails closed when validation is incomplete • Cleanup refuses to run while build related processes are active • Cleanup validates repositories configure trees generated contracts paths identities and locks before removing transient data • CI containers runtime data deployment paths and release behavior remain unchanged unless shared cache is explicitly enabled PowerShell tooling This commit adds setup audit cleanup repository registration and Solution contract generation tools. The main shared cache setup script configures the cache root registers the current Git repository family manages relevant user environment variables configures vcpkg downloads and binary cache locations updates managed sccache base directories and can invoke the Solution bridge when applicable. The Solution cache script evaluates MSBuild properties resolves the shared dependency contract through the CMake resolver writes the generated props file and supports audit only and validate only modes. Both PowerShell tools require PowerShell 7 2 and run under strict mode. Tooling added • tools configure shared build cache ps1 • tools configure shared solution cache ps1 • setup mode for shared cache configuration • audit only mode for validating existing shared cache state • guarded transient vcpkg cleanup • guarded fingerprint specific transient cleanup • repository registration and unregister support • generated Solution props creation • generated Solution props validation • generated Solution props audit • MSBuild property based contract evaluation • local fixed filesystem validation • active build process protection before cleanup Protobuf and Solution build fixes This branch also includes follow up fixes around protobuf generation and Solution build behavior. Protobuf and Solution changes • Fixes Solution protobuf generation by using one normalized absolute proto root • Uses the host triplet protoc path for protobuf generation • Adds incremental inputs and outputs to the ProtoCompile target • Excludes generated protobuf compile items when protobuf support is disabled • Keeps protobuf generated source handling tied to RunProtoCompile • Lets both CMake and Solution builds honor configured global vcpkg binary sources • Avoids leaking Ninja compiler overrides into Visual Studio CMake presets Documentation and repository updates • Adds shared build cache documentation • Documents setup and migration flow • Documents CMake and Visual Studio integration • Documents cache contract rules • Documents concurrency and locking behavior • Documents fallback behavior • Documents recovery and cleanup procedures • Documents cross fork compatibility rules • Updates README with shared cache guidance • Updates gitignore for generated local shared cache files • Updates CI path filters so vcpkg configuration overlay and cache related changes trigger the correct workflows Validation performed • PowerShell parser validation for both cache tools • PowerShell 5 1 prerequisite failure verified • CMake preset validation • JSON validation • Visual Studio project XML validation • Evaluated MSBuild property and item checks • Checked protobuf enabled mode • Checked protobuf disabled mode • Local shared cache Solution build validation • GitHub Actions Windows CMake matrix • GitHub Actions Windows Solution matrix Overall this commit adds a safe content addressed shared dependency cache for OTClient builds. It lets compatible CMake and Visual Studio configurations reuse expensive vcpkg dependency artifacts while keeping all build outputs local and falling back to worktree local storage whenever the shared contract cannot be fully validated. --- .github/workflows/ci.yml | 14 + .gitignore | 2 + CMakeLists.txt | 6 + CMakePresets.json | 16 +- README.md | 3 + cmake/SharedBuildCache.cmake | 3026 +++++++++++++++++++++ docs/development/shared-build-cache.md | 298 ++ tools/configure_shared_build_cache.ps1 | 1476 ++++++++++ tools/configure_shared_solution_cache.ps1 | 724 +++++ vc18/SharedVcpkgCache.targets | 77 + vc18/otclient.vcxproj | 50 +- 11 files changed, 5661 insertions(+), 31 deletions(-) create mode 100644 cmake/SharedBuildCache.cmake create mode 100644 docs/development/shared-build-cache.md create mode 100644 tools/configure_shared_build_cache.ps1 create mode 100644 tools/configure_shared_solution_cache.ps1 create mode 100644 vc18/SharedVcpkgCache.targets diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 4f7a7b832..3ddc1df80 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -16,6 +16,8 @@ on: - "tools/**" - "vc18/**" - "vcpkg.json" + - "vcpkg-configuration.json" + - "overlay-ports/**" - "CMakeLists.txt" - "CMakePresets.json" - "Dockerfile" @@ -47,6 +49,8 @@ on: - "tools/**" - "vc18/**" - "vcpkg.json" + - "vcpkg-configuration.json" + - "overlay-ports/**" - "CMakeLists.txt" - "CMakePresets.json" - "Dockerfile" @@ -97,6 +101,8 @@ jobs: - "cmake/**" - "vc18/**" - "vcpkg.json" + - "vcpkg-configuration.json" + - "overlay-ports/**" - "CMakeLists.txt" - "CMakePresets.json" - ".yamllint.yaml" @@ -110,6 +116,8 @@ jobs: - "src/**" - "cmake/**" - "vcpkg.json" + - "vcpkg-configuration.json" + - "overlay-ports/**" - "CMakeLists.txt" - "CMakePresets.json" - ".github/workflows/ci.yml" @@ -120,6 +128,8 @@ jobs: - "cmake/**" - "CMakeLists.txt" - "vcpkg.json" + - "vcpkg-configuration.json" + - "overlay-ports/**" - "build_luajit_android.sh" - "setup_android_deps.sh" - ".github/workflows/ci.yml" @@ -130,6 +140,8 @@ jobs: - "src/**" - "cmake/**" - "vcpkg.json" + - "vcpkg-configuration.json" + - "overlay-ports/**" - "CMakeLists.txt" - "CMakePresets.json" - ".yamllint.yaml" @@ -142,6 +154,8 @@ jobs: - "src/**" - "cmake/**" - "vcpkg.json" + - "vcpkg-configuration.json" + - "overlay-ports/**" - "CMakeLists.txt" - ".yamllint.yaml" - ".github/workflows/ci.yml" diff --git a/.gitignore b/.gitignore index 51e1737ec..1ee823df4 100644 --- a/.gitignore +++ b/.gitignore @@ -332,3 +332,5 @@ android/app/.cxx/ android/.gradle/ android/build/ luajit-src/ +CMakeUserPresets.json +**/.canary-shared-cache/ diff --git a/CMakeLists.txt b/CMakeLists.txt index 3674753c0..37068e8f4 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -36,6 +36,12 @@ if (VCPKG_TARGET_ANDROID) include_directories(Android_INCLUDES) endif() +if(CMAKE_VERSION VERSION_GREATER_EQUAL 3.19) + include("${CMAKE_CURRENT_SOURCE_DIR}/cmake/SharedBuildCache.cmake") +elseif(DEFINED ENV{CANARY_SHARED_CACHE_ROOT} OR DEFINED CANARY_SHARED_CACHE_ROOT) + message(WARNING "Shared build caching requires CMake 3.19 or newer; using build-local vcpkg storage.") +endif() + # ***************************************************************************** # Project otclient # ***************************************************************************** diff --git a/CMakePresets.json b/CMakePresets.json index 06c84d7a6..98882f776 100644 --- a/CMakePresets.json +++ b/CMakePresets.json @@ -18,7 +18,7 @@ }, "CMAKE_COLOR_DIAGNOSTICS": "ON", "SPEED_UP_BUILD_UNITY": "ON", - "VCPKG_INSTALL_OPTIONS": "--binarysource=clear;--binarysource=files,$env{VCPKG_ROOT}/binary-cache,readwrite" + "VCPKG_INSTALL_OPTIONS": "--clean-packages-after-build;--clean-buildtrees-after-build" } }, { @@ -28,10 +28,11 @@ "description": "Windows release build with Ninja and vcpkg", "cacheVariables": { "BUILD_STATIC_LIBRARY": "ON", + "CMAKE_C_COMPILER": "cl.exe", + "CMAKE_CXX_COMPILER": "cl.exe", "VCPKG_TARGET_TRIPLET": "x64-windows-static-release", "VCPKG_HOST_TRIPLET": "x64-windows", "VCPKG_OVERLAY_TRIPLETS": "${sourceDir}/cmake/triplets", - "VCPKG_INSTALLED_DIR": "${sourceDir}/vcpkg_installed/windows-release", "VCPKG_BUILD_TYPE": "release", "VCPKG_PLATFORM_TOOLSET": "v145", "CMAKE_BUILD_TYPE": "RelWithDebInfo", @@ -65,7 +66,6 @@ "BUILD_STATIC_LIBRARY": "OFF", "VCPKG_TARGET_TRIPLET": "x64-windows-test-release", "VCPKG_HOST_TRIPLET": "x64-windows", - "VCPKG_INSTALLED_DIR": "${sourceDir}/vcpkg_installed/windows-release-asan", "OTCLIENT_BUILD_TESTS": "ON", "VCPKG_MANIFEST_FEATURES": "tests" } @@ -83,7 +83,6 @@ "SPEED_UP_BUILD_UNITY": "OFF", "VCPKG_TARGET_TRIPLET": "x64-windows", "VCPKG_HOST_TRIPLET": "x64-windows", - "VCPKG_INSTALLED_DIR": "${sourceDir}/vcpkg_installed/windows-debug", "VCPKG_BUILD_TYPE": "debug", "OTCLIENT_BUILD_TESTS": "OFF" } @@ -96,7 +95,6 @@ "cacheVariables": { "VCPKG_TARGET_TRIPLET": "x64-windows-test-debug", "VCPKG_HOST_TRIPLET": "x64-windows", - "VCPKG_INSTALLED_DIR": "${sourceDir}/vcpkg_installed/windows-tests", "VCPKG_MANIFEST_FEATURES": "tests", "OTCLIENT_BUILD_TESTS": "ON" } @@ -110,7 +108,9 @@ "binaryDir": "${sourceDir}/build/${presetName}", "cacheVariables": { "ASAN_ENABLED": "ON", - "TOGGLE_PRE_COMPILED_HEADER": "OFF" + "TOGGLE_PRE_COMPILED_HEADER": "OFF", + "CMAKE_C_COMPILER": null, + "CMAKE_CXX_COMPILER": null } }, { @@ -122,7 +122,6 @@ "CMAKE_BUILD_TYPE": "RelWithDebInfo", "VCPKG_TARGET_TRIPLET": "x64-linux", "VCPKG_HOST_TRIPLET": "x64-linux", - "VCPKG_INSTALLED_DIR": "${sourceDir}/vcpkg_installed/linux-release", "VCPKG_BUILD_TYPE": "release", "OPTIONS_ENABLE_CCACHE": "ON", "OTCLIENT_BUILD_TESTS": "OFF" @@ -145,7 +144,6 @@ "ASAN_ENABLED": "OFF", "TOGGLE_PRE_COMPILED_HEADER": "OFF", "SPEED_UP_BUILD_UNITY": "OFF", - "VCPKG_INSTALLED_DIR": "${sourceDir}/vcpkg_installed/linux-debug", "VCPKG_BUILD_TYPE": "debug", "VCPKG_MANIFEST_FEATURES": "tests", "OTCLIENT_BUILD_TESTS": "ON" @@ -165,7 +163,6 @@ "CMAKE_C_FLAGS_RELEASE": "-O2 -DNDEBUG", "VCPKG_TARGET_TRIPLET": "arm64-osx", "VCPKG_HOST_TRIPLET": "arm64-osx", - "VCPKG_INSTALLED_DIR": "${sourceDir}/vcpkg_installed/macos-release", "VCPKG_BUILD_TYPE": "release", "OTCLIENT_BUILD_TESTS": "OFF" }, @@ -186,7 +183,6 @@ "ASAN_ENABLED": "ON", "BUILD_STATIC_LIBRARY": "OFF", "SPEED_UP_BUILD_UNITY": "OFF", - "VCPKG_INSTALLED_DIR": "${sourceDir}/vcpkg_installed/macos-debug", "VCPKG_BUILD_TYPE": "debug", "VCPKG_MANIFEST_FEATURES": "tests", "OTCLIENT_BUILD_TESTS": "ON" diff --git a/README.md b/README.md index 5274d1845..bc2c9a58f 100644 --- a/README.md +++ b/README.md @@ -514,6 +514,9 @@ This is a fork of edubart's OTClient. The objective of this fork is to develop a ## 🔨 Compiling If you are interested in compiling this project, visit the **[Wiki](https://github.com/mehah/otclient/wiki)**. +For content-addressed vcpkg reuse across CMake presets, Visual Studio Solutions, +worktrees, and compatible forks, see the [shared build cache guide](docs/development/shared-build-cache.md). + --- ## 🐳 Docker diff --git a/cmake/SharedBuildCache.cmake b/cmake/SharedBuildCache.cmake new file mode 100644 index 000000000..b1175f19f --- /dev/null +++ b/cmake/SharedBuildCache.cmake @@ -0,0 +1,3026 @@ +include_guard(GLOBAL) + +set(CANARY_SHARED_CACHE_SCHEMA + "v3" +) + +file( + READ + "${CMAKE_CURRENT_LIST_FILE}" + canary_shared_cache_implementation +) +string( + REPLACE "\r\n" + "\n" + canary_shared_cache_implementation + "${canary_shared_cache_implementation}" +) +string( + REPLACE "\r" + "\n" + canary_shared_cache_implementation + "${canary_shared_cache_implementation}" +) +string(SHA256 CANARY_SHARED_CACHE_IMPLEMENTATION_SHA256 + "${canary_shared_cache_implementation}" +) + +function( + canary_shared_cache_file_signature + label + path + output +) + if(NOT CMAKE_SCRIPT_MODE_FILE) + get_filename_component( + candidate_directory + "${path}" + DIRECTORY + ) + get_filename_component( + candidate_name + "${path}" + NAME + ) + if(IS_DIRECTORY "${candidate_directory}") + file( + GLOB + watched_candidate + CONFIGURE_DEPENDS + LIST_DIRECTORIES false + "${candidate_directory}/${candidate_name}" + ) + endif() + endif() + + if(EXISTS "${path}") + file( + SHA256 + "${path}" + file_hash + ) + if(NOT CMAKE_SCRIPT_MODE_FILE) + set_property( + DIRECTORY + APPEND + PROPERTY CMAKE_CONFIGURE_DEPENDS "${path}" + ) + endif() + set(signature + "${label}=${file_hash}\n" + ) + else() + set(signature + "${label}=\n" + ) + endif() + + set(${output} + "${signature}" + PARENT_SCOPE + ) +endfunction() + +function( + canary_shared_cache_tree_signature + label + root + output +) + if(NOT CMAKE_SCRIPT_MODE_FILE) + get_filename_component( + candidate_directory + "${root}" + DIRECTORY + ) + get_filename_component( + candidate_name + "${root}" + NAME + ) + if(IS_DIRECTORY "${candidate_directory}") + file( + GLOB + watched_root + CONFIGURE_DEPENDS + "${candidate_directory}/${candidate_name}" + ) + endif() + endif() + + if(NOT + IS_DIRECTORY + "${root}" + ) + set(${output} + "${label}=\n" + PARENT_SCOPE + ) + return() + endif() + + file( + GLOB_RECURSE tree_entries + LIST_DIRECTORIES true + "${root}/*" + ) + foreach( + tree_entry IN + LISTS tree_entries + ) + if(IS_SYMLINK "${tree_entry}") + message( + FATAL_ERROR + "Shared-cache input trees cannot contain symbolic links or reparse points: ${tree_entry}" + ) + endif() + if(WIN32 + AND IS_DIRECTORY "${tree_entry}" + ) + execute_process( + COMMAND + powershell -NoProfile -NonInteractive -Command + "& { param([string]$candidate) if ((Get-Item -LiteralPath $candidate -Force).Attributes -band [IO.FileAttributes]::ReparsePoint) { exit 42 } }" + "${tree_entry}" + RESULT_VARIABLE tree_entry_reparse_result + OUTPUT_QUIET ERROR_QUIET + ) + if(tree_entry_reparse_result + EQUAL + 42 + ) + message( + FATAL_ERROR + "Shared-cache input trees cannot contain symbolic links or reparse points: ${tree_entry}" + ) + elseif( + NOT + tree_entry_reparse_result + EQUAL + 0 + ) + message( + FATAL_ERROR + "Unable to verify a shared-cache input directory for reparse points: ${tree_entry}" + ) + endif() + endif() + endforeach() + + file( + REAL_PATH + "${root}" + resolved_tree_root + ) + get_filename_component( + lexical_tree_root + "${root}" + ABSOLUTE + ) + file( + TO_CMAKE_PATH + "${resolved_tree_root}" + resolved_tree_root + ) + file( + TO_CMAKE_PATH + "${lexical_tree_root}" + lexical_tree_root + ) + if(WIN32) + string(TOLOWER "${resolved_tree_root}" resolved_tree_root) + string(TOLOWER "${lexical_tree_root}" lexical_tree_root) + endif() + if(NOT + lexical_tree_root + STREQUAL + resolved_tree_root + ) + message( + FATAL_ERROR + "Shared-cache input trees cannot be symbolic links or reparse points: ${root}" + ) + endif() + + if(CMAKE_SCRIPT_MODE_FILE) + file( + GLOB_RECURSE tree_files + LIST_DIRECTORIES false + "${root}/*" + ) + else() + file( + GLOB_RECURSE + tree_files + CONFIGURE_DEPENDS + LIST_DIRECTORIES false + "${root}/*" + ) + endif() + + set(filtered_tree_files) + foreach( + tree_file IN + LISTS tree_files + ) + file( + REAL_PATH + "${tree_file}" + resolved_tree_file + ) + get_filename_component( + lexical_tree_file + "${tree_file}" + ABSOLUTE + ) + file( + TO_CMAKE_PATH + "${resolved_tree_file}" + resolved_tree_file + ) + file( + TO_CMAKE_PATH + "${lexical_tree_file}" + lexical_tree_file + ) + if(WIN32) + string(TOLOWER "${resolved_tree_file}" resolved_tree_file) + string(TOLOWER "${lexical_tree_file}" lexical_tree_file) + endif() + if(NOT + lexical_tree_file + STREQUAL + resolved_tree_file + ) + message( + FATAL_ERROR + "Shared-cache input trees cannot contain symbolic links or reparse points: ${tree_file}" + ) + endif() + file( + RELATIVE_PATH + relative_candidate + "${root}" + "${tree_file}" + ) + string( + REPLACE "\\" + "/" + relative_candidate + "${relative_candidate}" + ) + if(NOT + relative_candidate + MATCHES + "(^|/)\\.git(/|$)" + ) + list( + APPEND + filtered_tree_files + "${tree_file}" + ) + endif() + endforeach() + set(tree_files + ${filtered_tree_files} + ) + list(SORT tree_files) + set(signature + "${label}=\n" + ) + foreach( + tree_file IN + LISTS tree_files + ) + file( + RELATIVE_PATH + relative_path + "${root}" + "${tree_file}" + ) + string( + REPLACE "\\" + "/" + relative_path + "${relative_path}" + ) + file( + SHA256 + "${tree_file}" + file_hash + ) + string( + APPEND + signature + "${label}/${relative_path}=${file_hash}\n" + ) + endforeach() + + set(${output} + "${signature}" + PARENT_SCOPE + ) +endfunction() + +function( + canary_shared_cache_msvc_signature + root + output + found_output +) + if(CMAKE_SCRIPT_MODE_FILE) + file( + GLOB_RECURSE msvc_compilers + LIST_DIRECTORIES false + "${root}/VC/Tools/MSVC/*/bin/*/*/cl.exe" + ) + else() + file( + GLOB_RECURSE + msvc_compilers + CONFIGURE_DEPENDS + LIST_DIRECTORIES false + "${root}/VC/Tools/MSVC/*/bin/*/*/cl.exe" + ) + endif() + + list(SORT msvc_compilers) + set(signature + "vcpkg-msvc-compilers=\n" + ) + foreach( + msvc_compiler IN + LISTS msvc_compilers + ) + get_filename_component( + msvc_bin_directory + "${msvc_compiler}" + DIRECTORY + ) + foreach( + tool_name IN + ITEMS cl.exe + c1.dll + c1xx.dll + c2.dll + link.exe + lib.exe + ) + set(msvc_tool + "${msvc_bin_directory}/${tool_name}" + ) + if(EXISTS "${msvc_tool}") + file( + RELATIVE_PATH + relative_path + "${root}" + "${msvc_tool}" + ) + string( + REPLACE "\\" + "/" + relative_path + "${relative_path}" + ) + file( + SHA256 + "${msvc_tool}" + tool_hash + ) + string( + APPEND + signature + "vcpkg-msvc-tools/${relative_path}=${tool_hash}\n" + ) + endif() + endforeach() + endforeach() + + if(msvc_compilers) + set(found + true + ) + else() + set(found + false + ) + endif() + set(${output} + "${signature}" + PARENT_SCOPE + ) + set(${found_output} + "${found}" + PARENT_SCOPE + ) +endfunction() + +function( + canary_shared_cache_resolve_program + candidate + output +) + unset(resolved_program) + unset(resolved_program CACHE) + if(candidate) + if(IS_ABSOLUTE "${candidate}" + AND EXISTS "${candidate}" + ) + file( + REAL_PATH + "${candidate}" + resolved_program + ) + else() + find_program( + resolved_program + NAMES "${candidate}" NO_CACHE + ) + endif() + endif() + + set(${output} + "${resolved_program}" + PARENT_SCOPE + ) +endfunction() + +function( + canary_shared_cache_normalize_path + input + output +) + get_filename_component( + normalized_path + "${input}" + ABSOLUTE + BASE_DIR + "${CMAKE_BINARY_DIR}" + ) + if(EXISTS "${normalized_path}") + file( + REAL_PATH + "${normalized_path}" + normalized_path + ) + endif() + file( + TO_CMAKE_PATH + "${normalized_path}" + normalized_path + ) + string( + REGEX + REPLACE "/+$" + "" + normalized_path + "${normalized_path}" + ) + if(WIN32) + string(TOLOWER "${normalized_path}" normalized_path) + endif() + + set(${output} + "${normalized_path}" + PARENT_SCOPE + ) +endfunction() + +function( + canary_shared_cache_path_is_within + child + parent + output +) + canary_shared_cache_normalize_path("${child}" normalized_child) + canary_shared_cache_normalize_path("${parent}" normalized_parent) + string( + FIND "${normalized_child}/" + "${normalized_parent}/" + prefix_index + ) + if(prefix_index + EQUAL + 0 + ) + set(is_within + true + ) + else() + set(is_within + false + ) + endif() + + set(${output} + "${is_within}" + PARENT_SCOPE + ) +endfunction() + +function(canary_shared_cache_use_local_transient_roots replace_managed_roots) + set(local_vcpkg_install_options + ${VCPKG_INSTALL_OPTIONS} + ) + if(replace_managed_roots) + list( + FILTER + local_vcpkg_install_options + EXCLUDE + REGEX + "^--x-(buildtrees|packages)-root(=|$)" + ) + endif() + + set(has_buildtrees_root + false + ) + set(has_packages_root + false + ) + foreach( + install_option IN + LISTS local_vcpkg_install_options + ) + if(install_option + MATCHES + "^--x-(buildtrees|packages)-root(=|$)" + ) + if(NOT + install_option + MATCHES + "^--x-(buildtrees|packages)-root=(.+)$" + ) + message( + FATAL_ERROR + "Explicit vcpkg transient roots must use --x--root= so their isolation can be verified." + ) + endif() + get_filename_component( + local_explicit_transient_root + "${CMAKE_MATCH_2}" + ABSOLUTE + BASE_DIR + "${CMAKE_BINARY_DIR}" + ) + canary_shared_cache_path_is_within( + "${local_explicit_transient_root}" + "${CMAKE_BINARY_DIR}" + local_explicit_transient_root_is_local + ) + if(NOT local_explicit_transient_root_is_local) + message( + FATAL_ERROR + "An explicit vcpkg transient root is outside this configure tree and could collide with another opt-out build: ${local_explicit_transient_root}" + ) + endif() + endif() + if(install_option + MATCHES + "^--x-buildtrees-root(=|$)" + ) + set(has_buildtrees_root + true + ) + elseif( + install_option + MATCHES + "^--x-packages-root(=|$)" + ) + set(has_packages_root + true + ) + endif() + endforeach() + + if(NOT has_buildtrees_root) + list( + APPEND + local_vcpkg_install_options + "--x-buildtrees-root=${CMAKE_BINARY_DIR}/vcpkg-buildtrees" + ) + endif() + if(NOT has_packages_root) + list( + APPEND + local_vcpkg_install_options + "--x-packages-root=${CMAKE_BINARY_DIR}/vcpkg-packages" + ) + endif() + + set(VCPKG_INSTALL_OPTIONS + "${local_vcpkg_install_options}" + CACHE STRING + "Options passed to vcpkg manifest installation" + FORCE + ) +endfunction() + +function( + canary_shared_cache_base_install_options + output + has_explicit_transient_roots_output +) + set(base_install_options) + set(has_explicit_transient_roots + false + ) + foreach( + install_option IN + LISTS VCPKG_INSTALL_OPTIONS + ) + if(install_option + MATCHES + "^--x-buildtrees-root(=|$)" + ) + set(expected_managed_option + "--x-buildtrees-root=${CANARY_SHARED_VCPKG_BUILDTREES_ROOT}" + ) + elseif( + install_option + MATCHES + "^--x-packages-root(=|$)" + ) + set(expected_managed_option + "--x-packages-root=${CANARY_SHARED_VCPKG_PACKAGES_ROOT}" + ) + else() + list( + APPEND + base_install_options + "${install_option}" + ) + continue() + endif() + + if(CANARY_SHARED_VCPKG_MANAGED + AND install_option + STREQUAL + expected_managed_option + ) + continue() + endif() + + if(NOT + install_option + MATCHES + "^--x-(buildtrees|packages)-root=(.+)$" + ) + message( + FATAL_ERROR + "Explicit vcpkg transient roots must use --x--root= so their isolation can be verified." + ) + endif() + get_filename_component( + explicit_transient_root + "${CMAKE_MATCH_2}" + ABSOLUTE + BASE_DIR + "${CMAKE_BINARY_DIR}" + ) + canary_shared_cache_path_is_within( + "${explicit_transient_root}" + "${CMAKE_BINARY_DIR}" + explicit_transient_root_is_local + ) + if(NOT explicit_transient_root_is_local) + message( + FATAL_ERROR + "An explicit vcpkg transient root is outside this configure tree. Shared-cache fallback cannot prove that it is isolated: ${explicit_transient_root}" + ) + endif() + set(has_explicit_transient_roots + true + ) + endforeach() + + set(${output} + "${base_install_options}" + PARENT_SCOPE + ) + set(${has_explicit_transient_roots_output} + "${has_explicit_transient_roots}" + PARENT_SCOPE + ) +endfunction() + +function( + canary_shared_cache_append_environment_paths + environment_name + list_name +) + if(NOT + DEFINED + ENV{${environment_name}} + OR "$ENV{${environment_name}}" + STREQUAL + "" + ) + return() + endif() + + set(environment_append_failed + false + ) + if(WIN32) + set(environment_path_separator + ";" + ) + else() + set(environment_path_separator + ":" + ) + endif() + string( + REPLACE "${environment_path_separator}" + ";" + environment_paths + "$ENV{${environment_name}}" + ) + set(resolved_environment_paths) + foreach( + environment_path IN + LISTS environment_paths + ) + get_filename_component( + resolved_environment_path + "${environment_path}" + ABSOLUTE + BASE_DIR + "${CMAKE_SOURCE_DIR}" + ) + if(NOT + IS_DIRECTORY + "${resolved_environment_path}" + ) + canary_shared_cache_reset_managed_install( + "${environment_name} contains a missing directory" + ) + set(CANARY_SHARED_CACHE_ENVIRONMENT_APPEND_FAILED + true + PARENT_SCOPE + ) + return() + endif() + list( + APPEND + resolved_environment_paths + "${resolved_environment_path}" + ) + endforeach() + set(${list_name} + "${${list_name}};${resolved_environment_paths}" + PARENT_SCOPE + ) +endfunction() + +function(canary_shared_cache_native_triplet output) + string(TOLOWER "${CMAKE_HOST_SYSTEM_NAME}" host_system) + string(TOLOWER "${CMAKE_HOST_SYSTEM_PROCESSOR}" host_processor) + if(NOT host_processor + AND WIN32 + AND DEFINED ENV{PROCESSOR_ARCHITECTURE} + ) + string(TOLOWER "$ENV{PROCESSOR_ARCHITECTURE}" host_processor) + endif() + if(NOT host_processor + AND UNIX + ) + execute_process( + COMMAND uname -m + RESULT_VARIABLE host_processor_result + OUTPUT_VARIABLE host_processor + ERROR_QUIET OUTPUT_STRIP_TRAILING_WHITESPACE + ) + if(NOT + host_processor_result + EQUAL + 0 + ) + set(host_processor) + endif() + string(TOLOWER "${host_processor}" host_processor) + endif() + + if(host_processor + MATCHES + "^(amd64|x86_64|x64)$" + ) + set(triplet_architecture + "x64" + ) + elseif( + host_processor + MATCHES + "^(arm64|aarch64)$" + ) + set(triplet_architecture + "arm64" + ) + elseif( + host_processor + MATCHES + "^(x86|i[3-6]86)$" + ) + set(triplet_architecture + "x86" + ) + else() + set(triplet_architecture) + endif() + + if(host_system + STREQUAL + "windows" + ) + set(triplet_platform + "windows" + ) + elseif( + host_system + STREQUAL + "linux" + ) + set(triplet_platform + "linux" + ) + elseif( + host_system + STREQUAL + "darwin" + ) + set(triplet_platform + "osx" + ) + else() + set(triplet_platform) + endif() + + if(triplet_architecture + AND triplet_platform + ) + set(native_triplet + "${triplet_architecture}-${triplet_platform}" + ) + else() + set(native_triplet) + endif() + + set(${output} + "${native_triplet}" + PARENT_SCOPE + ) +endfunction() + +function(canary_shared_cache_reset_managed_install reason) + if(CANARY_SHARED_VCPKG_MANAGED + OR (CMAKE_PROJECT_NAME + AND CANARY_USE_SHARED_VCPKG_INSTALLED) + ) + message( + FATAL_ERROR + "The previously managed shared vcpkg contract is no longer usable (${reason}). Cached package paths cannot be migrated safely in place. Re-run cmake --fresh --preset ." + ) + endif() + + if(CANARY_USE_SHARED_VCPKG_INSTALLED) + set(VCPKG_INSTALLED_DIR + "${CMAKE_BINARY_DIR}/vcpkg_installed" + CACHE PATH + "vcpkg manifest installation directory" + FORCE + ) + canary_shared_cache_use_local_transient_roots(false) + set(CANARY_SHARED_VCPKG_MANAGED + false + CACHE INTERNAL + "Whether Canary owns VCPKG_INSTALLED_DIR" + FORCE + ) + endif() + + unset(CANARY_VCPKG_CACHE_FINGERPRINT CACHE) + unset(CANARY_VCPKG_DEPENDENCY_FINGERPRINT CACHE) + unset(CANARY_VCPKG_CONSUMER_FINGERPRINT CACHE) + unset(CANARY_SHARED_VCPKG_BUILDTREES_ROOT CACHE) + unset(CANARY_SHARED_VCPKG_PACKAGES_ROOT CACHE) + + set(CANARY_SHARED_VCPKG_ACTIVE + false + CACHE INTERNAL + "Whether the shared vcpkg installed tree is active" + FORCE + ) + if(reason) + message(STATUS "Shared vcpkg installed tree disabled: ${reason}") + endif() +endfunction() + +set(shared_cache_root_default) +if(DEFINED ENV{CANARY_SHARED_CACHE_ROOT} + AND NOT + "$ENV{CANARY_SHARED_CACHE_ROOT}" + STREQUAL + "" +) + set(shared_cache_root_default + "$ENV{CANARY_SHARED_CACHE_ROOT}" + ) +endif() + +set(CANARY_SHARED_CACHE_ROOT + "${shared_cache_root_default}" + CACHE PATH "Global cache root shared by compatible Canary worktrees" +) + +set(shared_cache_local_filesystem_verified_default + false +) +if(DEFINED ENV{CANARY_SHARED_CACHE_LOCAL_FILESYSTEM_VERIFIED} + AND "$ENV{CANARY_SHARED_CACHE_LOCAL_FILESYSTEM_VERIFIED}" + MATCHES + "^(1|ON|TRUE|YES)$" +) + set(shared_cache_local_filesystem_verified_default + true + ) +endif() +set(CANARY_SHARED_CACHE_LOCAL_FILESYSTEM_VERIFIED + ${shared_cache_local_filesystem_verified_default} + CACHE + BOOL + "Confirm that the shared pool is on a local filesystem with reliable locks" +) +set(shared_cache_verified_root_default) +if(DEFINED ENV{CANARY_SHARED_CACHE_VERIFIED_ROOT} + AND NOT + "$ENV{CANARY_SHARED_CACHE_VERIFIED_ROOT}" + STREQUAL + "" +) + set(shared_cache_verified_root_default + "$ENV{CANARY_SHARED_CACHE_VERIFIED_ROOT}" + ) +endif() +set(CANARY_SHARED_CACHE_VERIFIED_ROOT + "${shared_cache_verified_root_default}" + CACHE PATH + "Exact shared pool root covered by the local-filesystem verification" +) + +set(use_shared_install_default + false +) +if(CANARY_SHARED_CACHE_ROOT) + set(use_shared_install_default + true + ) +endif() +option( + CANARY_USE_SHARED_VCPKG_INSTALLED + "Share compatible vcpkg manifest installations across worktrees" + ${use_shared_install_default} +) +option( + CANARY_SHARED_CACHE_READ_ONLY + "Compute and report the shared-cache fingerprint without writing the pool" + false +) +option( + CANARY_SHARED_CACHE_PREPARE_ONLY + "Prepare a shared-cache contract without configuring a CMake project" + false +) + +if(CANARY_SHARED_CACHE_READ_ONLY + AND NOT CMAKE_SCRIPT_MODE_FILE +) + message( + FATAL_ERROR + "CANARY_SHARED_CACHE_READ_ONLY is available only in CMake script mode; it cannot safely continue into project()" + ) +endif() +if(CANARY_SHARED_CACHE_PREPARE_ONLY + AND NOT CMAKE_SCRIPT_MODE_FILE +) + message( + FATAL_ERROR + "CANARY_SHARED_CACHE_PREPARE_ONLY is available only in CMake script mode" + ) +endif() +if(CANARY_SHARED_CACHE_READ_ONLY + AND CANARY_SHARED_CACHE_PREPARE_ONLY +) + message( + FATAL_ERROR + "CANARY_SHARED_CACHE_READ_ONLY and CANARY_SHARED_CACHE_PREPARE_ONLY are mutually exclusive" + ) +endif() + +if(DEFINED VCPKG_MANIFEST_INSTALL + AND NOT VCPKG_MANIFEST_INSTALL +) + if(CANARY_SHARED_VCPKG_MANAGED + OR CANARY_SHARED_VCPKG_ACTIVE + ) + message( + FATAL_ERROR + "The previously managed shared vcpkg contract cannot switch to a preprovisioned installation in place. Re-run cmake --fresh --preset ." + ) + endif() + + set(CANARY_SHARED_VCPKG_MANAGED + false + CACHE INTERNAL + "Whether Canary owns VCPKG_INSTALLED_DIR" + FORCE + ) + set(CANARY_SHARED_VCPKG_ACTIVE + false + CACHE INTERNAL + "Whether the shared vcpkg installed tree is active" + FORCE + ) + set(CANARY_SHARED_VCPKG_PREPROVISIONED + true + CACHE + INTERNAL + "Whether this configure consumes a preprovisioned vcpkg installation" + FORCE + ) + unset(CANARY_VCPKG_CACHE_FINGERPRINT CACHE) + unset(CANARY_VCPKG_DEPENDENCY_FINGERPRINT CACHE) + unset(CANARY_VCPKG_CONSUMER_FINGERPRINT CACHE) + unset(CANARY_SHARED_VCPKG_BUILDTREES_ROOT CACHE) + unset(CANARY_SHARED_VCPKG_PACKAGES_ROOT CACHE) + message( + STATUS + "Shared vcpkg installed tree disabled: VCPKG_MANIFEST_INSTALL is disabled; preserving the preprovisioned VCPKG_INSTALLED_DIR" + ) + return() +endif() + +if(CANARY_SHARED_VCPKG_PREPROVISIONED) + message( + FATAL_ERROR + "The previously preprovisioned vcpkg contract cannot enable manifest installation in place. Re-run cmake --fresh --preset ." + ) +endif() + +if(NOT CANARY_USE_SHARED_VCPKG_INSTALLED) + if(DEFINED VCPKG_INSTALLED_DIR + AND NOT + VCPKG_INSTALLED_DIR + STREQUAL + "" + ) + get_filename_component( + opt_out_installed_root + "${VCPKG_INSTALLED_DIR}" + ABSOLUTE + BASE_DIR + "${CMAKE_BINARY_DIR}" + ) + canary_shared_cache_path_is_within( + "${opt_out_installed_root}" + "${CMAKE_BINARY_DIR}" + opt_out_installed_root_is_local + ) + if(NOT opt_out_installed_root_is_local) + message( + FATAL_ERROR + "Opt-out VCPKG_INSTALLED_DIR must remain inside this configure tree: ${opt_out_installed_root}" + ) + endif() + endif() + if(NOT CANARY_SHARED_VCPKG_MANAGED) + canary_shared_cache_use_local_transient_roots(false) + endif() + canary_shared_cache_reset_managed_install("explicitly disabled") + return() +endif() + +canary_shared_cache_base_install_options(canary_base_vcpkg_install_options + canary_has_explicit_transient_roots +) +if(canary_has_explicit_transient_roots) + canary_shared_cache_reset_managed_install( + "explicit build-local vcpkg transient roots keep this configure isolated" + ) + return() +endif() +set(VCPKG_INSTALL_OPTIONS + "${canary_base_vcpkg_install_options}" + CACHE STRING + "Options passed to vcpkg manifest installation" + FORCE +) + +if(NOT CANARY_SHARED_CACHE_ROOT) + canary_shared_cache_reset_managed_install( + "CANARY_SHARED_CACHE_ROOT is not configured" + ) + return() +endif() + +get_filename_component( + shared_cache_root + "${CANARY_SHARED_CACHE_ROOT}" + ABSOLUTE + BASE_DIR + "${CMAKE_BINARY_DIR}" +) +file( + TO_CMAKE_PATH + "${shared_cache_root}" + shared_cache_root +) + +canary_shared_cache_path_is_within( + "${shared_cache_root}" + "${CMAKE_SOURCE_DIR}" + cache_inside_source +) +canary_shared_cache_path_is_within( + "${CMAKE_SOURCE_DIR}" + "${shared_cache_root}" + source_inside_cache +) +canary_shared_cache_path_is_within( + "${shared_cache_root}" + "${CMAKE_BINARY_DIR}" + cache_inside_binary +) +canary_shared_cache_path_is_within( + "${CMAKE_BINARY_DIR}" + "${shared_cache_root}" + binary_inside_cache +) +if(cache_inside_source + OR source_inside_cache + OR cache_inside_binary + OR binary_inside_cache +) + message( + FATAL_ERROR + "CANARY_SHARED_CACHE_ROOT must be outside and separate from the source and binary directory hierarchies" + ) +endif() + +if(NOT CANARY_SHARED_CACHE_LOCAL_FILESYSTEM_VERIFIED) + canary_shared_cache_reset_managed_install( + "the shared cache filesystem has not been verified as local" + ) + return() +endif() +if(NOT CANARY_SHARED_CACHE_VERIFIED_ROOT) + canary_shared_cache_reset_managed_install( + "the local-filesystem verification is not bound to an exact cache root" + ) + return() +endif() +get_filename_component( + verified_shared_cache_root + "${CANARY_SHARED_CACHE_VERIFIED_ROOT}" + ABSOLUTE + BASE_DIR + "${CMAKE_BINARY_DIR}" +) +file( + TO_CMAKE_PATH + "${verified_shared_cache_root}" + verified_shared_cache_root +) +set(normalized_shared_cache_root + "${shared_cache_root}" +) +set(normalized_verified_shared_cache_root + "${verified_shared_cache_root}" +) +string( + REGEX + REPLACE "/+$" + "" + normalized_shared_cache_root + "${normalized_shared_cache_root}" +) +string( + REGEX + REPLACE "/+$" + "" + normalized_verified_shared_cache_root + "${normalized_verified_shared_cache_root}" +) +if(WIN32) + string(TOLOWER "${normalized_shared_cache_root}" + normalized_shared_cache_root + ) + string(TOLOWER "${normalized_verified_shared_cache_root}" + normalized_verified_shared_cache_root + ) +endif() +if(NOT + normalized_shared_cache_root + STREQUAL + normalized_verified_shared_cache_root +) + canary_shared_cache_reset_managed_install( + "the cache root differs from the path covered by local-filesystem verification" + ) + return() +endif() +if(EXISTS "${shared_cache_root}") + file( + REAL_PATH + "${shared_cache_root}" + resolved_shared_cache_root + ) + file( + TO_CMAKE_PATH + "${resolved_shared_cache_root}" + normalized_resolved_shared_cache_root + ) + string( + REGEX + REPLACE "/+$" + "" + normalized_resolved_shared_cache_root + "${normalized_resolved_shared_cache_root}" + ) + if(WIN32) + string(TOLOWER "${normalized_resolved_shared_cache_root}" + normalized_resolved_shared_cache_root + ) + endif() + if(NOT + normalized_shared_cache_root + STREQUAL + normalized_resolved_shared_cache_root + ) + canary_shared_cache_reset_managed_install( + "the cache root traverses a symbolic link or reparse point" + ) + return() + endif() +endif() + +set(cxx_compiler_candidate) +if(CMAKE_CXX_COMPILER) + set(cxx_compiler_candidate + "${CMAKE_CXX_COMPILER}" + ) +elseif( + DEFINED ENV{CXX} + AND NOT + "$ENV{CXX}" + STREQUAL + "" +) + set(cxx_compiler_candidate + "$ENV{CXX}" + ) +endif() + +set(c_compiler_candidate) +if(CMAKE_C_COMPILER) + set(c_compiler_candidate + "${CMAKE_C_COMPILER}" + ) +elseif( + DEFINED ENV{CC} + AND NOT + "$ENV{CC}" + STREQUAL + "" +) + set(c_compiler_candidate + "$ENV{CC}" + ) +endif() + +canary_shared_cache_resolve_program("${cxx_compiler_candidate}" + cxx_compiler_path +) +canary_shared_cache_resolve_program("${c_compiler_candidate}" c_compiler_path) + +if(NOT cxx_compiler_path + OR NOT c_compiler_path +) + canary_shared_cache_reset_managed_install( + "the C and C++ compilers cannot both be identified before project(); use a compiler environment or set CMAKE_C_COMPILER and CMAKE_CXX_COMPILER" + ) + return() +endif() + +file( + REAL_PATH + "${cxx_compiler_path}" + cxx_compiler_path +) +file( + SHA256 + "${cxx_compiler_path}" + cxx_compiler_hash +) +get_filename_component( + cxx_compiler_name + "${cxx_compiler_path}" + NAME +) +file( + REAL_PATH + "${c_compiler_path}" + c_compiler_path +) +file( + SHA256 + "${c_compiler_path}" + c_compiler_hash +) +get_filename_component( + c_compiler_name + "${c_compiler_path}" + NAME +) + +file( + REAL_PATH + "${CMAKE_COMMAND}" + cmake_executable +) +file( + SHA256 + "${cmake_executable}" + cmake_executable_hash +) + +set(vcpkg_root_from_toolchain) +if(CMAKE_TOOLCHAIN_FILE) + get_filename_component( + selected_toolchain + "${CMAKE_TOOLCHAIN_FILE}" + ABSOLUTE + BASE_DIR + "${CMAKE_SOURCE_DIR}" + ) + file( + TO_CMAKE_PATH + "${selected_toolchain}" + selected_toolchain + ) +endif() +if(selected_toolchain + MATCHES + "/scripts/buildsystems/vcpkg\\.cmake$" +) + get_filename_component( + vcpkg_toolchain_directory + "${selected_toolchain}" + DIRECTORY + ) + get_filename_component( + vcpkg_root_from_toolchain + "${vcpkg_toolchain_directory}/../.." + ABSOLUTE + ) +elseif(selected_toolchain) + canary_shared_cache_reset_managed_install( + "CMAKE_TOOLCHAIN_FILE is not the direct vcpkg toolchain" + ) + return() +endif() + +set(vcpkg_root_from_environment) +if(DEFINED ENV{VCPKG_ROOT} + AND NOT + "$ENV{VCPKG_ROOT}" + STREQUAL + "" +) + get_filename_component( + vcpkg_root_from_environment + "$ENV{VCPKG_ROOT}" + ABSOLUTE + ) +endif() + +if(vcpkg_root_from_toolchain + AND vcpkg_root_from_environment +) + file( + REAL_PATH + "${vcpkg_root_from_toolchain}" + toolchain_root_real + ) + file( + REAL_PATH + "${vcpkg_root_from_environment}" + environment_root_real + ) + file( + TO_CMAKE_PATH + "${toolchain_root_real}" + toolchain_root_real + ) + file( + TO_CMAKE_PATH + "${environment_root_real}" + environment_root_real + ) + if(WIN32) + string(TOLOWER "${toolchain_root_real}" toolchain_root_comparable) + string(TOLOWER "${environment_root_real}" environment_root_comparable) + else() + set(toolchain_root_comparable + "${toolchain_root_real}" + ) + set(environment_root_comparable + "${environment_root_real}" + ) + endif() + if(NOT + toolchain_root_comparable + STREQUAL + environment_root_comparable + ) + canary_shared_cache_reset_managed_install( + "CMAKE_TOOLCHAIN_FILE and VCPKG_ROOT select different vcpkg installations" + ) + return() + endif() +endif() + +if(vcpkg_root_from_toolchain) + set(vcpkg_root + "${vcpkg_root_from_toolchain}" + ) +else() + set(vcpkg_root + "${vcpkg_root_from_environment}" + ) +endif() + +if(NOT + IS_DIRECTORY + "${vcpkg_root}" +) + canary_shared_cache_reset_managed_install( + "the vcpkg installation cannot be identified" + ) + return() +endif() + +if(WIN32) + set(vcpkg_executable + "${vcpkg_root}/vcpkg.exe" + ) +else() + set(vcpkg_executable + "${vcpkg_root}/vcpkg" + ) +endif() +set(vcpkg_toolchain + "${vcpkg_root}/scripts/buildsystems/vcpkg.cmake" +) + +if(NOT + EXISTS + "${vcpkg_executable}" + OR NOT + EXISTS + "${vcpkg_toolchain}" +) + canary_shared_cache_reset_managed_install( + "the vcpkg executable or toolchain is missing" + ) + return() +endif() + +file( + SHA256 + "${vcpkg_executable}" + vcpkg_executable_hash +) +file( + SHA256 + "${vcpkg_toolchain}" + vcpkg_toolchain_hash +) +if(NOT CMAKE_SCRIPT_MODE_FILE) + set_property( + DIRECTORY + APPEND + PROPERTY CMAKE_CONFIGURE_DEPENDS + "${CMAKE_CURRENT_LIST_FILE}" + "${cxx_compiler_path}" + "${c_compiler_path}" + "${cmake_executable}" + "${vcpkg_executable}" + "${vcpkg_toolchain}" + ) +endif() +execute_process( + COMMAND git -C "${vcpkg_root}" rev-parse HEAD + OUTPUT_VARIABLE vcpkg_revision + RESULT_VARIABLE vcpkg_revision_result + ERROR_QUIET OUTPUT_STRIP_TRAILING_WHITESPACE +) +execute_process( + COMMAND + git -C "${vcpkg_root}" status --porcelain --untracked-files=all -- ports + versions triplets scripts + OUTPUT_VARIABLE vcpkg_dirty_state + RESULT_VARIABLE vcpkg_dirty_result + ERROR_QUIET OUTPUT_STRIP_TRAILING_WHITESPACE +) +if(NOT + vcpkg_revision_result + EQUAL + 0 + OR NOT + vcpkg_dirty_result + EQUAL + 0 +) + canary_shared_cache_reset_managed_install( + "the vcpkg Git revision cannot be verified" + ) + return() +endif() +if(vcpkg_dirty_state) + canary_shared_cache_reset_managed_install( + "the vcpkg ports, versions, triplets, or scripts tree has local changes" + ) + return() +endif() + +canary_shared_cache_native_triplet(native_triplet) +if(NOT VCPKG_TARGET_TRIPLET + AND DEFINED ENV{VCPKG_DEFAULT_TRIPLET} + AND NOT + "$ENV{VCPKG_DEFAULT_TRIPLET}" + STREQUAL + "" +) + set(VCPKG_TARGET_TRIPLET + "$ENV{VCPKG_DEFAULT_TRIPLET}" + ) +endif() +if(NOT VCPKG_HOST_TRIPLET + AND DEFINED ENV{VCPKG_DEFAULT_HOST_TRIPLET} + AND NOT + "$ENV{VCPKG_DEFAULT_HOST_TRIPLET}" + STREQUAL + "" +) + set(VCPKG_HOST_TRIPLET + "$ENV{VCPKG_DEFAULT_HOST_TRIPLET}" + ) +endif() +if(NOT VCPKG_TARGET_TRIPLET) + set(VCPKG_TARGET_TRIPLET + "${native_triplet}" + ) +endif() +if(NOT VCPKG_HOST_TRIPLET) + set(VCPKG_HOST_TRIPLET + "${native_triplet}" + ) +endif() +if(NOT VCPKG_TARGET_TRIPLET + OR NOT VCPKG_HOST_TRIPLET +) + canary_shared_cache_reset_managed_install( + "the effective target or host triplet cannot be resolved safely" + ) + return() +endif() +set(VCPKG_TARGET_TRIPLET + "${VCPKG_TARGET_TRIPLET}" + CACHE STRING + "vcpkg target triplet" + FORCE +) +set(VCPKG_HOST_TRIPLET + "${VCPKG_HOST_TRIPLET}" + CACHE STRING + "vcpkg host triplet" + FORCE +) + +set(manifest_root + "${CMAKE_SOURCE_DIR}" +) +if(VCPKG_MANIFEST_DIR) + get_filename_component( + manifest_root + "${VCPKG_MANIFEST_DIR}" + ABSOLUTE + BASE_DIR + "${CMAKE_SOURCE_DIR}" + ) +endif() + +if(DEFINED VCPKG_MANIFEST_MODE + AND NOT VCPKG_MANIFEST_MODE +) + canary_shared_cache_reset_managed_install("VCPKG_MANIFEST_MODE is disabled") + return() +endif() +if(NOT + EXISTS + "${manifest_root}/vcpkg.json" +) + canary_shared_cache_reset_managed_install( + "no vcpkg manifest exists at the selected manifest root" + ) + return() +endif() +if(DEFINED VCPKG_MANIFEST_INSTALL + AND NOT VCPKG_MANIFEST_INSTALL +) + canary_shared_cache_reset_managed_install( + "VCPKG_MANIFEST_INSTALL is disabled" + ) + return() +endif() +set(VCPKG_MANIFEST_MODE + ON + CACHE BOOL + "Use vcpkg manifest mode" + FORCE +) +set(VCPKG_MANIFEST_INSTALL + ON + CACHE BOOL + "Install vcpkg manifest dependencies" + FORCE +) + +set(fingerprint_input + "schema=${CANARY_SHARED_CACHE_SCHEMA}\n" +) +string( + APPEND + fingerprint_input + "implementation-sha256=${CANARY_SHARED_CACHE_IMPLEMENTATION_SHA256}\n" + "host-system=${CMAKE_HOST_SYSTEM_NAME}\n" + "host-processor=${CMAKE_HOST_SYSTEM_PROCESSOR}\n" + "dependency-cmake-version=${CMAKE_VERSION}\n" + "dependency-cmake-sha256=${cmake_executable_hash}\n" + "cxx-compiler-name=${cxx_compiler_name}\n" + "cxx-compiler-sha256=${cxx_compiler_hash}\n" + "c-compiler-name=${c_compiler_name}\n" + "c-compiler-sha256=${c_compiler_hash}\n" + "vcpkg-revision=${vcpkg_revision}\n" + "vcpkg-executable-sha256=${vcpkg_executable_hash}\n" + "vcpkg-toolchain-sha256=${vcpkg_toolchain_hash}\n" +) + +foreach( + setting IN + ITEMS VCPKG_TARGET_TRIPLET + VCPKG_HOST_TRIPLET + VCPKG_MANIFEST_FEATURES + VCPKG_MANIFEST_NO_DEFAULT_FEATURES + VCPKG_MANIFEST_MODE + VCPKG_MANIFEST_INSTALL + VCPKG_FEATURE_FLAGS + VCPKG_BUILD_TYPE + VCPKG_INSTALL_OPTIONS + VCPKG_CMAKE_SYSTEM_NAME + VCPKG_CMAKE_SYSTEM_VERSION + VCPKG_CRT_LINKAGE + VCPKG_LIBRARY_LINKAGE + VCPKG_PLATFORM_TOOLSET + VCPKG_PLATFORM_TOOLSET_VERSION + VCPKG_LOAD_VCVARS_ENV +) + if(DEFINED ${setting}) + set(setting_value + "${${setting}}" + ) + string( + REPLACE ";" + "|" + setting_value + "${setting_value}" + ) + else() + set(setting_value + "" + ) + endif() + string( + APPEND + fingerprint_input + "${setting}=${setting_value}\n" + ) +endforeach() + +foreach( + environment_setting IN + ITEMS VisualStudioVersion + VCToolsVersion + WindowsSDKVersion + WindowsSDKLibVersion + UCRTVersion + VSCMD_ARG_HOST_ARCH + VSCMD_ARG_TGT_ARCH +) + if(DEFINED ENV{${environment_setting}} + AND NOT + "$ENV{${environment_setting}}" + STREQUAL + "" + ) + set(environment_value + "$ENV{${environment_setting}}" + ) + string( + REPLACE "\\" + "/" + environment_value + "${environment_value}" + ) + else() + set(environment_value + "" + ) + endif() + string( + APPEND + fingerprint_input + "env/${environment_setting}=${environment_value}\n" + ) +endforeach() + +canary_shared_cache_file_signature( + "manifest/vcpkg.json" + "${manifest_root}/vcpkg.json" + manifest_signature +) +string( + APPEND + fingerprint_input + "${manifest_signature}" +) +canary_shared_cache_file_signature( + "manifest/vcpkg-configuration.json" + "${manifest_root}/vcpkg-configuration.json" + manifest_configuration_signature +) +string( + APPEND + fingerprint_input + "${manifest_configuration_signature}" +) + +set(install_option_overlay_ports) +set(install_option_overlay_triplets) +foreach( + install_option IN + LISTS canary_base_vcpkg_install_options +) + if(install_option + MATCHES + "^--overlay-(ports|triplets)=(.+)$" + ) + set(install_overlay_kind + "${CMAKE_MATCH_1}" + ) + get_filename_component( + install_overlay_root + "${CMAKE_MATCH_2}" + ABSOLUTE + BASE_DIR + "${manifest_root}" + ) + if(NOT + IS_DIRECTORY + "${install_overlay_root}" + ) + canary_shared_cache_reset_managed_install( + "VCPKG_INSTALL_OPTIONS references a missing overlay directory" + ) + return() + endif() + if(install_overlay_kind + STREQUAL + "ports" + ) + list( + APPEND + install_option_overlay_ports + "${install_overlay_root}" + ) + else() + list( + APPEND + install_option_overlay_triplets + "${install_overlay_root}" + ) + endif() + elseif( + install_option + MATCHES + "^--overlay-(ports|triplets)($|[[:space:]])" + ) + message( + FATAL_ERROR + "Overlay install options must use --overlay-= so the tree can be fingerprinted." + ) + endif() +endforeach() + +set(configuration_overlay_ports) +set(configuration_overlay_triplets) +set(variable_overlay_ports + ${VCPKG_OVERLAY_PORTS} +) +set(variable_overlay_triplets + ${VCPKG_OVERLAY_TRIPLETS} +) +set(environment_overlay_ports) +set(environment_overlay_triplets) +set(CANARY_SHARED_CACHE_ENVIRONMENT_APPEND_FAILED + false +) +canary_shared_cache_append_environment_paths(VCPKG_OVERLAY_PORTS + environment_overlay_ports +) +if(CANARY_SHARED_CACHE_ENVIRONMENT_APPEND_FAILED) + return() +endif() +canary_shared_cache_append_environment_paths(VCPKG_OVERLAY_TRIPLETS + environment_overlay_triplets +) +if(CANARY_SHARED_CACHE_ENVIRONMENT_APPEND_FAILED) + return() +endif() +set(vcpkg_configuration_path + "${manifest_root}/vcpkg-configuration.json" +) +file( + READ + "${manifest_root}/vcpkg.json" + vcpkg_manifest_json +) +set(has_embedded_vcpkg_configuration + false +) +foreach( + embedded_configuration_field IN + ITEMS configuration vcpkg-configuration +) + string( + JSON + embedded_configuration_type + ERROR_VARIABLE + embedded_configuration_error + TYPE + "${vcpkg_manifest_json}" + "${embedded_configuration_field}" + ) + if(embedded_configuration_error + STREQUAL + "NOTFOUND" + ) + if(NOT + embedded_configuration_type + STREQUAL + "OBJECT" + ) + canary_shared_cache_reset_managed_install( + "vcpkg.json contains an invalid ${embedded_configuration_field} value" + ) + return() + endif() + if(has_embedded_vcpkg_configuration) + canary_shared_cache_reset_managed_install( + "vcpkg.json defines both embedded configuration spellings" + ) + return() + endif() + string( + JSON + vcpkg_configuration_json + GET + "${vcpkg_manifest_json}" + "${embedded_configuration_field}" + ) + set(has_embedded_vcpkg_configuration + true + ) + set(vcpkg_configuration_source + "vcpkg.json ${embedded_configuration_field}" + ) + elseif( + NOT + embedded_configuration_error + MATCHES + "member '.*' not found" + ) + canary_shared_cache_reset_managed_install( + "vcpkg.json is not valid JSON" + ) + return() + endif() +endforeach() + +if(EXISTS "${vcpkg_configuration_path}") + if(has_embedded_vcpkg_configuration) + canary_shared_cache_reset_managed_install( + "vcpkg rejects a separate vcpkg-configuration.json combined with embedded configuration" + ) + return() + endif() + file( + READ + "${vcpkg_configuration_path}" + vcpkg_configuration_json + ) + set(vcpkg_configuration_source + "vcpkg-configuration.json" + ) + set(has_vcpkg_configuration + true + ) +elseif(has_embedded_vcpkg_configuration) + set(has_vcpkg_configuration + true + ) +else() + set(has_vcpkg_configuration + false + ) +endif() + +if(has_vcpkg_configuration) + string( + JSON + configuration_root_type + ERROR_VARIABLE + configuration_root_error + TYPE + "${vcpkg_configuration_json}" + ) + if(NOT + configuration_root_error + STREQUAL + "NOTFOUND" + OR NOT + configuration_root_type + STREQUAL + "OBJECT" + ) + canary_shared_cache_reset_managed_install( + "${vcpkg_configuration_source} is not a valid JSON object" + ) + return() + endif() + + foreach( + configuration_overlay_kind IN + ITEMS overlay-ports overlay-triplets + ) + string( + JSON + configuration_overlay_type + ERROR_VARIABLE + configuration_overlay_error + TYPE + "${vcpkg_configuration_json}" + "${configuration_overlay_kind}" + ) + if(configuration_overlay_error + STREQUAL + "NOTFOUND" + AND configuration_overlay_type + STREQUAL + "ARRAY" + ) + string( + JSON configuration_overlay_count + LENGTH "${vcpkg_configuration_json}" + "${configuration_overlay_kind}" + ) + if(configuration_overlay_count + GREATER + 0 + ) + math( + EXPR + configuration_overlay_last + "${configuration_overlay_count} - 1" + ) + foreach( + configuration_overlay_index + RANGE 0 ${configuration_overlay_last} + ) + string( + JSON + configuration_overlay_relative_path + ERROR_VARIABLE + configuration_overlay_path_error + GET + "${vcpkg_configuration_json}" + "${configuration_overlay_kind}" + ${configuration_overlay_index} + ) + if(NOT + configuration_overlay_path_error + STREQUAL + "NOTFOUND" + ) + canary_shared_cache_reset_managed_install( + "vcpkg-configuration.json contains an invalid ${configuration_overlay_kind} entry" + ) + return() + endif() + get_filename_component( + configuration_overlay_root + "${configuration_overlay_relative_path}" + ABSOLUTE + BASE_DIR + "${manifest_root}" + ) + if(NOT + IS_DIRECTORY + "${configuration_overlay_root}" + ) + canary_shared_cache_reset_managed_install( + "a configured ${configuration_overlay_kind} directory is missing" + ) + return() + endif() + canary_shared_cache_tree_signature( + "configuration-${configuration_overlay_kind}-${configuration_overlay_index}" + "${configuration_overlay_root}" + configuration_overlay_signature + ) + string( + APPEND + fingerprint_input + "${configuration_overlay_signature}" + ) + if(configuration_overlay_kind + STREQUAL + "overlay-ports" + ) + list( + APPEND + configuration_overlay_ports + "${configuration_overlay_root}" + ) + else() + list( + APPEND + configuration_overlay_triplets + "${configuration_overlay_root}" + ) + endif() + endforeach() + endif() + elseif( + NOT + configuration_overlay_error + STREQUAL + "member '${configuration_overlay_kind}' not found" + AND NOT + configuration_overlay_error + MATCHES + "member '.*' not found" + ) + canary_shared_cache_reset_managed_install( + "vcpkg-configuration.json contains an invalid ${configuration_overlay_kind} value" + ) + return() + endif() + endforeach() + + set(filesystem_registry_index + 0 + ) + string( + JSON + default_registry_type + ERROR_VARIABLE + default_registry_error + TYPE + "${vcpkg_configuration_json}" + "default-registry" + ) + if(default_registry_error + STREQUAL + "NOTFOUND" + AND default_registry_type + STREQUAL + "OBJECT" + ) + string( + JSON + default_registry_kind + ERROR_VARIABLE + default_registry_kind_error + GET + "${vcpkg_configuration_json}" + "default-registry" + "kind" + ) + if(default_registry_kind_error + STREQUAL + "NOTFOUND" + AND default_registry_kind + STREQUAL + "filesystem" + ) + string( + JSON + filesystem_registry_relative_path + ERROR_VARIABLE + filesystem_registry_path_error + GET + "${vcpkg_configuration_json}" + "default-registry" + "path" + ) + if(NOT + filesystem_registry_path_error + STREQUAL + "NOTFOUND" + ) + canary_shared_cache_reset_managed_install( + "the default filesystem registry does not define a valid path" + ) + return() + endif() + get_filename_component( + filesystem_registry_root + "${filesystem_registry_relative_path}" + ABSOLUTE + BASE_DIR + "${manifest_root}" + ) + if(NOT + IS_DIRECTORY + "${filesystem_registry_root}" + ) + canary_shared_cache_reset_managed_install( + "the default filesystem registry directory is missing" + ) + return() + endif() + canary_shared_cache_tree_signature( + "filesystem-registry-default" + "${filesystem_registry_root}" + filesystem_registry_signature + ) + string( + APPEND + fingerprint_input + "${filesystem_registry_signature}" + ) + endif() + endif() + + string( + JSON + registries_type + ERROR_VARIABLE + registries_error + TYPE + "${vcpkg_configuration_json}" + "registries" + ) + if(registries_error + STREQUAL + "NOTFOUND" + AND registries_type + STREQUAL + "ARRAY" + ) + string( + JSON registry_count + LENGTH "${vcpkg_configuration_json}" "registries" + ) + if(registry_count + GREATER + 0 + ) + math( + EXPR + registry_last + "${registry_count} - 1" + ) + foreach( + registry_index + RANGE 0 ${registry_last} + ) + string( + JSON + registry_kind + ERROR_VARIABLE + registry_kind_error + GET + "${vcpkg_configuration_json}" + "registries" + ${registry_index} + "kind" + ) + if(registry_kind_error + STREQUAL + "NOTFOUND" + AND registry_kind + STREQUAL + "filesystem" + ) + string( + JSON + filesystem_registry_relative_path + ERROR_VARIABLE + filesystem_registry_path_error + GET + "${vcpkg_configuration_json}" + "registries" + ${registry_index} + "path" + ) + if(NOT + filesystem_registry_path_error + STREQUAL + "NOTFOUND" + ) + canary_shared_cache_reset_managed_install( + "a filesystem registry does not define a valid path" + ) + return() + endif() + get_filename_component( + filesystem_registry_root + "${filesystem_registry_relative_path}" + ABSOLUTE + BASE_DIR + "${manifest_root}" + ) + if(NOT + IS_DIRECTORY + "${filesystem_registry_root}" + ) + canary_shared_cache_reset_managed_install( + "a filesystem registry directory is missing" + ) + return() + endif() + math( + EXPR + filesystem_registry_index + "${filesystem_registry_index} + 1" + ) + canary_shared_cache_tree_signature( + "filesystem-registry-${filesystem_registry_index}" + "${filesystem_registry_root}" + filesystem_registry_signature + ) + string( + APPEND + fingerprint_input + "${filesystem_registry_signature}" + ) + endif() + endforeach() + endif() + endif() +endif() + +set(effective_overlay_ports + ${variable_overlay_ports} + ${install_option_overlay_ports} + ${configuration_overlay_ports} + ${environment_overlay_ports} +) +set(effective_overlay_triplets + ${variable_overlay_triplets} + ${install_option_overlay_triplets} + ${configuration_overlay_triplets} + ${environment_overlay_triplets} +) + +set(overlay_index + 0 +) +foreach( + overlay_root IN + LISTS effective_overlay_ports +) + math( + EXPR + overlay_index + "${overlay_index} + 1" + ) + canary_shared_cache_tree_signature( + "overlay-port-${overlay_index}" + "${overlay_root}" + overlay_signature + ) + string( + APPEND + fingerprint_input + "${overlay_signature}" + ) +endforeach() +if(overlay_index + EQUAL + 0 +) + string( + APPEND + fingerprint_input + "overlay-ports=\n" + ) +endif() + +set(overlay_triplet_index + 0 +) +foreach( + overlay_triplet_root IN + LISTS effective_overlay_triplets +) + math( + EXPR + overlay_triplet_index + "${overlay_triplet_index} + 1" + ) + canary_shared_cache_tree_signature( + "overlay-triplet-${overlay_triplet_index}" + "${overlay_triplet_root}" + overlay_triplet_signature + ) + string( + APPEND + fingerprint_input + "${overlay_triplet_signature}" + ) +endforeach() +if(overlay_triplet_index + EQUAL + 0 +) + string( + APPEND + fingerprint_input + "overlay-triplets=\n" + ) +endif() + +foreach( + triplet_kind IN + ITEMS TARGET HOST +) + if(triplet_kind + STREQUAL + "TARGET" + ) + set(triplet_name + "${VCPKG_TARGET_TRIPLET}" + ) + else() + set(triplet_name + "${VCPKG_HOST_TRIPLET}" + ) + endif() + + if(NOT triplet_name) + set(triplet_name + "" + ) + endif() + string( + APPEND + fingerprint_input + "${triplet_kind}-triplet=${triplet_name}\n" + ) + + set(triplet_file) + if(NOT + triplet_name + STREQUAL + "" + ) + set(effective_overlay_triplet_roots + ${effective_overlay_triplets} + ) + list(REMOVE_DUPLICATES effective_overlay_triplet_roots) + foreach( + overlay_triplet_root IN + LISTS effective_overlay_triplet_roots + ) + if(EXISTS "${overlay_triplet_root}/${triplet_name}.cmake") + set(triplet_file + "${overlay_triplet_root}/${triplet_name}.cmake" + ) + break() + endif() + endforeach() + if(NOT triplet_file + AND EXISTS "${vcpkg_root}/triplets/${triplet_name}.cmake" + ) + set(triplet_file + "${vcpkg_root}/triplets/${triplet_name}.cmake" + ) + elseif( + NOT triplet_file + AND EXISTS "${vcpkg_root}/triplets/community/${triplet_name}.cmake" + ) + set(triplet_file + "${vcpkg_root}/triplets/community/${triplet_name}.cmake" + ) + endif() + endif() + + canary_shared_cache_file_signature( + "${triplet_kind}-triplet-file" + "${triplet_file}" + triplet_signature + ) + if(NOT triplet_file) + canary_shared_cache_reset_managed_install( + "the ${triplet_kind} triplet file cannot be identified" + ) + return() + endif() + set(${triplet_kind}_triplet_file_path + "${triplet_file}" + ) + string( + APPEND + fingerprint_input + "${triplet_signature}" + ) +endforeach() + +set(vcpkg_compiler_selection_signature + "vcpkg-compiler-selection=\n" +) +set(vcpkg_visual_studio_root) +if(WIN32) + foreach( + selected_triplet_file IN + ITEMS "${TARGET_triplet_file_path}" "${HOST_triplet_file_path}" + ) + file( + STRINGS + "${selected_triplet_file}" + triplet_contract_lines + ) + foreach( + triplet_contract_line IN + LISTS triplet_contract_lines + ) + string(STRIP "${triplet_contract_line}" triplet_contract_line) + if(NOT + triplet_contract_line + MATCHES + "^#" + AND (triplet_contract_line + MATCHES + "VCPKG_VISUAL_STUDIO_PATH" + OR triplet_contract_line + MATCHES + "^include[ \\t]*\\(" + ) + ) + canary_shared_cache_reset_managed_install( + "the selected Windows triplet can override Visual Studio selection indirectly" + ) + return() + endif() + endforeach() + endforeach() + + if(DEFINED ENV{VCPKG_VISUAL_STUDIO_PATH} + AND NOT + "$ENV{VCPKG_VISUAL_STUDIO_PATH}" + STREQUAL + "" + ) + set(vcpkg_visual_studio_candidate + "$ENV{VCPKG_VISUAL_STUDIO_PATH}" + ) + elseif( + DEFINED ENV{CANARY_VCPKG_VISUAL_STUDIO_PATH} + AND NOT + "$ENV{CANARY_VCPKG_VISUAL_STUDIO_PATH}" + STREQUAL + "" + ) + set(vcpkg_visual_studio_candidate + "$ENV{CANARY_VCPKG_VISUAL_STUDIO_PATH}" + ) + endif() + + if(NOT vcpkg_visual_studio_candidate) + canary_shared_cache_reset_managed_install( + "the vcpkg Visual Studio instance is not pinned" + ) + return() + endif() + + get_filename_component( + vcpkg_visual_studio_root + "${vcpkg_visual_studio_candidate}" + ABSOLUTE + ) + if(NOT + IS_DIRECTORY + "${vcpkg_visual_studio_root}/VC/Tools/MSVC" + OR NOT + EXISTS + "${vcpkg_visual_studio_root}/VC/Auxiliary/Build/vcvarsall.bat" + ) + canary_shared_cache_reset_managed_install( + "the pinned vcpkg Visual Studio path is not a complete C++ installation" + ) + return() + endif() + file( + REAL_PATH + "${vcpkg_visual_studio_root}" + vcpkg_visual_studio_root + ) + file( + TO_NATIVE_PATH + "${vcpkg_visual_studio_root}" + vcpkg_visual_studio_native + ) + set(ENV{VCPKG_VISUAL_STUDIO_PATH} + "${vcpkg_visual_studio_native}" + ) + + canary_shared_cache_normalize_path("${vcpkg_visual_studio_root}" + normalized_vcpkg_visual_studio_root + ) + canary_shared_cache_msvc_signature( + "${vcpkg_visual_studio_root}" + vcpkg_msvc_signature + vcpkg_msvc_found + ) + if(NOT vcpkg_msvc_found) + canary_shared_cache_reset_managed_install( + "no MSVC compiler exists below VCPKG_VISUAL_STUDIO_PATH" + ) + return() + endif() + canary_shared_cache_file_signature( + "vcpkg-vcvarsall" + "${vcpkg_visual_studio_root}/VC/Auxiliary/Build/vcvarsall.bat" + vcpkg_vcvarsall_signature + ) + canary_shared_cache_file_signature( + "vcpkg-msbuild" + "${vcpkg_visual_studio_root}/MSBuild/Current/Bin/amd64/MSBuild.exe" + vcpkg_msbuild_signature + ) + set(vcpkg_compiler_selection_signature + "vcpkg-visual-studio-root=${normalized_vcpkg_visual_studio_root}\n${vcpkg_msvc_signature}${vcpkg_vcvarsall_signature}${vcpkg_msbuild_signature}" + ) +endif() + +string( + APPEND + fingerprint_input + "${vcpkg_compiler_selection_signature}" +) + +if(VCPKG_CHAINLOAD_TOOLCHAIN_FILE) + canary_shared_cache_file_signature( + "chainload-toolchain" + "${VCPKG_CHAINLOAD_TOOLCHAIN_FILE}" + chainload_signature + ) + string( + APPEND + fingerprint_input + "${chainload_signature}" + ) +else() + string( + APPEND + fingerprint_input + "chainload-toolchain=\n" + ) +endif() + +string(SHA256 dependency_fingerprint "${fingerprint_input}") + +set(shared_cache_consumer + "cmake" +) +if(DEFINED CANARY_SHARED_CACHE_CONSUMER + AND NOT + CANARY_SHARED_CACHE_CONSUMER + STREQUAL + "" +) + string(TOLOWER "${CANARY_SHARED_CACHE_CONSUMER}" shared_cache_consumer) +endif() + +set(consumer_fingerprint_input + "schema=${CANARY_SHARED_CACHE_SCHEMA}\n" + "implementation-sha256=${CANARY_SHARED_CACHE_IMPLEMENTATION_SHA256}\n" + "dependency-fingerprint=${dependency_fingerprint}\n" + "consumer=${shared_cache_consumer}\n" +) +if(shared_cache_consumer + STREQUAL + "cmake" +) + string( + APPEND + consumer_fingerprint_input + "generator=${CMAKE_GENERATOR}\n" + "generator-platform=${CMAKE_GENERATOR_PLATFORM}\n" + "generator-toolset=${CMAKE_GENERATOR_TOOLSET}\n" + "cmake-version=${CMAKE_VERSION}\n" + "cmake-sha256=${cmake_executable_hash}\n" + ) +elseif( + shared_cache_consumer + STREQUAL + "msbuild" +) + if(NOT CANARY_SHARED_CACHE_CONSUMER_TOOL) + message( + FATAL_ERROR + "The MSBuild consumer contract requires CANARY_SHARED_CACHE_CONSUMER_TOOL" + ) + endif() + canary_shared_cache_resolve_program("${CANARY_SHARED_CACHE_CONSUMER_TOOL}" + consumer_tool_path + ) + if(NOT consumer_tool_path) + message( + FATAL_ERROR + "The MSBuild consumer executable cannot be identified: ${CANARY_SHARED_CACHE_CONSUMER_TOOL}" + ) + endif() + file( + SHA256 + "${consumer_tool_path}" + consumer_tool_hash + ) + get_filename_component( + consumer_tool_name + "${consumer_tool_path}" + NAME + ) + string( + APPEND + consumer_fingerprint_input + "consumer-tool-name=${consumer_tool_name}\n" + "consumer-tool-sha256=${consumer_tool_hash}\n" + ) + foreach( + consumer_setting IN + ITEMS CANARY_SHARED_CACHE_CONSUMER_CONFIGURATION + CANARY_SHARED_CACHE_CONSUMER_PLATFORM + CANARY_SHARED_CACHE_CONSUMER_LINK_CONFIGURATION + CANARY_SHARED_CACHE_CONSUMER_TOOLSET + CANARY_SHARED_CACHE_CONSUMER_SDK + ) + if(DEFINED ${consumer_setting}) + set(consumer_setting_value + "${${consumer_setting}}" + ) + else() + set(consumer_setting_value + "" + ) + endif() + string( + APPEND + consumer_fingerprint_input + "${consumer_setting}=${consumer_setting_value}\n" + ) + endforeach() +else() + message( + FATAL_ERROR + "Unsupported shared-cache consumer '${shared_cache_consumer}'" + ) +endif() +string(SHA256 consumer_fingerprint "${consumer_fingerprint_input}") + +if(CANARY_SHARED_CACHE_FINGERPRINT_INPUT_FILE) + get_filename_component( + fingerprint_input_file + "${CANARY_SHARED_CACHE_FINGERPRINT_INPUT_FILE}" + ABSOLUTE + BASE_DIR + "${CMAKE_BINARY_DIR}" + ) + file( + WRITE "${fingerprint_input_file}" + "[dependency]\n${fingerprint_input}[consumer]\n${consumer_fingerprint_input}" + ) +endif() + +set(cache_fingerprint + "${dependency_fingerprint}" +) +string( + SUBSTRING "${dependency_fingerprint}" + 0 + 24 + short_fingerprint +) +set(shared_installed_root + "${shared_cache_root}/vcpkg-installed/${CANARY_SHARED_CACHE_SCHEMA}/${short_fingerprint}" +) +set(shared_metadata_root + "${shared_cache_root}/metadata/${CANARY_SHARED_CACHE_SCHEMA}" +) +set(shared_buildtrees_root + "${shared_cache_root}/vcpkg-buildtrees/${CANARY_SHARED_CACHE_SCHEMA}/${short_fingerprint}" +) +set(shared_packages_root + "${shared_cache_root}/vcpkg-packages/${CANARY_SHARED_CACHE_SCHEMA}/${short_fingerprint}" +) + +if(CMAKE_PROJECT_NAME + AND NOT CANARY_SHARED_VCPKG_MANAGED + AND VCPKG_INSTALLED_DIR +) + canary_shared_cache_normalize_path("${VCPKG_INSTALLED_DIR}" + previous_unmanaged_installed_root + ) + canary_shared_cache_normalize_path("${shared_installed_root}" + expected_unmanaged_installed_root + ) + if(NOT + previous_unmanaged_installed_root + STREQUAL + expected_unmanaged_installed_root + ) + message( + FATAL_ERROR + "This configured build tree uses a different vcpkg installation contract. Re-run cmake --fresh --preset before enabling the shared pool." + ) + endif() +endif() + +if(CANARY_SHARED_VCPKG_MANAGED) + if(NOT + DEFINED + CANARY_VCPKG_CACHE_FINGERPRINT + OR NOT + CANARY_VCPKG_CACHE_FINGERPRINT + STREQUAL + cache_fingerprint + ) + message( + FATAL_ERROR + "The shared vcpkg fingerprint changed. Cached package paths cannot be migrated safely in place. Re-run cmake --fresh --preset ." + ) + endif() + + if(NOT + DEFINED + CANARY_VCPKG_CONSUMER_FINGERPRINT + OR NOT + CANARY_VCPKG_CONSUMER_FINGERPRINT + STREQUAL + consumer_fingerprint + ) + message( + FATAL_ERROR + "The shared vcpkg consumer fingerprint changed. Cached build-system paths cannot be migrated safely in place. Re-run cmake --fresh --preset ." + ) + endif() + + canary_shared_cache_normalize_path("${VCPKG_INSTALLED_DIR}" + previous_installed_root + ) + canary_shared_cache_normalize_path("${shared_installed_root}" + expected_installed_root + ) + if(NOT + previous_installed_root + STREQUAL + expected_installed_root + ) + message( + FATAL_ERROR + "The managed VCPKG_INSTALLED_DIR does not match its fingerprint. Re-run cmake --fresh --preset ." + ) + endif() +endif() + +if(CANARY_SHARED_CACHE_READ_ONLY) + if(CANARY_SHARED_CACHE_RESULT_FILE) + get_filename_component( + shared_cache_result_file + "${CANARY_SHARED_CACHE_RESULT_FILE}" + ABSOLUTE + BASE_DIR + "${CMAKE_BINARY_DIR}" + ) + file( + WRITE "${shared_cache_result_file}" + "active=true\n" + "schema=${CANARY_SHARED_CACHE_SCHEMA}\n" + "implementation-sha256=${CANARY_SHARED_CACHE_IMPLEMENTATION_SHA256}\n" + "dependency-fingerprint=${dependency_fingerprint}\n" + "consumer-fingerprint=${consumer_fingerprint}\n" + "installed-root=${shared_installed_root}\n" + "buildtrees-root=${shared_buildtrees_root}\n" + "packages-root=${shared_packages_root}\n" + "target-triplet=${VCPKG_TARGET_TRIPLET}\n" + "host-triplet=${VCPKG_HOST_TRIPLET}\n" + ) + endif() + message( + STATUS + "Shared vcpkg dependency fingerprint: ${shared_installed_root} (${dependency_fingerprint}); consumer ${consumer_fingerprint}" + ) + return() +endif() + +file(MAKE_DIRECTORY "${shared_installed_root}") +file(MAKE_DIRECTORY "${shared_metadata_root}") +file(MAKE_DIRECTORY "${shared_buildtrees_root}") +file(MAKE_DIRECTORY "${shared_packages_root}") + +set(fingerprint_identity_path + "${shared_metadata_root}/${short_fingerprint}.txt" +) +set(fingerprint_identity_lock_path + "${shared_metadata_root}/${short_fingerprint}.lock" +) +file( + LOCK "${fingerprint_identity_lock_path}" + GUARD FILE + TIMEOUT 30 + RESULT_VARIABLE fingerprint_identity_lock_result +) +if(NOT + fingerprint_identity_lock_result + EQUAL + 0 +) + message( + FATAL_ERROR + "Unable to acquire the shared-cache fingerprint identity lock: ${fingerprint_identity_lock_result}" + ) +endif() +if(EXISTS "${fingerprint_identity_path}") + file( + STRINGS + "${fingerprint_identity_path}" + existing_fingerprint_lines + REGEX "^fingerprint=" + LIMIT_COUNT 1 + ) + if(NOT + existing_fingerprint_lines + STREQUAL + "fingerprint=${cache_fingerprint}" + ) + message( + FATAL_ERROR + "A shared-cache directory prefix collision or corrupt identity was detected for ${short_fingerprint}. Refusing to reuse the mutable pool." + ) + endif() +endif() + +set(VCPKG_INSTALL_OPTIONS + ${canary_base_vcpkg_install_options} + "--x-buildtrees-root=${shared_buildtrees_root}" + "--x-packages-root=${shared_packages_root}" + CACHE STRING + "Options passed to vcpkg manifest installation" + FORCE +) + +set(metadata + "schema=${CANARY_SHARED_CACHE_SCHEMA}\n" +) +string( + APPEND + metadata + "implementation-sha256=${CANARY_SHARED_CACHE_IMPLEMENTATION_SHA256}\n" + "fingerprint=${dependency_fingerprint}\n" + "dependency-fingerprint=${dependency_fingerprint}\n" + "cxx-compiler=${cxx_compiler_name}\n" + "cxx-compiler-sha256=${cxx_compiler_hash}\n" + "c-compiler=${c_compiler_name}\n" + "c-compiler-sha256=${c_compiler_hash}\n" + "target-triplet=${VCPKG_TARGET_TRIPLET}\n" + "host-triplet=${VCPKG_HOST_TRIPLET}\n" + "vcpkg-revision=${vcpkg_revision}\n" + "vcpkg-visual-studio=${vcpkg_visual_studio_root}\n" + "buildtrees-root=${shared_buildtrees_root}\n" + "packages-root=${shared_packages_root}\n" +) +file( + WRITE "${fingerprint_identity_path}" + "${metadata}" +) +file( + LOCK + "${fingerprint_identity_lock_path}" + RELEASE +) + +set(VCPKG_INSTALLED_DIR + "${shared_installed_root}" + CACHE PATH + "Content-addressed vcpkg installation shared by compatible worktrees" + FORCE +) +set(CANARY_VCPKG_CACHE_FINGERPRINT + "${dependency_fingerprint}" + CACHE INTERNAL + "Fingerprint of the shared vcpkg installation contract" + FORCE +) +set(CANARY_VCPKG_DEPENDENCY_FINGERPRINT + "${dependency_fingerprint}" + CACHE INTERNAL + "Build-system-neutral vcpkg dependency fingerprint" + FORCE +) +set(CANARY_VCPKG_CONSUMER_FINGERPRINT + "${consumer_fingerprint}" + CACHE INTERNAL + "Build-system-specific consumer fingerprint" + FORCE +) +set(CANARY_SHARED_VCPKG_MANAGED + true + CACHE INTERNAL + "Whether Canary owns VCPKG_INSTALLED_DIR" + FORCE +) +set(CANARY_SHARED_VCPKG_BUILDTREES_ROOT + "${shared_buildtrees_root}" + CACHE INTERNAL + "Managed vcpkg buildtrees root" + FORCE +) +set(CANARY_SHARED_VCPKG_PACKAGES_ROOT + "${shared_packages_root}" + CACHE INTERNAL + "Managed vcpkg packages root" + FORCE +) +set(CANARY_SHARED_VCPKG_ACTIVE + true + CACHE INTERNAL + "Whether the shared vcpkg installed tree is active" + FORCE +) + +if(NOT CMAKE_SCRIPT_MODE_FILE) + file( + WRITE "${CMAKE_BINARY_DIR}/canary-shared-cache.txt" + "dependency-fingerprint=${dependency_fingerprint}\nconsumer-fingerprint=${consumer_fingerprint}\ninstalled=${shared_installed_root}\n" + ) +endif() + +if(CANARY_SHARED_CACHE_RESULT_FILE) + get_filename_component( + shared_cache_result_file + "${CANARY_SHARED_CACHE_RESULT_FILE}" + ABSOLUTE + BASE_DIR + "${CMAKE_BINARY_DIR}" + ) + file( + WRITE "${shared_cache_result_file}" + "active=true\n" + "schema=${CANARY_SHARED_CACHE_SCHEMA}\n" + "implementation-sha256=${CANARY_SHARED_CACHE_IMPLEMENTATION_SHA256}\n" + "dependency-fingerprint=${dependency_fingerprint}\n" + "consumer-fingerprint=${consumer_fingerprint}\n" + "installed-root=${shared_installed_root}\n" + "buildtrees-root=${shared_buildtrees_root}\n" + "packages-root=${shared_packages_root}\n" + "target-triplet=${VCPKG_TARGET_TRIPLET}\n" + "host-triplet=${VCPKG_HOST_TRIPLET}\n" + ) +endif() + +if(CANARY_SHARED_CACHE_PREPARE_ONLY) + message( + STATUS + "Prepared shared vcpkg dependency contract: ${shared_installed_root} (${short_fingerprint})" + ) + return() +endif() + +message( + STATUS + "Shared vcpkg installed tree: ${shared_installed_root} (${short_fingerprint})" +) diff --git a/docs/development/shared-build-cache.md b/docs/development/shared-build-cache.md new file mode 100644 index 000000000..365eff232 --- /dev/null +++ b/docs/development/shared-build-cache.md @@ -0,0 +1,298 @@ +# Shared build cache for worktrees and forks + +Related native-code repositories can share expensive reusable artifacts across Git worktrees and independent forks without sharing mutable CMake build trees. The shared pool must live on a local filesystem outside every source and build hierarchy, so removing one checkout cannot remove another checkout's dependencies. + +## Cache ownership + +| Layer | Ownership | Reason | +| --- | --- | --- | +| CMake and Solution outputs | One per worktree and build system | CMake/Ninja state, MSBuild intermediate directories, objects, PCH/PDB files, generated files, and executables contain consumer-specific paths. | +| vcpkg binary cache | Global | vcpkg addresses binary packages by their package ABI. Different manifests and baselines can reuse a package when its ABI is identical. | +| vcpkg downloads | Global | Sources and tools are reusable download assets. | +| `VCPKG_INSTALLED_DIR` | One per complete dependency fingerprint | Any worktree or independent fork with the same contract uses the same installed tree. Different contracts receive different trees. | +| vcpkg `buildtrees` and `packages` | One per fingerprint, transient | Compatible installs serialize on one installed-root lock. Incompatible installs cannot stage or clean each other's files. | +| sccache storage | Global per user or machine | Compiler outputs are content-addressed. Exact source roots in `SCCACHE_BASEDIRS` normalize equivalent paths across checkouts. | + +MSVC precompiled headers remain worktree-local. Current sccache releases report compilations using `/Fp` or `/Yc` as non-cacheable, so `SCCACHE_BASEDIRS` benefits only eligible units and tools. Do not share `.pch` files or remove PCH flags merely to force cache hits; use `sccache --show-stats` to distinguish this expected limitation from a path-normalization regression. + +Never junction, symlink, or otherwise share an entire `build` or `vcpkg_installed` directory manually. Do not place the pool inside a primary worktree. + +## Windows setup + +The setup and Solution helpers require PowerShell 7.2 or newer. Invoke them with `pwsh`, not Windows PowerShell 5.1. + +From a worktree in each independent Git repository that should participate, run: + +```powershell +pwsh -File tools/configure_shared_build_cache.ps1 +``` + +The Solution bridge requires MSBuild 17.8 or newer because it queries evaluated project properties without compiling. If the machine has more than one Visual Studio/MSBuild installation, select the supported executable that actually opens or builds the Solutions: + +```powershell +pwsh -File tools/configure_shared_build_cache.ps1 -SolutionMSBuildPath +``` + +The helper persists that choice as `CANARY_SOLUTION_MSBUILD_PATH` and uses it whenever it regenerates Solution contracts. The exact MSBuild executable is a consumer input, so switching to another installation requires regenerating the `.props`; the neutral dependency fingerprint and expanded vcpkg tree can still remain identical. + +The first invocation creates a machine-local repository registry below the shared cache root. Later invocations add the current repository's Git common directory. Every invocation re-enumerates all registered repositories and their worktrees, so `SCCACHE_BASEDIRS` and cache audits cover the complete set instead of replacing one fork with another. + +For backward compatibility, the default shared root is a `canary-build-cache` directory beside the active `VCPKG_ROOT`. Override it with the same short, local path in every participating repository when needed: + +```powershell +pwsh -File tools/configure_shared_build_cache.ps1 -CacheRoot +``` + +The helper: + +- persists `CANARY_SHARED_CACHE_ROOT` for the current Windows user; +- verifies that the pool and its existing ancestors are on a ready local fixed volume without reparse points, persists `CANARY_SHARED_CACHE_LOCAL_FILESYSTEM_VERIFIED=ON`, and binds that proof to the exact path in `CANARY_SHARED_CACHE_VERIFIED_ROOT`; +- registers the current independent Git repository and discovers all its worktrees; +- preserves an existing vcpkg binary-cache configuration or creates a local file backend when absent; +- never persists or prints an existing `VCPKG_BINARY_SOURCES`, because it may contain credentials; +- makes the vcpkg downloads directory explicit and global; +- does not persist or change `VCPKG_ROOT`; the active project or tool manager owns the vcpkg executable; +- pins the Visual Studio instance selected by vcpkg in the compatibility variable `CANARY_VCPKG_VISUAL_STUDIO_PATH`; the CMake module exports the standard vcpkg variable only to its configure process; +- manages `SCCACHE_BASEDIRS` as the union of every registered worktree root plus pre-existing unmanaged entries; +- creates cache directories and, when the repository carries the Solution bridge, prepares its ignored generated `.props`; it does not configure CMake or compile the project. + +Restart open terminals and long-running development applications after setup. Restart an existing sccache server only after active builds finish. Run the helper after adding, moving, or removing worktrees. + +Audit all registered repositories without mutation: + +```powershell +pwsh -File tools/configure_shared_build_cache.ps1 -AuditOnly +``` + +Remove the current repository family from the machine-local registry before retiring it: + +```powershell +pwsh -File tools/configure_shared_build_cache.ps1 -UnregisterCurrentRepository +``` + +This updates the managed sccache roots but does not delete the repository or any cache data. + +Remove only the legacy transient directories below the active `VCPKG_ROOT` with: + +```powershell +pwsh -File tools/configure_shared_build_cache.ps1 -CleanTransientVcpkg +``` + +The helper refuses cleanup while build-related processes are active and holds the vcpkg root lock during deletion. It preserves installed trees, downloads, binary packages, and fingerprint-specific pools. Cleanup mode does not register repositories, persist environment variables, create the shared layout, detect compilers, or regenerate Solution contracts. + +To reclaim only the disposable `buildtrees` and `packages` data for one known schema-v3 pool, pass its full dependency SHA-256: + +```text +pwsh -File tools/configure_shared_build_cache.ps1 -CleanSharedFingerprintTransients +``` + +This narrower cleanup validates the full-hash identity metadata, confines both targets to the verified local cache root, holds the registry operation lock and that installed tree's vcpkg lock, and refuses to run while build processes are active. It never removes the expanded installed tree, metadata, downloads, or binary cache, and it performs no setup side effects. Because it does not prune persistent data, it remains suitable when the global consumer audit is incomplete; pruning an installed fingerprint still requires the complete audit described below. + +## Non-Windows setup + +Set `CANARY_SHARED_CACHE_ROOT` to one short local path outside every participating checkout. After independently verifying that exact path is on a local filesystem with reliable lock and rename semantics, set `CANARY_SHARED_CACHE_LOCAL_FILESYSTEM_VERIFIED=ON` and set `CANARY_SHARED_CACHE_VERIFIED_ROOT` to the same absolute path. Repeat the verification whenever the root changes. Keep downloads, the vcpkg binary cache, and sccache global. Build the `SCCACHE_BASEDIRS` list from the exact roots emitted by `git worktree list` for every independent repository, separated by `:`. + +Do not use a network filesystem for mutable installed or transient pools. Its locking and rename semantics may not be strong enough for concurrent vcpkg operations. + +## Normal configure and build + +Continue using repository presets: + +```text +cmake --preset +cmake --build --preset +``` + +`cmake/SharedBuildCache.cmake` runs before the first `project()` call. When it can prove the complete installation contract, it selects: + +```text +/vcpkg-installed/v3/ +/vcpkg-buildtrees/v3/ +/vcpkg-packages/v3/ +``` + +The first directory is persistent. The latter two are transient and are cleaned after successful dependency builds by the preset's vcpkg options. + +If sharing cannot be proven, a fresh configure uses `/vcpkg_installed`, `/vcpkg-buildtrees`, and `/vcpkg-packages`. This fallback prefers duplication over mixing incompatible binaries and cannot collide with another opt-out project. + +Container and packaging stages may preprovision an immutable installed tree and configure with `VCPKG_MANIFEST_INSTALL=OFF`. In that explicit mode the module preserves the caller's `VCPKG_INSTALLED_DIR`, does not assign shared transient roots, and marks the shared pool inactive. Switching an existing managed configure tree to or from this mode still requires `--fresh`. Do not use this exception for an installation that vcpkg will mutate. + +Windows Ninja presets that select `cl.exe` require a Visual Studio developer environment. `VsDevCmd.bat` may replace an existing `VCPKG_ROOT` with the vcpkg bundled by Visual Studio, so initialize the developer environment first and then select the project-managed `VCPKG_ROOT` before running the helper, configure, or build in that same environment. A different vcpkg installation is a different dependency contract and may intentionally select a different pool or trigger the safe local fallback. On other hosts, set both `CC` and `CXX`, or both CMake compiler variables, when the first configure cannot identify them safely. + +Disable the shared installed tree for an isolated configure with: + +```text +-DCANARY_USE_SHARED_VCPKG_INSTALLED=OFF +``` + +Switching an existing build tree between opt-in and opt-out changes dependency paths and therefore requires: + +```text +cmake --fresh --preset -DCANARY_USE_SHARED_VCPKG_INSTALLED=OFF +``` + +Use separate configure presets and binary directories if opt-in and opt-out builds must exist simultaneously. + +## Visual Studio Solution builds + +Repositories that include `SharedVcpkgCache.targets` in their maintained Solution directory use the same dependency resolver for CMake and MSBuild. Normal setup generates an ignored, machine-local `.canary-shared-cache/SharedVcpkgCache.props` beside the selected project. The bridge discovers the maintained `vcproj`, `vc18`, or `vc17` project and all configurations for the requested platform. Regenerate it directly when only the Solution contract changed: + +```powershell +pwsh -File tools/configure_shared_solution_cache.ps1 +``` + +Projects whose Solution uses configuration names other than the conventional +`Debug` and `Release` can generate all contracts explicitly, for example: + +```powershell +pwsh -File tools/configure_shared_solution_cache.ps1 -Configurations Debug,OpenGL,DirectX +``` + +Configuration and platform names may contain spaces, such as `Release Static` or `Any CPU`. The helper rejects XML, path, and MSBuild metacharacters instead of embedding untrusted values in the generated condition. + +The setup and audit helper discovers the maintained x64 project in the common +`vcproj`, `vc18`, or `vc17` layout and derives its configuration names from the +project file. This keeps repositories with a CMake entry point and a native +Solution under the same dependency contract without sharing their object, +PCH, PDB, generated-source, or output directories. + +Reload the Visual Studio project after the file changes. The generated file supplies each supported configuration with its validated `VcpkgInstalledDir`, fingerprint-specific `buildtrees` and `packages` roots, cleanup options, selected vcpkg checkout, and pinned Visual Studio instance. The tracked target re-evaluates the complete contract immediately before `VcpkgInstallManifestDependencies` inside the active developer environment; stale generated values stop the build and request regeneration instead of mutating the wrong pool. + +The bridge uses two hashes: + +- the **dependency fingerprint** is neutral between CMake and MSBuild and owns the expanded installed and transient roots; +- the **consumer fingerprint** binds that dependency contract to the invoking build system, generator or configuration, and its build-system tool. + +Consequently, a Solution Release configuration and a CMake Release preset share one expanded tree only when their target and host triplets, features, registries, overlays, linkage, compiler, toolset, SDK, vcpkg revision, and dependency tools all match. Debug or any other configuration with a different contract receives another dependency fingerprint automatically. Command-line MSBuild overrides for the manifest root, triplets, link configuration, toolset, SDK, or install options are compared with the generated contract and fail closed when incompatible. Never copy a fingerprint from another configuration or edit the generated `.props`. + +When no generated `.props` exists, the Solution uses `vcpkg_installed`, `.vcpkg-buildtrees/`, and `.vcpkg-packages/` below its own project directory. This is the safe fallback when the global cache is not configured. Solution objects, PCH/PDB files, generated protocol sources, intermediate directories, and executables always remain local, even when dependencies converge with CMake. + +Audit only the Solution bridge without writing pools or generated files: + +```powershell +pwsh -File tools/configure_shared_solution_cache.ps1 -AuditOnly +``` + +Use `-WhatIf` to preview the contracts. The repository-wide `configure_shared_build_cache.ps1 -AuditOnly` also scans and re-evaluates generated Solution contracts across every registered worktree. The Solution inherits the global vcpkg downloads and binary cache. It does not automatically route compiler invocations through sccache; adding such a launcher is a separate concern, and MSVC PCH compilations remain ineligible. + +## Sharing across baselines and forks + +The repository name, branch, and absolute checkout path are not fingerprint inputs. Independent forks therefore converge when their declared and effective dependency contracts are identical. + +A common `builtin-baseline` improves the chance of reuse but is not sufficient. Sharing an installed tree also requires identical manifests, enabled features, registry configuration and content, ordered overlays, target and host triplets, linkage, vcpkg options, compiler/toolset identity, dependency-tool identity, and vcpkg revision. + +Repositories in one maintained fork family must inherit their vcpkg baseline, default registry baseline, version metadata, and shared custom-port payloads from that family's open upstream. A fork may add a dependency required by its own code, but it must resolve common dependencies through the same catalog instead of selecting an independent version. Scheduled update automation must synchronize from the open upstream rather than advancing each fork independently. + +An unrelated project family or a temporarily incompatible migration may still use a different catalog. The fingerprint isolates that contract while downloads and ABI-compatible binary packages remain global. Treat such a split as an explicit compatibility boundary, not as a disk-saving shortcut, and document why the canonical catalog cannot yet be used. + +One fixed global vcpkg tool checkout may serve several manifest baselines when that is the repository's supported workflow. If projects require different vcpkg tool revisions, activate an immutable tool root per revision or per shell. Never let concurrent projects switch one shared mutable vcpkg checkout between revisions. The selected executable, toolchain, and Git revision participate in the fingerprint. + +vcpkg does not provide a supported content-addressed or hardlinked representation for files expanded into different installed trees. The safe native limit is one expanded tree per exact contract; do not hardlink or junction two distinct fingerprints. + +### Canonical dependency contracts between forks + +The fingerprint deliberately does not guess that two different vcpkg declarations are semantically equivalent. For example, a byte-identical custom port delivered as an overlay and as a filesystem registry still produces different fingerprints. Those mechanisms have different precedence, version selection, and metadata rules, so automatically treating them as interchangeable could mix incompatible installations. + +When several maintained forks are intended to use the same dependency contract, select one canonical representation and port that representation atomically. A versioned filesystem registry is usually preferable for a maintained custom port because it records both the port payload and its version metadata. Equality requires more than copying the port directory: + +- keep `vcpkg.json`, the separate or embedded vcpkg configuration, registry metadata, triplets, features, and install options aligned; +- include the filesystem registry's `versions/baseline.json` and per-port version database, not only `ports/`; +- keep the same normalized `cmake/SharedBuildCache.cmake` implementation and schema; +- use the same vcpkg tool revision and compiler contract for configurations that are expected to converge; +- when automation updates a baseline that is declared in both the manifest and `default-registry`, update both declarations in one change. + +Before canonicalizing two forks, compare the dependency inputs and prove that the selected features, linkage, target and host triplets, and custom port contents are compatible. Align common dependency versions to the open upstream catalog. Preserve only genuinely additive dependencies and build-contract differences; they receive a separate fingerprint while still sharing downloads, sccache storage, and ABI-compatible binary packages. + +After adopting the canonical representation, run the setup helper from the newly participating repository, configure its existing preset with `--fresh`, regenerate any Solution `.props`, and compare the full dependency fingerprints. Matching fingerprints are the result of matching contracts; never override or manually rename a fingerprint to force convergence. If the fingerprints differ, inspect their metadata and preserve the separate pools until the remaining input difference is understood. + +Another build system must keep its expanded installed tree local until it consumes this same neutral dependency contract, validates its own consumer fingerprint, and supplies equivalent locking and transient-root options. It may still use the global downloads and vcpkg binary cache. + +## Fingerprint contract + +The dependency fingerprint contains inputs that can change the manifest installation or package ABI: + +- the normalized implementation hash and schema of `cmake/SharedBuildCache.cmake`; +- the complete `vcpkg.json`, including either supported embedded configuration spelling, and optional separate `vcpkg-configuration.json`; +- contents of filesystem registries declared by the configuration; +- ordered contents of overlay port and overlay triplet directories declared by variables or configuration; +- target and host triplet names and selected triplet files; +- manifest features, feature flags, linkage settings, build type, and install options; +- chainloaded toolchain contents; +- host identity and the CMake executable/version used as a dependency tool; +- C and C++ compiler executable hashes; +- on Windows, the pinned vcpkg Visual Studio instance, `vcvarsall.bat`, and selectable MSVC compiler tool binaries; +- selected Visual Studio toolset, Windows SDK, and host/target architecture environment; +- vcpkg executable, toolchain, repository revision, and relevant dirty state. + +The consumer fingerprint includes the dependency fingerprint and then adds either the CMake generator and CMake consumer identity, or the MSBuild executable, build configuration, link configuration, platform, toolset, and SDK. Consumer-specific values do not create duplicate installed trees when the dependency contract is identical, but they are validated to prevent a stale CMake cache or generated `.props` from silently changing build systems. + +The module disables sharing when any required identity or the local-filesystem guarantee is ambiguous. Absolute worktree paths do not participate. Content paths inside manifests and configurations still participate through the files themselves, while referenced local trees are hashed using relative file names and contents. + +The module's normalized SHA-256 is part of schema `v3`. Copies in different forks must therefore be byte-equivalent after newline normalization to converge. A divergent implementation selects another fingerprint even if a maintainer forgets to bump the schema. + +An existing configured preset never changes fingerprint or falls back in place. Cached package variables could retain paths into the old pool, so the module stops before `project()` and requests `cmake --fresh --preset `. + +Fingerprint input files and trees are CMake configure dependencies. Adding, removing, or changing a manifest, registry, overlay, triplet, compiler, or toolchain input requests regeneration. + +The full dependency SHA-256 and non-local metadata are written below `/metadata/v3`. Directory names use the first 24 hexadecimal characters to limit Windows path length; the metadata lock verifies the full hash before a shortened directory is accepted. + +## Concurrency + +Consumers of one fingerprint request one package set and installed-root lock. Different fingerprints receive independent installed, `buildtrees`, and `packages` roots. Projects that opt out receive build-local roots. + +Do not manually modify a fingerprint directory. Do not prune caches while CMake, vcpkg, Ninja, a compiler, or a linker is using any registered build family. + +## Migrating existing build trees + +Reconfigure an existing preset with `--fresh`; do not create an ad-hoc build directory: + +```text +cmake --fresh --preset +``` + +Verify its `CMakeCache.txt`: + +```text +CANARY_SHARED_VCPKG_ACTIVE:INTERNAL=true +CANARY_VCPKG_DEPENDENCY_FINGERPRINT:INTERNAL= +CANARY_VCPKG_CONSUMER_FINGERPRINT:INTERNAL= +VCPKG_INSTALLED_DIR:PATH=/vcpkg-installed/v3/ +``` + +Also confirm that `CMakeCache.txt` and the generated native build files contain neither a legacy local installed path nor another global fingerprint. Inspect `build.ninja` for a Ninja preset; inspect the generated `.sln` and `.vcxproj` files for a Visual Studio preset. Complete a build against the refreshed preset before deleting the old local tree, then build again after deletion. The final invocation must not recreate the local installation. + +For a Solution migration, generate the `.props`, reload the project, build every migrated configuration successfully, and confirm a no-op rebuild. Only then remove that worktree's exact legacy `vcpkg_installed` directory and repeat the build. If multiple configurations used the same local installed directory, all of them must be migrated and validated before deletion. + +## Cleanup and recovery + +Before pruning a fingerprint: + +1. Confirm no configure or build process is active. +2. Run the audit from any registered repository. +3. Inspect every reported `CMakeCache.txt` and generator-specific build file: `build.ninja` for Ninja, or the generated `.sln` and `.vcxproj` files for Visual Studio. Inspect every generated Solution cache contract as well. +4. Preserve every installed root referenced by any registered CMake or MSBuild consumer. +5. Remove only an unreferenced fingerprint and its matching transient and metadata entries. + +An audit that reports an unregistered, unavailable, partially enumerated, or malformed repository/configure tree, a non-local/reparse root, or a missing/mismatched full-hash identity is incomplete and exits with failure. Do not prune any global fingerprint until every registered family is available and the audit succeeds. + +Schema migrations intentionally create a new pool. Keep the previous schema until every registered configured build has migrated, built successfully, and stopped referencing it. + +If one fingerprint becomes corrupt, stop all its consumers, remove only that exact directory, and reconfigure one existing preset. vcpkg recreates it from the binary cache. Do not delete downloads or the global binary cache during normal recovery. + +## CI and production boundaries + +The feature is opt-in through `CANARY_SHARED_CACHE_ROOT`. CI and fresh clones retain build-local manifest installations unless their environment explicitly enables the pool. Cache setup must not change deployment directories, runtime data, production services, or release publication behavior. + +## Regression checklist + +Before changing build or dependency configuration, verify: + +- `build/` remains local to each worktree; +- Solution intermediate/output directories, PCH/PDB files, and generated files remain local; +- all vcpkg settings are finalized before `project()`; +- new manifest, registry, overlay, triplet, compiler, and toolchain inputs participate in the fingerprint; +- independent forks use the same module implementation before sharing a fingerprint; +- opt-out and fallback transients remain build-local; +- no machine-local path appears in committed presets or documentation; +- shared mutable state remains on a local filesystem outside every checkout; +- the repository registry and cleanup audit cover every consumer before data is removed. diff --git a/tools/configure_shared_build_cache.ps1 b/tools/configure_shared_build_cache.ps1 new file mode 100644 index 000000000..b5c0c16bb --- /dev/null +++ b/tools/configure_shared_build_cache.ps1 @@ -0,0 +1,1476 @@ +#Requires -Version 7.2 + +[CmdletBinding(SupportsShouldProcess = $true)] +param( + [Parameter()] + [string] $CacheRoot, + + [Parameter()] + [switch] $AuditOnly, + + [Parameter()] + [switch] $CleanTransientVcpkg, + + [Parameter()] + [string[]] $CleanSharedFingerprintTransients = @(), + + [Parameter()] + [string] $SolutionMSBuildPath, + + [Parameter()] + [switch] $UnregisterCurrentRepository +) + +Set-StrictMode -Version Latest +$ErrorActionPreference = "Stop" + +if ($env:OS -ne "Windows_NT") { + throw "This helper persists Windows user environment variables. Follow docs/development/shared-build-cache.md for non-Windows setup." +} + +if ( + $AuditOnly -and + ($CleanTransientVcpkg -or $CleanSharedFingerprintTransients.Count -gt 0 -or $UnregisterCurrentRepository) +) { + throw "-AuditOnly cannot be combined with a mutating option." +} + +if ( + $UnregisterCurrentRepository -and + ($CleanTransientVcpkg -or $CleanSharedFingerprintTransients.Count -gt 0) +) { + throw "-UnregisterCurrentRepository cannot be combined with a transient cleanup option. Run cleanup first." +} + +$cleanupOnly = $CleanTransientVcpkg -or $CleanSharedFingerprintTransients.Count -gt 0 + +function Get-EnvironmentValue { + param( + [Parameter(Mandatory = $true)] + [string] $Name + ) + + foreach ($scope in @("Process", "User", "Machine")) { + $value = [Environment]::GetEnvironmentVariable($Name, $scope) + if (-not [string]::IsNullOrWhiteSpace($value)) { + return $value + } + } + + return $null +} + +function Get-FullPath { + param( + [Parameter(Mandatory = $true)] + [string] $Path + ) + + $fullPath = [IO.Path]::GetFullPath($Path) + $pathRoot = [IO.Path]::GetPathRoot($fullPath) + if ($fullPath -eq $pathRoot) { + return $fullPath + } + + return $fullPath.TrimEnd( + [IO.Path]::DirectorySeparatorChar, + [IO.Path]::AltDirectorySeparatorChar + ) +} + +function Get-SolutionCacheDefinition { + param( + [Parameter(Mandatory = $true)] + [string] $RepositoryRoot + ) + + foreach ($relativeProjectPath in @( + "vcproj\canary.vcxproj", + "vc18\otclient.vcxproj", + "vc17\otclient.vcxproj" + )) { + $projectPath = Join-Path $RepositoryRoot $relativeProjectPath + if (-not (Test-Path -LiteralPath $projectPath -PathType Leaf)) { + continue + } + + try { + [xml] $project = Get-Content -LiteralPath $projectPath -Raw + } catch { + throw "The Solution cache project is malformed: $projectPath" + } + $configurations = @( + $project.SelectNodes("//*[local-name()='ProjectConfiguration']") | + ForEach-Object { [string] $_.Include } | + Where-Object { $_ -match '^(.+)\|x64$' } | + ForEach-Object { ($_ -split '\|', 2)[0] } | + Select-Object -Unique + ) + if ($configurations.Count -eq 0) { + throw "The Solution cache project declares no x64 configurations: $projectPath" + } + + $projectDirectory = Split-Path -Parent $projectPath + return [pscustomobject]@{ + Project = $projectPath + Props = Join-Path $projectDirectory ".canary-shared-cache\SharedVcpkgCache.props" + Configurations = $configurations + } + } + + return $null +} + +function Assert-LocalFixedVolume { + param( + [Parameter(Mandatory = $true)] + [string] $Path, + + [Parameter(Mandatory = $true)] + [string] $Description + ) + + $fullPath = Get-FullPath -Path $Path + $existingPath = $fullPath + while (-not (Test-Path -LiteralPath $existingPath)) { + $parentPath = Split-Path -Parent $existingPath + if ([string]::IsNullOrWhiteSpace($parentPath) -or $parentPath -eq $existingPath) { + throw "$Description has no verifiable existing filesystem ancestor: $fullPath" + } + $existingPath = $parentPath + } + while (-not [string]::IsNullOrWhiteSpace($existingPath)) { + $existingItem = Get-Item -LiteralPath $existingPath -Force + if ($existingItem.Attributes -band [IO.FileAttributes]::ReparsePoint) { + throw "$Description traverses a reparse point and cannot prove local filesystem semantics: $existingPath" + } + $parentPath = Split-Path -Parent $existingPath + if ([string]::IsNullOrWhiteSpace($parentPath) -or $parentPath -eq $existingPath) { + break + } + $existingPath = $parentPath + } + + $pathRoot = [IO.Path]::GetPathRoot($fullPath) + if ([string]::IsNullOrWhiteSpace($pathRoot)) { + throw "$Description does not resolve to a filesystem volume: $fullPath" + } + $drive = [IO.DriveInfo]::new($pathRoot) + if (-not $drive.IsReady -or $drive.DriveType -ne [IO.DriveType]::Fixed) { + throw "$Description must use a ready local fixed volume: $fullPath" + } +} + +function Test-PathWithin { + param( + [Parameter(Mandatory = $true)] + [string] $Path, + + [Parameter(Mandatory = $true)] + [string] $Parent + ) + + $fullPath = "$(Get-FullPath -Path $Path)$([IO.Path]::DirectorySeparatorChar)" + $fullParent = "$(Get-FullPath -Path $Parent)$([IO.Path]::DirectorySeparatorChar)" + return $fullPath.StartsWith($fullParent, [StringComparison]::OrdinalIgnoreCase) +} + +function Test-SharedFingerprintIdentity { + param( + [Parameter(Mandatory = $true)] + [string] $CacheRoot, + [Parameter(Mandatory = $true)] + [string] $Schema, + [Parameter(Mandatory = $true)] + [string] $Fingerprint + ) + + if ($Schema -notmatch "^v[0-9]+$" -or $Fingerprint -notmatch "^[0-9a-fA-F]{64}$") { + return $false + } + $shortFingerprint = $Fingerprint.Substring(0, 24).ToLowerInvariant() + $identityPath = Join-Path $CacheRoot "metadata\$Schema\$shortFingerprint.txt" + if (-not (Test-Path -LiteralPath $identityPath -PathType Leaf)) { + return $false + } + try { + Assert-LocalFixedVolume -Path $identityPath -Description "The shared fingerprint identity" + } catch { + return $false + } + $identityLine = Get-Content -LiteralPath $identityPath | + Where-Object { $_.StartsWith("fingerprint=", [StringComparison]::OrdinalIgnoreCase) } | + Select-Object -First 1 + return $identityLine -eq "fingerprint=$Fingerprint" +} + +function Set-UserEnvironmentValue { + param( + [Parameter(Mandatory = $true)] + [string] $Name, + + [Parameter(Mandatory = $true)] + [AllowEmptyString()] + [string] $Value + ) + + $currentValue = [Environment]::GetEnvironmentVariable($Name, "User") + if ($currentValue -eq $Value) { + return $false + } + + if ($PSCmdlet.ShouldProcess("Windows user environment", "set $Name")) { + [Environment]::SetEnvironmentVariable($Name, $Value, "User") + [Environment]::SetEnvironmentVariable($Name, $Value, "Process") + return $true + } + + return $false +} + +function Enter-SharedCacheOperationLock { + param( + [Parameter(Mandatory = $true)] + [string] $LockPath, + + [Parameter(Mandatory = $true)] + [IO.FileMode] $FileMode + ) + + $deadline = [DateTime]::UtcNow.AddSeconds(30) + while ($true) { + try { + return [IO.File]::Open( + $LockPath, + $FileMode, + [IO.FileAccess]::ReadWrite, + [IO.FileShare]::None + ) + } catch [IO.IOException] { + if ([DateTime]::UtcNow -ge $deadline) { + throw "Unable to acquire the shared-cache operation lock within 30 seconds: $LockPath" + } + Start-Sleep -Milliseconds 100 + } + } +} + +function Get-GitCommonDirectory { + param( + [Parameter(Mandatory = $true)] + [string] $RepositoryRoot + ) + + $commonDirectoryOutput = @(& git -C $RepositoryRoot rev-parse --path-format=absolute --git-common-dir 2>$null) + $gitExitCode = $LASTEXITCODE + $commonDirectory = ($commonDirectoryOutput -join [Environment]::NewLine).Trim() + if ($gitExitCode -ne 0 -or [string]::IsNullOrWhiteSpace($commonDirectory)) { + throw "Unable to identify the Git common directory for $RepositoryRoot." + } + + return (Get-FullPath -Path $commonDirectory) +} + +function Get-RegisteredGitCommonDirectories { + param( + [Parameter(Mandatory = $true)] + [string] $RegistryPath + ) + + if (-not (Test-Path -LiteralPath $RegistryPath -PathType Leaf)) { + return @() + } + + $registry = Get-Content -Raw -LiteralPath $RegistryPath | ConvertFrom-Json + if ($registry.schema -ne 2) { + throw "Unsupported shared-cache repository registry schema in $RegistryPath." + } + + return @( + $registry.repositories | + ForEach-Object { + if ([string]::IsNullOrWhiteSpace($_.gitCommonDir)) { + throw "The shared-cache repository registry contains an empty Git common directory." + } + [pscustomobject]@{ + GitCommonDirectory = Get-FullPath -Path $_.gitCommonDir + WorktreeRoots = @( + $_.worktreeRoots | + ForEach-Object { Get-FullPath -Path $_ } + ) + } + } + ) +} + +function Set-RegisteredGitCommonDirectories { + param( + [Parameter(Mandatory = $true)] + [string] $RegistryPath, + + [Parameter(Mandatory = $true)] + [AllowEmptyCollection()] + [string[]] $GitCommonDirectories, + + [Parameter()] + [string] $RemoveGitCommonDirectory + ) + + $registryDirectory = Split-Path -Parent $RegistryPath + [void] (New-Item -ItemType Directory -Path $registryDirectory -Force) + $registryLockPath = "$RegistryPath.lock" + $registryLock = $null + try { + $registryLock = [IO.File]::Open( + $registryLockPath, + [IO.FileMode]::OpenOrCreate, + [IO.FileAccess]::ReadWrite, + [IO.FileShare]::None + ) + $finalGitCommonDirectories = [Collections.Generic.HashSet[string]]::new( + [StringComparer]::OrdinalIgnoreCase + ) + $registeredSnapshots = @{} + foreach ($registeredRepository in Get-RegisteredGitCommonDirectories -RegistryPath $RegistryPath) { + [void] $finalGitCommonDirectories.Add($registeredRepository.GitCommonDirectory) + $registeredSnapshots[$registeredRepository.GitCommonDirectory] = @($registeredRepository.WorktreeRoots) + } + foreach ($gitCommonDirectory in $GitCommonDirectories) { + [void] $finalGitCommonDirectories.Add((Get-FullPath -Path $gitCommonDirectory)) + } + if (-not [string]::IsNullOrWhiteSpace($RemoveGitCommonDirectory)) { + [void] $finalGitCommonDirectories.Remove((Get-FullPath -Path $RemoveGitCommonDirectory)) + } + + $registry = [ordered]@{ + schema = 2 + repositories = @( + $finalGitCommonDirectories | + Sort-Object -Unique | + ForEach-Object { + $enumerationComplete = $false + $worktreeRoots = @( + Get-GitWorktreeRoots ` + -GitCommonDirectory $_ ` + -Complete ([ref] $enumerationComplete) + ) + if (-not $enumerationComplete -and $registeredSnapshots.ContainsKey($_)) { + $worktreeRoots = @( + $worktreeRoots + $registeredSnapshots[$_] | + Sort-Object -Unique + ) + } + $normalizedCommonDirectory = $_.Replace("\", "/").ToLowerInvariant() + $pathBytes = [Text.Encoding]::UTF8.GetBytes($normalizedCommonDirectory) + $sha256 = [Security.Cryptography.SHA256]::Create() + try { + $repositoryId = [Convert]::ToHexString($sha256.ComputeHash($pathBytes)).ToLowerInvariant() + } finally { + $sha256.Dispose() + } + [ordered]@{ + id = $repositoryId + gitCommonDir = $_ + worktreeRoots = @($worktreeRoots | Sort-Object -Unique) + } + } + ) + } + $json = $registry | ConvertTo-Json -Depth 4 + $temporaryPath = "$RegistryPath.$PID.tmp" + [IO.File]::WriteAllText( + $temporaryPath, + "$json$([Environment]::NewLine)", + [Text.UTF8Encoding]::new($false) + ) + try { + if (Test-Path -LiteralPath $RegistryPath -PathType Leaf) { + [IO.File]::Move($temporaryPath, $RegistryPath, $true) + } else { + [IO.File]::Move($temporaryPath, $RegistryPath) + } + } finally { + if (Test-Path -LiteralPath $temporaryPath -PathType Leaf) { + Remove-Item -LiteralPath $temporaryPath -Force + } + } + } catch [IO.IOException] { + throw "Unable to update the shared-cache repository registry: $($_.Exception.Message)" + } finally { + if ($null -ne $registryLock) { + $registryLock.Dispose() + } + } +} + +function Get-GitWorktreeRoots { + param( + [Parameter(Mandatory = $true)] + [string] $GitCommonDirectory, + + [Parameter()] + [ref] $Complete + ) + + if ($null -ne $Complete) { + $Complete.Value = $false + } + + if (-not (Test-Path -LiteralPath $GitCommonDirectory)) { + Write-Warning "Registered Git common directory is unavailable and remains registered: $GitCommonDirectory" + return @() + } + + $worktreeOutput = & git "--git-dir=$GitCommonDirectory" worktree list --porcelain 2>$null + if ($LASTEXITCODE -ne 0) { + Write-Warning "Unable to enumerate the registered Git repository: $GitCommonDirectory" + return @() + } + + $worktreeRoots = @() + $enumerationComplete = $true + foreach ($worktreeLine in $worktreeOutput | Where-Object { $_ -like "worktree *" }) { + $worktreeRoot = Get-FullPath -Path $worktreeLine.Substring(9) + if (-not (Test-Path -LiteralPath $worktreeRoot -PathType Container)) { + Write-Warning "Registered worktree is unavailable and was excluded from the active roots: $worktreeRoot" + $enumerationComplete = $false + continue + } + try { + $actualGitCommonDirectory = Get-GitCommonDirectory -RepositoryRoot $worktreeRoot + } catch { + Write-Warning "Registered worktree could not be verified and was excluded: $worktreeRoot" + $enumerationComplete = $false + continue + } + if ($actualGitCommonDirectory -ne (Get-FullPath -Path $GitCommonDirectory)) { + Write-Warning "Registered worktree resolves to another Git common directory and was excluded: $worktreeRoot" + $enumerationComplete = $false + continue + } + $worktreeRoots += $worktreeRoot + } + + if ($null -ne $Complete) { + $Complete.Value = $enumerationComplete + } + + return $worktreeRoots +} + +function Get-CMakeCacheValue { + param( + [Parameter(Mandatory = $true)] + [string] $CachePath, + + [Parameter(Mandatory = $true)] + [string] $Name + ) + + $match = Select-String -LiteralPath $CachePath -Pattern "^$([regex]::Escape($Name)):[^=]+=(.*)$" | + Select-Object -First 1 + if ($null -eq $match) { + return $null + } + + return $match.Matches[0].Groups[1].Value +} + +function Assert-NoActiveBuildProcesses { + $buildProcessNames = @( + "cc", + "cc1", + "cc1plus", + "cl", + "clang", + "clang++", + "clang-cl", + "cmake", + "c++", + "g++", + "gcc", + "jom", + "ld", + "link", + "lld", + "lld-link", + "make", + "msbuild", + "ninja", + "protoc", + "vcpkg" + ) + $activeBuildProcesses = @(Get-Process -Name $buildProcessNames -ErrorAction SilentlyContinue) + if ($activeBuildProcesses.Count -eq 0) { + return + } + + $processSummary = ($activeBuildProcesses | + Sort-Object ProcessName, Id | + ForEach-Object { "$($_.ProcessName):$($_.Id)" }) -join ", " + throw "Refusing to clean transient directories while build-related processes are running: $processSummary" +} + +$repositoryRootOutput = @(& git rev-parse --show-toplevel 2>$null) +$gitExitCode = $LASTEXITCODE +$repositoryRoot = ($repositoryRootOutput -join [Environment]::NewLine).Trim() +if ($gitExitCode -ne 0 -or [string]::IsNullOrWhiteSpace($repositoryRoot)) { + throw "Run this helper from a Canary Git worktree." +} +$repositoryRoot = Get-FullPath -Path $repositoryRoot +$currentGitCommonDirectory = Get-GitCommonDirectory -RepositoryRoot $repositoryRoot + +$vcpkgRoot = Get-EnvironmentValue -Name "VCPKG_ROOT" +if ([string]::IsNullOrWhiteSpace($vcpkgRoot)) { + throw "VCPKG_ROOT must point to the shared vcpkg installation." +} +$vcpkgRoot = Get-FullPath -Path $vcpkgRoot +if (-not (Test-Path -LiteralPath (Join-Path $vcpkgRoot "vcpkg.exe") -PathType Leaf)) { + throw "VCPKG_ROOT does not contain vcpkg.exe: $vcpkgRoot" +} + +if ([string]::IsNullOrWhiteSpace($CacheRoot)) { + $CacheRoot = Get-EnvironmentValue -Name "CANARY_SHARED_CACHE_ROOT" +} +if ([string]::IsNullOrWhiteSpace($CacheRoot)) { + $CacheRoot = Join-Path (Split-Path -Parent $vcpkgRoot) "canary-build-cache" +} +$CacheRoot = Get-FullPath -Path $CacheRoot +if ($CacheRoot.StartsWith("\\", [StringComparison]::Ordinal)) { + throw "The shared cache must use a local filesystem, not a UNC path." +} +Assert-LocalFixedVolume -Path $CacheRoot -Description "The shared cache" +$repositoryRegistryPath = Get-FullPath -Path (Join-Path $CacheRoot "registry\v2\repositories.json") +$operationLockPath = Get-FullPath -Path (Join-Path $CacheRoot "registry\v2\operation.lock") +$operationLock = $null +if (-not $WhatIfPreference) { + if ($AuditOnly -or $cleanupOnly) { + if (-not (Test-Path -LiteralPath $operationLockPath -PathType Leaf)) { + $operationName = if ($AuditOnly) { "Audit" } else { "Cleanup" } + throw "$operationName cannot acquire the shared-cache operation lock because normal setup has not initialized it. Run normal setup first." + } + $operationLock = Enter-SharedCacheOperationLock -LockPath $operationLockPath -FileMode Open + } else { + [void] (New-Item -ItemType Directory -Path (Split-Path -Parent $operationLockPath) -Force) + $operationLock = Enter-SharedCacheOperationLock -LockPath $operationLockPath -FileMode OpenOrCreate + } +} + +try { +if ($UnregisterCurrentRepository) { + if (-not (Test-Path -LiteralPath $repositoryRegistryPath -PathType Leaf)) { + throw "The current repository cannot be unregistered because no repository registry exists." + } + if ($PSCmdlet.ShouldProcess($repositoryRegistryPath, "unregister the current Git repository")) { + Set-RegisteredGitCommonDirectories ` + -RegistryPath $repositoryRegistryPath ` + -GitCommonDirectories @() ` + -RemoveGitCommonDirectory $currentGitCommonDirectory + } + if ($WhatIfPreference) { + Write-Output "Preview only: -WhatIf prevented repository registry changes." + } else { + $remainingWorktreeRootSet = [Collections.Generic.HashSet[string]]::new( + [StringComparer]::OrdinalIgnoreCase + ) + foreach ($remainingRepository in Get-RegisteredGitCommonDirectories -RegistryPath $repositoryRegistryPath) { + $remainingEnumerationComplete = $false + $remainingRoots = @( + Get-GitWorktreeRoots ` + -GitCommonDirectory $remainingRepository.GitCommonDirectory ` + -Complete ([ref] $remainingEnumerationComplete) + ) + if (-not $remainingEnumerationComplete) { + $remainingRoots = @( + $remainingRoots + $remainingRepository.WorktreeRoots | + Sort-Object -Unique + ) + } + foreach ($remainingRoot in $remainingRoots) { + [void] $remainingWorktreeRootSet.Add($remainingRoot) + } + } + + $updatedBaseDirSet = [Collections.Generic.HashSet[string]]::new( + [StringComparer]::OrdinalIgnoreCase + ) + $existingBaseDirs = Get-EnvironmentValue -Name "SCCACHE_BASEDIRS" + if (-not [string]::IsNullOrWhiteSpace($existingBaseDirs)) { + foreach ($baseDir in $existingBaseDirs.Split(";", [StringSplitOptions]::RemoveEmptyEntries)) { + [void] $updatedBaseDirSet.Add((Get-FullPath -Path $baseDir)) + } + } + $previousManagedBaseDirs = Get-EnvironmentValue -Name "CANARY_SCCACHE_BASEDIRS" + if (-not [string]::IsNullOrWhiteSpace($previousManagedBaseDirs)) { + foreach ($baseDir in $previousManagedBaseDirs.Split(";", [StringSplitOptions]::RemoveEmptyEntries)) { + [void] $updatedBaseDirSet.Remove((Get-FullPath -Path $baseDir)) + } + } + foreach ($remainingRoot in $remainingWorktreeRootSet) { + [void] $updatedBaseDirSet.Add($remainingRoot) + } + $remainingManagedRoots = ($remainingWorktreeRootSet | Sort-Object) -join ";" + $updatedBaseDirs = ($updatedBaseDirSet | Sort-Object) -join ";" + [void] (Set-UserEnvironmentValue -Name "CANARY_SCCACHE_BASEDIRS" -Value $remainingManagedRoots) + [void] (Set-UserEnvironmentValue -Name "SCCACHE_BASEDIRS" -Value $updatedBaseDirs) + Write-Output "The current Git repository family was removed from the shared-cache registry and managed sccache roots." + } + return +} + +$registeredRepositorySnapshots = @{} +$registeredGitCommonDirectories = [Collections.Generic.HashSet[string]]::new( + [StringComparer]::OrdinalIgnoreCase +) +foreach ($registeredRepository in Get-RegisteredGitCommonDirectories -RegistryPath $repositoryRegistryPath) { + [void] $registeredGitCommonDirectories.Add($registeredRepository.GitCommonDirectory) + $registeredRepositorySnapshots[$registeredRepository.GitCommonDirectory] = @($registeredRepository.WorktreeRoots) +} +$legacyManagedBaseDirs = Get-EnvironmentValue -Name "CANARY_SCCACHE_BASEDIRS" +if (-not [string]::IsNullOrWhiteSpace($legacyManagedBaseDirs)) { + foreach ($legacyWorktreeRoot in $legacyManagedBaseDirs.Split(";", [StringSplitOptions]::RemoveEmptyEntries)) { + if (-not (Test-Path -LiteralPath $legacyWorktreeRoot -PathType Container)) { + continue + } + try { + $legacyGitCommonDirectory = Get-GitCommonDirectory -RepositoryRoot $legacyWorktreeRoot + [void] $registeredGitCommonDirectories.Add($legacyGitCommonDirectory) + } catch { + Write-Warning "Unable to migrate a legacy managed sccache root into the repository registry: $legacyWorktreeRoot" + } + } +} +$currentRepositoryRegistered = $registeredGitCommonDirectories.Contains($currentGitCommonDirectory) +$currentRepositoryIsAuditCandidate = $AuditOnly -and -not $currentRepositoryRegistered +if ((-not $AuditOnly -and -not $cleanupOnly) -or $currentRepositoryIsAuditCandidate) { + [void] $registeredGitCommonDirectories.Add($currentGitCommonDirectory) +} + +$worktreeRootSet = [Collections.Generic.HashSet[string]]::new( + [StringComparer]::OrdinalIgnoreCase +) +$auditIncomplete = $currentRepositoryIsAuditCandidate +$repositoryFamilies = @( + foreach ($gitCommonDirectory in $registeredGitCommonDirectories | Sort-Object) { + $familyEnumerationComplete = $false + $familyWorktreeRoots = @( + Get-GitWorktreeRoots ` + -GitCommonDirectory $gitCommonDirectory ` + -Complete ([ref] $familyEnumerationComplete) + ) + $available = (Test-Path -LiteralPath $gitCommonDirectory) -and $familyEnumerationComplete + if (-not $available) { + $auditIncomplete = $true + } + if (-not $familyEnumerationComplete -and $registeredRepositorySnapshots.ContainsKey($gitCommonDirectory)) { + $familyWorktreeRoots = @( + $familyWorktreeRoots + $registeredRepositorySnapshots[$gitCommonDirectory] | + Sort-Object -Unique + ) + } + foreach ($worktreeRoot in $familyWorktreeRoots) { + [void] $worktreeRootSet.Add($worktreeRoot) + } + [pscustomobject]@{ + GitCommonDirectory = $gitCommonDirectory + Worktrees = $familyWorktreeRoots.Count + Available = $available + Registered = $gitCommonDirectory -ne $currentGitCommonDirectory -or $currentRepositoryRegistered -or -not $AuditOnly + } + } +) +$worktreeRoots = @($worktreeRootSet | Sort-Object) +$validationRoots = @($worktreeRoots + $repositoryRoot | Sort-Object -Unique) + +if ($CacheRoot.StartsWith("\\", [StringComparison]::Ordinal)) { + throw "The shared cache must use a local filesystem, not a UNC path." +} +Assert-LocalFixedVolume -Path $CacheRoot -Description "The shared cache" +Assert-LocalFixedVolume -Path $vcpkgRoot -Description "VCPKG_ROOT" +foreach ($worktreeRoot in $validationRoots) { + Assert-LocalFixedVolume -Path $worktreeRoot -Description "Registered worktree" + if ( + (Test-PathWithin -Path $CacheRoot -Parent $worktreeRoot) -or + (Test-PathWithin -Path $worktreeRoot -Parent $CacheRoot) + ) { + throw "The shared cache must be separate from every worktree hierarchy: $worktreeRoot" + } +} + +$vcpkgVisualStudioPath = Get-EnvironmentValue -Name "CANARY_VCPKG_VISUAL_STUDIO_PATH" +$explicitVcpkgVisualStudioPath = Get-EnvironmentValue -Name "VCPKG_VISUAL_STUDIO_PATH" +$visualStudioDetectionStatus = $null +if (-not [string]::IsNullOrWhiteSpace($explicitVcpkgVisualStudioPath)) { + $vcpkgVisualStudioPath = $explicitVcpkgVisualStudioPath + $visualStudioDetectionStatus = "using an existing explicit vcpkg override" +} +if ( + [string]::IsNullOrWhiteSpace($vcpkgVisualStudioPath) -and + -not $AuditOnly -and + -not $cleanupOnly +) { + $compilerDetectionRoot = Get-FullPath -Path (Join-Path $CacheRoot "compiler-detection") + if ($PSCmdlet.ShouldProcess($compilerDetectionRoot, "detect the Visual Studio instance selected by vcpkg")) { + [void] (New-Item -ItemType Directory -Path $compilerDetectionRoot -Force) + $compilerDetectionLockPath = Join-Path $compilerDetectionRoot "detection.lock" + $compilerDetectionLock = $null + try { + $compilerDetectionLock = [IO.File]::Open( + $compilerDetectionLockPath, + [IO.FileMode]::OpenOrCreate, + [IO.FileAccess]::ReadWrite, + [IO.FileShare]::None + ) + $vcpkgExecutable = Join-Path $vcpkgRoot "vcpkg.exe" + $compilerArguments = @( + "env", + "where cl", + "--triplet", + "x64-windows", + "--x-buildtrees-root=$compilerDetectionRoot" + ) + $compilerOutput = @(& $vcpkgExecutable @compilerArguments 2>&1) + if ($LASTEXITCODE -ne 0) { + throw "vcpkg could not identify its Visual Studio compiler: $($compilerOutput -join [Environment]::NewLine)" + } + $vcpkgCompiler = $compilerOutput | + ForEach-Object { $_.ToString().Trim() } | + Where-Object { + -not [string]::IsNullOrWhiteSpace($_) -and + (Split-Path -Leaf $_) -eq "cl.exe" -and + (Test-Path -LiteralPath $_ -PathType Leaf) + } | + Select-Object -First 1 + if ([string]::IsNullOrWhiteSpace($vcpkgCompiler)) { + throw "vcpkg did not return a verifiable cl.exe path." + } + if ($vcpkgCompiler -notmatch "^(?.+)\\VC\\Tools\\MSVC\\[^\\]+\\bin\\[^\\]+\\[^\\]+\\cl\.exe$") { + throw "Unexpected vcpkg compiler layout: $vcpkgCompiler" + } + $vcpkgVisualStudioPath = Get-FullPath -Path $Matches.root + $visualStudioDetectionStatus = "detected from vcpkg compiler selection" + } finally { + if ($null -ne $compilerDetectionLock) { + $compilerDetectionLock.Dispose() + } + } + } +} +if (-not [string]::IsNullOrWhiteSpace($vcpkgVisualStudioPath)) { + $vcpkgVisualStudioPath = Get-FullPath -Path $vcpkgVisualStudioPath + $vcvarsPath = Join-Path $vcpkgVisualStudioPath "VC\Auxiliary\Build\vcvarsall.bat" + if (-not (Test-Path -LiteralPath $vcvarsPath -PathType Leaf)) { + throw "The pinned vcpkg Visual Studio path is not a complete C++ installation: $vcpkgVisualStudioPath" + } +} + +$binaryCache = Get-EnvironmentValue -Name "VCPKG_DEFAULT_BINARY_CACHE" +if ([string]::IsNullOrWhiteSpace($binaryCache)) { + $binaryCache = Join-Path $CacheRoot "vcpkg-binary-cache" +} +$binaryCache = Get-FullPath -Path $binaryCache + +$binarySources = Get-EnvironmentValue -Name "VCPKG_BINARY_SOURCES" +$persistBinarySources = [string]::IsNullOrWhiteSpace($binarySources) +if ([string]::IsNullOrWhiteSpace($binarySources)) { + $binarySources = "clear;files,$binaryCache,readwrite" +} + +$downloadsRoot = Get-EnvironmentValue -Name "VCPKG_DOWNLOADS" +if ([string]::IsNullOrWhiteSpace($downloadsRoot)) { + $downloadsRoot = Join-Path $CacheRoot "vcpkg-downloads" +} +$downloadsRoot = Get-FullPath -Path $downloadsRoot + +if ([string]::IsNullOrWhiteSpace($SolutionMSBuildPath)) { + $SolutionMSBuildPath = Get-EnvironmentValue -Name "CANARY_SOLUTION_MSBUILD_PATH" +} +if (-not [string]::IsNullOrWhiteSpace($SolutionMSBuildPath)) { + $SolutionMSBuildPath = Get-FullPath -Path $SolutionMSBuildPath + if (-not (Test-Path -LiteralPath $SolutionMSBuildPath -PathType Leaf)) { + throw "The preferred Solution MSBuild executable was not found: $SolutionMSBuildPath" + } + Assert-LocalFixedVolume -Path $SolutionMSBuildPath -Description "The preferred Solution MSBuild executable" +} + +$existingBaseDirs = Get-EnvironmentValue -Name "SCCACHE_BASEDIRS" +$previousManagedBaseDirs = Get-EnvironmentValue -Name "CANARY_SCCACHE_BASEDIRS" +$baseDirSet = [Collections.Generic.HashSet[string]]::new( + [StringComparer]::OrdinalIgnoreCase +) +if (-not [string]::IsNullOrWhiteSpace($existingBaseDirs)) { + foreach ($baseDir in $existingBaseDirs.Split(";", [StringSplitOptions]::RemoveEmptyEntries)) { + [void] $baseDirSet.Add((Get-FullPath -Path $baseDir)) + } +} +if (-not [string]::IsNullOrWhiteSpace($previousManagedBaseDirs)) { + foreach ($baseDir in $previousManagedBaseDirs.Split(";", [StringSplitOptions]::RemoveEmptyEntries)) { + [void] $baseDirSet.Remove((Get-FullPath -Path $baseDir)) + } +} +foreach ($worktreeRoot in $worktreeRoots) { + [void] $baseDirSet.Add($worktreeRoot) +} +$sccacheBaseDirs = ($baseDirSet | Sort-Object) -join ";" +$managedSccacheBaseDirs = ($worktreeRoots | Sort-Object -Unique) -join ";" + +$environmentValues = [ordered]@{ + CANARY_SHARED_CACHE_ROOT = $CacheRoot + CANARY_SHARED_CACHE_LOCAL_FILESYSTEM_VERIFIED = "ON" + CANARY_SHARED_CACHE_VERIFIED_ROOT = $CacheRoot + CANARY_SCCACHE_BASEDIRS = $managedSccacheBaseDirs + VCPKG_DEFAULT_BINARY_CACHE = $binaryCache + VCPKG_DOWNLOADS = $downloadsRoot + SCCACHE_BASEDIRS = $sccacheBaseDirs +} +if (-not [string]::IsNullOrWhiteSpace($vcpkgVisualStudioPath)) { + $environmentValues.CANARY_VCPKG_VISUAL_STUDIO_PATH = $vcpkgVisualStudioPath +} +if (-not [string]::IsNullOrWhiteSpace($SolutionMSBuildPath)) { + $environmentValues.CANARY_SOLUTION_MSBUILD_PATH = $SolutionMSBuildPath +} +if ($persistBinarySources) { + $environmentValues.VCPKG_BINARY_SOURCES = $binarySources +} + +$reportedEnvironmentValues = [ordered]@{} +foreach ($entry in $environmentValues.GetEnumerator()) { + $reportedEnvironmentValues[$entry.Key] = $entry.Value +} +$reportedEnvironmentValues.VCPKG_ROOT = "$vcpkgRoot (active input; not managed by this helper)" +$reportedEnvironmentValues.RepositoryRegistry = $repositoryRegistryPath +$reportedEnvironmentValues.VCPKG_BINARY_SOURCES = if ($persistBinarySources) { + "" +} else { + "" +} +if ([string]::IsNullOrWhiteSpace($vcpkgVisualStudioPath)) { + $reportedEnvironmentValues.VCPKG_COMPILER_SELECTION = + "" +} elseif (-not [string]::IsNullOrWhiteSpace($visualStudioDetectionStatus)) { + $reportedEnvironmentValues.VCPKG_COMPILER_SELECTION = + "$vcpkgVisualStudioPath ($visualStudioDetectionStatus)" +} + +if (-not $AuditOnly) { + if (-not $cleanupOnly) { + foreach ($directory in @( + $CacheRoot, + (Join-Path $CacheRoot "vcpkg-installed\v2"), + (Join-Path $CacheRoot "vcpkg-buildtrees\v2"), + (Join-Path $CacheRoot "vcpkg-packages\v2"), + (Join-Path $CacheRoot "metadata\v2"), + (Join-Path $CacheRoot "vcpkg-installed\v3"), + (Join-Path $CacheRoot "vcpkg-buildtrees\v3"), + (Join-Path $CacheRoot "vcpkg-packages\v3"), + (Join-Path $CacheRoot "metadata\v3"), + (Join-Path $CacheRoot "registry\v2"), + $binaryCache, + $downloadsRoot + )) { + if ($PSCmdlet.ShouldProcess($directory, "create cache directory")) { + [void] (New-Item -ItemType Directory -Path $directory -Force) + } + } + + if ($PSCmdlet.ShouldProcess($repositoryRegistryPath, "update registered Git repositories")) { + $registryArguments = @{ + RegistryPath = $repositoryRegistryPath + GitCommonDirectories = @($registeredGitCommonDirectories) + } + Set-RegisteredGitCommonDirectories @registryArguments + } + + $environmentChanged = $false + foreach ($entry in $environmentValues.GetEnumerator()) { + if (Set-UserEnvironmentValue -Name $entry.Key -Value $entry.Value) { + $environmentChanged = $true + } + } + + if ($environmentChanged) { + Write-Warning "Environment changes apply automatically to new processes. Restart open terminals after active builds finish; restart the sccache server before measuring cross-worktree hits." + } + + $solutionCacheScript = Join-Path $repositoryRoot "tools\configure_shared_solution_cache.ps1" + $solutionDefinition = Get-SolutionCacheDefinition -RepositoryRoot $repositoryRoot + if ( + (Test-Path -LiteralPath $solutionCacheScript -PathType Leaf) -and + $null -ne $solutionDefinition + ) { + if ([string]::IsNullOrWhiteSpace($vcpkgVisualStudioPath)) { + throw "The Solution cache bridge requires a pinned Visual Studio instance." + } + $solutionProcessEnvironment = [ordered]@{ + CANARY_SHARED_CACHE_ROOT = $CacheRoot + CANARY_SHARED_CACHE_LOCAL_FILESYSTEM_VERIFIED = "ON" + CANARY_SHARED_CACHE_VERIFIED_ROOT = $CacheRoot + CANARY_VCPKG_VISUAL_STUDIO_PATH = $vcpkgVisualStudioPath + VCPKG_ROOT = $vcpkgRoot + } + $previousSolutionProcessEnvironment = @{} + $solutionCacheArguments = @( + "-NoLogo", + "-NoProfile", + "-NonInteractive", + "-File", + $solutionCacheScript, + "-ProjectFile", + $solutionDefinition.Project, + "-OutputProps", + $solutionDefinition.Props, + "-Configurations", + ($solutionDefinition.Configurations -join ","), + "-CacheRoot", + $CacheRoot, + "-VcpkgRoot", + $vcpkgRoot, + "-VisualStudioPath", + $vcpkgVisualStudioPath + ) + if (-not [string]::IsNullOrWhiteSpace($SolutionMSBuildPath)) { + $solutionCacheArguments += @("-MSBuildPath", $SolutionMSBuildPath) + } + if ($WhatIfPreference) { + $solutionCacheArguments += "-WhatIf" + } + $solutionCacheOutput = @() + $solutionCacheExitCode = 0 + try { + foreach ($processEnvironmentEntry in $solutionProcessEnvironment.GetEnumerator()) { + $previousSolutionProcessEnvironment[$processEnvironmentEntry.Key] = + [Environment]::GetEnvironmentVariable($processEnvironmentEntry.Key, "Process") + [Environment]::SetEnvironmentVariable( + $processEnvironmentEntry.Key, + $processEnvironmentEntry.Value, + "Process" + ) + } + $solutionCacheOutput = @(& pwsh.exe @solutionCacheArguments 2>&1) + $solutionCacheExitCode = $LASTEXITCODE + } finally { + foreach ($restoredName in @($previousSolutionProcessEnvironment.Keys)) { + $previousProcessValue = $previousSolutionProcessEnvironment[$restoredName] + if ($null -eq $previousProcessValue) { + [Environment]::SetEnvironmentVariable( + $restoredName, + [NullString]::Value, + [EnvironmentVariableTarget]::Process + ) + } else { + [Environment]::SetEnvironmentVariable( + $restoredName, + $previousProcessValue, + [EnvironmentVariableTarget]::Process + ) + } + } + } + if ($solutionCacheExitCode -ne 0) { + throw "The Solution cache bridge could not be configured.`n$($solutionCacheOutput -join [Environment]::NewLine)" + } + $solutionCacheOutput | Write-Output + } + } + + if ($CleanTransientVcpkg) { + Assert-NoActiveBuildProcesses + + $vcpkgLockPath = Join-Path $vcpkgRoot ".vcpkg-root" + $vcpkgLockStream = $null + try { + $vcpkgLockStream = [IO.File]::Open( + $vcpkgLockPath, + [IO.FileMode]::Open, + [IO.FileAccess]::ReadWrite, + [IO.FileShare]::None + ) + + foreach ($directoryName in @("packages", "buildtrees")) { + $transientRoot = Get-FullPath -Path (Join-Path $vcpkgRoot $directoryName) + $transientParent = Get-FullPath -Path (Split-Path -Parent $transientRoot) + $transientName = Split-Path -Leaf $transientRoot + $transientItem = Get-Item -LiteralPath $transientRoot -Force -ErrorAction SilentlyContinue + if ( + $transientParent -ne $vcpkgRoot -or + $transientName -ne $directoryName -or + -not (Test-PathWithin -Path $transientRoot -Parent $vcpkgRoot) -or + ($null -ne $transientItem -and + ($transientItem.Attributes -band [IO.FileAttributes]::ReparsePoint)) + ) { + throw "Refusing unsafe transient-cache target: $transientRoot" + } + + if (-not (Test-Path -LiteralPath $transientRoot -PathType Container)) { + continue + } + + $measurement = Get-ChildItem -LiteralPath $transientRoot -Recurse -File -Force | + Measure-Object -Property Length -Sum + if ($PSCmdlet.ShouldProcess($transientRoot, "remove disposable vcpkg $directoryName data")) { + Remove-Item -LiteralPath $transientRoot -Recurse -Force + [void] (New-Item -ItemType Directory -Path $transientRoot) + $removedGiB = [math]::Round($measurement.Sum / 1GB, 2) + Write-Output "Removed $removedGiB GiB from disposable vcpkg $directoryName data. It can be restored from the binary cache or rebuilt." + } + } + } catch [IO.IOException] { + throw "Refusing to clean because the vcpkg filesystem lock cannot be acquired: $($_.Exception.Message)" + } finally { + if ($null -ne $vcpkgLockStream) { + $vcpkgLockStream.Dispose() + } + } + } + + if ($CleanSharedFingerprintTransients.Count -gt 0) { + Assert-NoActiveBuildProcesses + + foreach ($fingerprintInput in $CleanSharedFingerprintTransients | Sort-Object -Unique) { + $fingerprint = $fingerprintInput.Trim().ToLowerInvariant() + if ($fingerprint -notmatch "^[0-9a-f]{64}$") { + throw "A shared fingerprint cleanup target must be a full 64-character SHA-256: $fingerprintInput" + } + if (-not (Test-SharedFingerprintIdentity -CacheRoot $CacheRoot -Schema "v3" -Fingerprint $fingerprint)) { + throw "The shared fingerprint identity is missing or does not match: $fingerprint" + } + + $shortFingerprint = $fingerprint.Substring(0, 24) + $installedRoot = Get-FullPath -Path (Join-Path $CacheRoot "vcpkg-installed\v3\$shortFingerprint") + $installedParent = Get-FullPath -Path (Join-Path $CacheRoot "vcpkg-installed\v3") + if ( + (Get-FullPath -Path (Split-Path -Parent $installedRoot)) -ne $installedParent -or + (Split-Path -Leaf $installedRoot) -ne $shortFingerprint -or + -not (Test-PathWithin -Path $installedRoot -Parent $CacheRoot) + ) { + throw "Refusing an unsafe shared installed-root lock target: $installedRoot" + } + $vcpkgLockPath = Join-Path $installedRoot "vcpkg\vcpkg-running.lock" + if (-not (Test-Path -LiteralPath $vcpkgLockPath -PathType Leaf)) { + throw "The shared fingerprint has no vcpkg filesystem lock: $vcpkgLockPath" + } + + $vcpkgLockStream = $null + try { + $vcpkgLockStream = [IO.File]::Open( + $vcpkgLockPath, + [IO.FileMode]::Open, + [IO.FileAccess]::ReadWrite, + [IO.FileShare]::None + ) + + foreach ($directoryName in @("vcpkg-buildtrees", "vcpkg-packages")) { + $schemaRoot = Get-FullPath -Path (Join-Path $CacheRoot "$directoryName\v3") + $transientRoot = Get-FullPath -Path (Join-Path $schemaRoot $shortFingerprint) + $transientItem = Get-Item -LiteralPath $transientRoot -Force -ErrorAction SilentlyContinue + if ( + (Get-FullPath -Path (Split-Path -Parent $transientRoot)) -ne $schemaRoot -or + (Split-Path -Leaf $transientRoot) -ne $shortFingerprint -or + -not (Test-PathWithin -Path $transientRoot -Parent $CacheRoot) -or + ($null -ne $transientItem -and + ($transientItem.Attributes -band [IO.FileAttributes]::ReparsePoint)) + ) { + throw "Refusing an unsafe shared transient-cache target: $transientRoot" + } + if (-not (Test-Path -LiteralPath $transientRoot -PathType Container)) { + continue + } + Assert-LocalFixedVolume -Path $transientRoot -Description "The shared transient cache" + + $measurement = Get-ChildItem -LiteralPath $transientRoot -Recurse -File -Force | + Measure-Object -Property Length -Sum + if ($PSCmdlet.ShouldProcess($transientRoot, "remove disposable fingerprint-specific vcpkg data")) { + Remove-Item -LiteralPath $transientRoot -Recurse -Force + [void] (New-Item -ItemType Directory -Path $transientRoot) + $removedGiB = [math]::Round($measurement.Sum / 1GB, 2) + Write-Output "Removed $removedGiB GiB from $transientRoot. It can be restored from the binary cache or rebuilt." + } + } + } catch [IO.IOException] { + throw "Refusing to clean because the fingerprint vcpkg lock cannot be acquired: $($_.Exception.Message)" + } finally { + if ($null -ne $vcpkgLockStream) { + $vcpkgLockStream.Dispose() + } + } + } + } + + if ($cleanupOnly) { + if ($WhatIfPreference) { + Write-Output "Preview only: -WhatIf prevented transient cache cleanup." + } else { + Write-Output "Cleanup complete. Repository registration, environment variables, and Solution contracts were left unchanged." + } + return + } +} + +Write-Output "Shared cache configuration" +$reportedEnvironmentValues.GetEnumerator() | ForEach-Object { + [pscustomobject]@{ + Name = $_.Key + Value = $_.Value + } +} | Format-Table -AutoSize | Out-String | Write-Output + +Write-Output "Registered Git repository families" +if ($repositoryFamilies.Count -eq 0) { + Write-Output "No Git repository families are registered." +} else { + $repositoryFamilies | Format-Table -AutoSize -Wrap | Out-String | Write-Output +} + +Write-Output "Existing configure trees" +$configureTrees = foreach ($worktreeRoot in $worktreeRoots) { + $buildRoot = Join-Path $worktreeRoot "build" + if (-not (Test-Path -LiteralPath $buildRoot -PathType Container)) { + continue + } + $buildRootItem = Get-Item -LiteralPath $buildRoot -Force + if ($buildRootItem.Attributes -band [IO.FileAttributes]::ReparsePoint) { + Write-Warning "The build root is a reparse point and cannot be audited safely: $buildRoot" + $auditIncomplete = $true + continue + } + + foreach ($presetDirectory in Get-ChildItem -LiteralPath $buildRoot -Directory -Force | + Where-Object { -not ($_.Attributes -band [IO.FileAttributes]::ReparsePoint) }) { + $cachePath = Join-Path $presetDirectory.FullName "CMakeCache.txt" + if (-not (Test-Path -LiteralPath $cachePath -PathType Leaf)) { + $orphanedGeneratedArtifacts = @(@( + (Join-Path $presetDirectory.FullName "build.ninja"), + (Join-Path $presetDirectory.FullName "canary-shared-cache.txt"), + (Join-Path $presetDirectory.FullName "CMakeFiles") + ) | Where-Object { Test-Path -LiteralPath $_ }) + if ($orphanedGeneratedArtifacts.Count -gt 0) { + Write-Warning "A partially removed configure tree still contains generated artifacts but no CMakeCache.txt: $($presetDirectory.FullName)" + $auditIncomplete = $true + } + continue + } + $cacheItem = Get-Item -LiteralPath $cachePath -Force + if ($cacheItem.Attributes -band [IO.FileAttributes]::ReparsePoint) { + Write-Warning "CMakeCache.txt is a reparse point and cannot be audited safely: $cachePath" + $auditIncomplete = $true + continue + } + + $cmakeHomeDirectory = Get-CMakeCacheValue -CachePath $cachePath -Name "CMAKE_HOME_DIRECTORY" + if ([string]::IsNullOrWhiteSpace($cmakeHomeDirectory)) { + Write-Warning "Ignoring a malformed CMake cache without CMAKE_HOME_DIRECTORY: $cachePath" + $auditIncomplete = $true + continue + } + if ((Get-FullPath -Path $cmakeHomeDirectory) -ne $worktreeRoot) { + Write-Warning "Ignoring a CMake cache whose source directory does not match its registered worktree: $cachePath" + $auditIncomplete = $true + continue + } + + $installedRoot = Get-CMakeCacheValue -CachePath $cachePath -Name "VCPKG_INSTALLED_DIR" + if ([string]::IsNullOrWhiteSpace($installedRoot)) { + Write-Warning "Ignoring a malformed CMake cache without VCPKG_INSTALLED_DIR: $cachePath" + $auditIncomplete = $true + continue + } + $sharedActive = Get-CMakeCacheValue -CachePath $cachePath -Name "CANARY_SHARED_VCPKG_ACTIVE" + $sharedActiveBoolean = $sharedActive -eq "true" -or $sharedActive -eq "ON" + $dependencyFingerprint = Get-CMakeCacheValue -CachePath $cachePath -Name "CANARY_VCPKG_DEPENDENCY_FINGERPRINT" + if ([string]::IsNullOrWhiteSpace($dependencyFingerprint)) { + $dependencyFingerprint = Get-CMakeCacheValue -CachePath $cachePath -Name "CANARY_VCPKG_CACHE_FINGERPRINT" + } + $sharedBuildtreesRoot = Get-CMakeCacheValue -CachePath $cachePath -Name "CANARY_SHARED_VCPKG_BUILDTREES_ROOT" + $sharedPackagesRoot = Get-CMakeCacheValue -CachePath $cachePath -Name "CANARY_SHARED_VCPKG_PACKAGES_ROOT" + $sharedSchema = "" + $sharedContractValid = $true + if ($sharedActiveBoolean) { + if ($dependencyFingerprint -notmatch "^[0-9a-fA-F]{64}$") { + $sharedContractValid = $false + } else { + foreach ($candidateSchema in @("v1", "v2", "v3")) { + $shortFingerprint = $dependencyFingerprint.Substring(0, 24).ToLowerInvariant() + $expectedInstalledRoot = Get-FullPath -Path (Join-Path $CacheRoot "vcpkg-installed\$candidateSchema\$shortFingerprint") + if ((Get-FullPath -Path $installedRoot).Equals($expectedInstalledRoot, [StringComparison]::OrdinalIgnoreCase)) { + $sharedSchema = $candidateSchema + break + } + } + if ([string]::IsNullOrWhiteSpace($sharedSchema)) { + $sharedContractValid = $false + } + } + + if ($sharedContractValid) { + $shortFingerprint = $dependencyFingerprint.Substring(0, 24).ToLowerInvariant() + $expectedBuildtreesRoot = Get-FullPath -Path (Join-Path $CacheRoot "vcpkg-buildtrees\$sharedSchema\$shortFingerprint") + $expectedPackagesRoot = Get-FullPath -Path (Join-Path $CacheRoot "vcpkg-packages\$sharedSchema\$shortFingerprint") + $sharedContractValid = + -not [string]::IsNullOrWhiteSpace($sharedBuildtreesRoot) -and + -not [string]::IsNullOrWhiteSpace($sharedPackagesRoot) -and + (Get-FullPath -Path $sharedBuildtreesRoot).Equals($expectedBuildtreesRoot, [StringComparison]::OrdinalIgnoreCase) -and + (Get-FullPath -Path $sharedPackagesRoot).Equals($expectedPackagesRoot, [StringComparison]::OrdinalIgnoreCase) -and + (Test-SharedFingerprintIdentity -CacheRoot $CacheRoot -Schema $sharedSchema -Fingerprint $dependencyFingerprint) + if ($sharedContractValid) { + try { + Assert-LocalFixedVolume -Path $installedRoot -Description "The shared installed root" + Assert-LocalFixedVolume -Path $sharedBuildtreesRoot -Description "The shared buildtrees root" + Assert-LocalFixedVolume -Path $sharedPackagesRoot -Description "The shared packages root" + } catch { + $sharedContractValid = $false + } + } + } + + if (-not $sharedContractValid) { + Write-Warning "A managed CMake cache does not identify exact, local fingerprint roots with a matching full-hash identity: $cachePath" + $auditIncomplete = $true + } + } + $legacyInstalledRoot = Get-FullPath -Path (Join-Path $presetDirectory.FullName "vcpkg_installed") + $legacyPatterns = @( + $legacyInstalledRoot, + $legacyInstalledRoot.Replace("\", "/") + ) | Select-Object -Unique + $generatedFiles = @($cachePath) + $ninjaPath = Join-Path $presetDirectory.FullName "build.ninja" + if (Test-Path -LiteralPath $ninjaPath -PathType Leaf) { + $ninjaItem = Get-Item -LiteralPath $ninjaPath -Force + if ($ninjaItem.Attributes -band [IO.FileAttributes]::ReparsePoint) { + Write-Warning "build.ninja is a reparse point and cannot be audited safely: $ninjaPath" + $auditIncomplete = $true + continue + } + $generatedFiles += $ninjaPath + } + $legacyReferenceSearch = @{ + LiteralPath = $generatedFiles + SimpleMatch = $true + Pattern = $legacyPatterns + ErrorAction = "Stop" + } + $legacyReferences = @(Select-String @legacyReferenceSearch).Count -gt 0 + + $sharedInstalledPrefix = Get-FullPath -Path (Join-Path $CacheRoot "vcpkg-installed") + $sharedInstalledPrefix = $sharedInstalledPrefix.Replace("\", "/").ToLowerInvariant() + $currentInstalledNormalized = if ([string]::IsNullOrWhiteSpace($installedRoot)) { + "" + } else { + $installedRoot.Replace("\", "/").TrimEnd("/").ToLowerInvariant() + } + $staleSharedReferences = $false + foreach ($generatedFile in $generatedFiles) { + $matchingLines = Select-String ` + -LiteralPath $generatedFile ` + -SimpleMatch "$sharedInstalledPrefix/" ` + -ErrorAction Stop + foreach ($matchingLine in $matchingLines) { + $normalizedLine = $matchingLine.Line.Replace("\", "/").ToLowerInvariant() + if ( + -not $normalizedLine.Contains("$currentInstalledNormalized/") -and + -not $normalizedLine.EndsWith("=$currentInstalledNormalized") + ) { + $staleSharedReferences = $true + break + } + } + if ($staleSharedReferences) { + break + } + } + [pscustomobject]@{ + Worktree = $worktreeRoot + Preset = $presetDirectory.Name + SharedActive = $sharedActiveBoolean + Schema = $sharedSchema + Fingerprint = $dependencyFingerprint + LegacyReferences = $legacyReferences + StaleSharedReferences = $staleSharedReferences + Installed = $installedRoot + } + } + + $reparsePresetDirectories = @( + Get-ChildItem -LiteralPath $buildRoot -Directory -Force | + Where-Object { $_.Attributes -band [IO.FileAttributes]::ReparsePoint } + ) + foreach ($reparsePresetDirectory in $reparsePresetDirectories) { + Write-Warning "A build preset directory is a reparse point and cannot be audited safely: $($reparsePresetDirectory.FullName)" + $auditIncomplete = $true + } +} + +if ($null -eq $configureTrees) { + Write-Output "No configured build trees found." +} else { + $configureTrees | Format-Table -AutoSize -Wrap | Out-String | Write-Output +} + +Write-Output "Existing Solution dependency contracts" +$solutionTrees = foreach ($worktreeRoot in $worktreeRoots) { + $solutionDefinition = Get-SolutionCacheDefinition -RepositoryRoot $worktreeRoot + if ($null -eq $solutionDefinition) { + continue + } + $solutionPropsPath = $solutionDefinition.Props + if (-not (Test-Path -LiteralPath $solutionPropsPath -PathType Leaf)) { + continue + } + $solutionPropsDirectory = Split-Path -Parent $solutionPropsPath + $solutionPropsDirectoryItem = Get-Item -LiteralPath $solutionPropsDirectory -Force + $solutionPropsItem = Get-Item -LiteralPath $solutionPropsPath -Force + if ( + ($solutionPropsDirectoryItem.Attributes -band [IO.FileAttributes]::ReparsePoint) -or + ($solutionPropsItem.Attributes -band [IO.FileAttributes]::ReparsePoint) + ) { + Write-Warning "Generated Solution cache props use a reparse point and cannot be audited safely: $solutionPropsPath" + $auditIncomplete = $true + continue + } + + try { + [xml] $solutionProps = Get-Content -LiteralPath $solutionPropsPath -Raw + } catch { + Write-Warning "Generated Solution cache props are malformed: $solutionPropsPath" + $auditIncomplete = $true + continue + } + + $activePropertyGroups = @( + $solutionProps.SelectNodes("/*[local-name()='Project']/*[local-name()='PropertyGroup']") | + Where-Object { $_.CanarySharedVcpkgActive -eq "true" } + ) + if ($activePropertyGroups.Count -eq 0) { + Write-Warning "Generated Solution cache props contain no active dependency contract: $solutionPropsPath" + $auditIncomplete = $true + continue + } + + $solutionContractValid = $true + foreach ($propertyGroup in $activePropertyGroups) { + $schema = [string] $propertyGroup.CanarySharedVcpkgSchema + $dependencyFingerprint = [string] $propertyGroup.CanarySharedVcpkgDependencyFingerprint + $consumerFingerprint = [string] $propertyGroup.CanarySharedVcpkgConsumerFingerprint + $configurationName = [string] $propertyGroup.CanarySharedVcpkgConfiguration + $installedRoot = [string] $propertyGroup.VcpkgInstalledDir + $buildtreesRoot = [string] $propertyGroup.CanarySharedVcpkgBuildtreesRoot + $packagesRoot = [string] $propertyGroup.CanarySharedVcpkgPackagesRoot + + if ( + $schema -ne "v3" -or + $dependencyFingerprint -notmatch "^[0-9a-fA-F]{64}$" -or + $consumerFingerprint -notmatch "^[0-9a-fA-F]{64}$" -or + [string]::IsNullOrWhiteSpace($configurationName) + ) { + Write-Warning "Generated Solution cache props contain an invalid schema or fingerprint: $solutionPropsPath" + $auditIncomplete = $true + $solutionContractValid = $false + continue + } + + $shortFingerprint = $dependencyFingerprint.Substring(0, 24).ToLowerInvariant() + $expectedInstalledRoot = Get-FullPath -Path (Join-Path $CacheRoot "vcpkg-installed\$schema\$shortFingerprint") + $expectedBuildtreesRoot = Get-FullPath -Path (Join-Path $CacheRoot "vcpkg-buildtrees\$schema\$shortFingerprint") + $expectedPackagesRoot = Get-FullPath -Path (Join-Path $CacheRoot "vcpkg-packages\$schema\$shortFingerprint") + $exactRoots = + -not [string]::IsNullOrWhiteSpace($installedRoot) -and + -not [string]::IsNullOrWhiteSpace($buildtreesRoot) -and + -not [string]::IsNullOrWhiteSpace($packagesRoot) -and + (Get-FullPath -Path $installedRoot).Equals($expectedInstalledRoot, [StringComparison]::OrdinalIgnoreCase) -and + (Get-FullPath -Path $buildtreesRoot).Equals($expectedBuildtreesRoot, [StringComparison]::OrdinalIgnoreCase) -and + (Get-FullPath -Path $packagesRoot).Equals($expectedPackagesRoot, [StringComparison]::OrdinalIgnoreCase) + if ($exactRoots) { + $exactRoots = Test-SharedFingerprintIdentity -CacheRoot $CacheRoot -Schema $schema -Fingerprint $dependencyFingerprint + } + if ($exactRoots) { + try { + Assert-LocalFixedVolume -Path $installedRoot -Description "The Solution installed root" + Assert-LocalFixedVolume -Path $buildtreesRoot -Description "The Solution buildtrees root" + Assert-LocalFixedVolume -Path $packagesRoot -Description "The Solution packages root" + } catch { + $exactRoots = $false + } + } + if (-not $exactRoots) { + Write-Warning "Generated Solution cache props do not identify exact, local fingerprint roots with a matching full-hash identity: $solutionPropsPath" + $auditIncomplete = $true + $solutionContractValid = $false + } + + [pscustomobject]@{ + Worktree = $worktreeRoot + Configuration = $configurationName + Dependency = $dependencyFingerprint + Consumer = $consumerFingerprint + Installed = $installedRoot + } + } + + if ($AuditOnly -and $solutionContractValid) { + $solutionAuditScript = Join-Path $worktreeRoot "tools\configure_shared_solution_cache.ps1" + if ( + -not (Test-Path -LiteralPath $solutionAuditScript -PathType Leaf) -or + -not (Test-Path -LiteralPath $solutionDefinition.Project -PathType Leaf) -or + [string]::IsNullOrWhiteSpace($vcpkgVisualStudioPath) + ) { + Write-Warning "A generated Solution contract cannot be re-evaluated from its worktree: $solutionPropsPath" + $auditIncomplete = $true + continue + } + $solutionAuditConfigurations = @( + $activePropertyGroups | + ForEach-Object { [string] $_.CanarySharedVcpkgConfiguration } | + Select-Object -Unique + ) + $solutionAuditArguments = @( + "-NoLogo", + "-NoProfile", + "-NonInteractive", + "-File", + $solutionAuditScript, + "-AuditOnly", + "-ProjectFile", + $solutionDefinition.Project, + "-OutputProps", + $solutionPropsPath, + "-Configurations", + ($solutionAuditConfigurations -join ","), + "-CacheRoot", + $CacheRoot, + "-VcpkgRoot", + $vcpkgRoot, + "-VisualStudioPath", + $vcpkgVisualStudioPath + ) + if (-not [string]::IsNullOrWhiteSpace($SolutionMSBuildPath)) { + $solutionAuditArguments += @("-MSBuildPath", $SolutionMSBuildPath) + } + $solutionAuditOutput = @(& pwsh.exe @solutionAuditArguments 2>&1) + if ($LASTEXITCODE -ne 0) { + Write-Warning "Generated Solution cache props are stale or cannot be validated: $solutionPropsPath`n$($solutionAuditOutput -join [Environment]::NewLine)" + $auditIncomplete = $true + } + } +} + +if ($null -eq $solutionTrees) { + Write-Output "No generated Solution dependency contracts found." +} else { + $solutionTrees | Format-Table -AutoSize -Wrap | Out-String | Write-Output +} + +if ($AuditOnly) { + Write-Output "Audit only: no directories or environment variables were changed." + if ($auditIncomplete) { + throw "Audit incomplete: a repository is unregistered, unavailable, partially enumerated, or contains a malformed configure tree. Do not prune any shared-cache fingerprint." + } +} elseif ($WhatIfPreference) { + Write-Output "Preview only: -WhatIf prevented directory, registry, and environment changes." +} else { + Write-Output "Setup complete. Reconfigure each preset normally to adopt its content-addressed vcpkg installation." +} +} finally { + if ($null -ne $operationLock) { + $operationLock.Dispose() + } +} diff --git a/tools/configure_shared_solution_cache.ps1 b/tools/configure_shared_solution_cache.ps1 new file mode 100644 index 000000000..06a1ed6be --- /dev/null +++ b/tools/configure_shared_solution_cache.ps1 @@ -0,0 +1,724 @@ +#Requires -Version 7.2 + +[CmdletBinding(SupportsShouldProcess = $true)] +param( + [ValidateNotNullOrEmpty()] + [string] $Configuration = "All", + [string[]] $Configurations = @(), + [string] $Platform = "x64", + [string] $ProjectFile, + [string] $OutputProps, + [string] $CacheRoot, + [string] $VcpkgRoot, + [string] $VisualStudioPath, + [string] $CMakePath, + [string] $MSBuildPath, + [string] $FingerprintInputDirectory, + [switch] $ValidateOnly, + [switch] $AuditOnly, + [string] $ExpectedDependencyFingerprint, + [string] $ExpectedConsumerFingerprint, + [string] $ExpectedInstalledRoot +) + +Set-StrictMode -Version Latest +$ErrorActionPreference = "Stop" + +function Get-FullPath { + param( + [Parameter(Mandatory = $true)] + [string] $Path, + [string] $BasePath = (Get-Location).Path + ) + + $fullPath = if ([IO.Path]::IsPathRooted($Path)) { + [IO.Path]::GetFullPath($Path) + } else { + [IO.Path]::GetFullPath((Join-Path $BasePath $Path)) + } + $pathRoot = [IO.Path]::GetPathRoot($fullPath) + if ($fullPath -eq $pathRoot) { + return $fullPath + } + + return $fullPath.TrimEnd( + [IO.Path]::DirectorySeparatorChar, + [IO.Path]::AltDirectorySeparatorChar + ) +} + +function Get-EnvironmentValue { + param([Parameter(Mandatory = $true)][string] $Name) + + foreach ($scope in @("Process", "User", "Machine")) { + $value = [Environment]::GetEnvironmentVariable($Name, $scope) + if (-not [string]::IsNullOrWhiteSpace($value)) { + return $value + } + } + + return $null +} + +function Assert-LocalFixedVolume { + param( + [Parameter(Mandatory = $true)] + [string] $Path, + [Parameter(Mandatory = $true)] + [string] $Description + ) + + $fullPath = Get-FullPath -Path $Path + $existingPath = $fullPath + while (-not (Test-Path -LiteralPath $existingPath)) { + $parentPath = Split-Path -Parent $existingPath + if ([string]::IsNullOrWhiteSpace($parentPath) -or $parentPath -eq $existingPath) { + throw "$Description has no verifiable existing filesystem ancestor: $fullPath" + } + $existingPath = $parentPath + } + while (-not [string]::IsNullOrWhiteSpace($existingPath)) { + $existingItem = Get-Item -LiteralPath $existingPath -Force + if ($existingItem.Attributes -band [IO.FileAttributes]::ReparsePoint) { + throw "$Description traverses a reparse point and cannot prove local filesystem semantics: $existingPath" + } + $parentPath = Split-Path -Parent $existingPath + if ([string]::IsNullOrWhiteSpace($parentPath) -or $parentPath -eq $existingPath) { + break + } + $existingPath = $parentPath + } + + $pathRoot = [IO.Path]::GetPathRoot($fullPath) + if ([string]::IsNullOrWhiteSpace($pathRoot)) { + throw "$Description does not resolve to a filesystem volume: $fullPath" + } + $drive = [IO.DriveInfo]::new($pathRoot) + if (-not $drive.IsReady -or $drive.DriveType -ne [IO.DriveType]::Fixed) { + throw "$Description must use a ready local fixed volume: $fullPath" + } +} + +function Resolve-Executable { + param( + [string] $Candidate, + [Parameter(Mandatory = $true)] + [string] $Name + ) + + if (-not [string]::IsNullOrWhiteSpace($Candidate)) { + $candidatePath = Get-FullPath -Path $Candidate + if (Test-Path -LiteralPath $candidatePath -PathType Leaf) { + return $candidatePath + } + throw "$Name was not found: $candidatePath" + } + + $command = Get-Command $Name -ErrorAction SilentlyContinue | Select-Object -First 1 + if ($null -ne $command) { + return Get-FullPath -Path $command.Source + } + + return $null +} + +function Assert-MSBuildPropertyQuerySupport { + param( + [Parameter(Mandatory = $true)] + [string] $Executable + ) + + $versionOutput = @(& $Executable -nologo -version 2>&1) + $msbuildExitCode = $LASTEXITCODE + if ($msbuildExitCode -ne 0) { + throw "MSBuild version detection failed for $Executable with exit code $msbuildExitCode." + } + + $versionMatch = [regex]::Match( + ($versionOutput -join [Environment]::NewLine), + '(?m)^\s*(\d+\.\d+(?:\.\d+){0,2})\s*$' + ) + if (-not $versionMatch.Success) { + throw "MSBuild did not report a recognizable version: $Executable" + } + + $msbuildVersion = [version] $versionMatch.Groups[1].Value + if ($msbuildVersion -lt [version] "17.8") { + throw "MSBuild 17.8 or newer is required for evaluated property queries. Found $msbuildVersion at $Executable." + } +} + +function Assert-SafeMSBuildDimension { + param( + [Parameter(Mandatory = $true)] + [string] $Value, + [Parameter(Mandatory = $true)] + [string] $Description + ) + + if ( + [string]::IsNullOrWhiteSpace($Value) -or + $Value.Length -gt 128 -or + $Value -notmatch '^[A-Za-z0-9_. -]+$' + ) { + throw "Unsupported $Description name: $Value" + } +} + +function Import-VisualStudioEnvironment { + param( + [Parameter(Mandatory = $true)] + [string] $Root, + [Parameter(Mandatory = $true)] + [string] $PreservedVcpkgRoot + ) + + $developerCommand = Join-Path $Root "Common7\Tools\VsDevCmd.bat" + if (-not (Test-Path -LiteralPath $developerCommand -PathType Leaf)) { + throw "Visual Studio developer environment was not found: $developerCommand" + } + + $environmentLines = & cmd.exe /d /s /c "`"$developerCommand`" -no_logo -arch=x64 -host_arch=x64 && set" + if ($LASTEXITCODE -ne 0) { + throw "Visual Studio developer environment initialization failed with exit code $LASTEXITCODE." + } + + foreach ($line in $environmentLines) { + $separator = $line.IndexOf("=") + if ($separator -le 0) { + continue + } + [Environment]::SetEnvironmentVariable( + $line.Substring(0, $separator), + $line.Substring($separator + 1), + "Process" + ) + } + + # VsDevCmd may select the Visual Studio bundled vcpkg. The repository contract + # owns the active vcpkg root, so restore it after importing compiler variables. + [Environment]::SetEnvironmentVariable("VCPKG_ROOT", $PreservedVcpkgRoot, "Process") +} + +function Get-MSBuildProperties { + param( + [Parameter(Mandatory = $true)] + [string] $Executable, + [Parameter(Mandatory = $true)] + [string] $Project, + [Parameter(Mandatory = $true)] + [string] $BuildConfiguration, + [Parameter(Mandatory = $true)] + [string] $BuildPlatform + ) + + $propertyNames = @( + "VcpkgRoot", + "VcpkgManifestRoot", + "VcpkgTriplet", + "VcpkgHostTriplet", + "VcpkgConfiguration", + "VcpkgEnableManifest", + "PlatformToolset", + "WindowsTargetPlatformVersion", + "CanaryVcpkgBuildType", + "CanaryVcpkgFeatureFlags", + "CanaryVcpkgManifestFeatures", + "CanaryVcpkgManifestNoDefaultFeatures", + "CanaryVcpkgOverlayPorts", + "CanaryVcpkgOverlayTriplets", + "CanaryVcpkgInstallOptions" + ) -join "," + + $arguments = @( + $Project, + "-nologo", + "-nodeReuse:false", + "-p:Configuration=$BuildConfiguration", + "-p:Platform=$BuildPlatform", + "-p:CanarySharedVcpkgDisable=true", + "-getProperty:$propertyNames" + ) + $output = & $Executable @arguments 2>&1 + if ($LASTEXITCODE -ne 0) { + throw "MSBuild property evaluation failed for $BuildConfiguration|$BuildPlatform.`n$($output -join [Environment]::NewLine)" + } + + $json = $output -join [Environment]::NewLine + $jsonStart = $json.IndexOf("{") + if ($jsonStart -lt 0) { + throw "MSBuild did not return its evaluated properties as JSON." + } + + return ($json.Substring($jsonStart) | ConvertFrom-Json).Properties +} + +function Convert-ContractResult { + param([Parameter(Mandatory = $true)][string] $Path) + + $result = [ordered]@{} + foreach ($line in Get-Content -LiteralPath $Path) { + $separator = $line.IndexOf("=") + if ($separator -le 0) { + continue + } + $result[$line.Substring(0, $separator)] = $line.Substring($separator + 1) + } + + foreach ($requiredName in @( + "active", + "schema", + "implementation-sha256", + "dependency-fingerprint", + "consumer-fingerprint", + "installed-root", + "buildtrees-root", + "packages-root", + "target-triplet", + "host-triplet" + )) { + if (-not $result.Contains($requiredName) -or [string]::IsNullOrWhiteSpace($result[$requiredName])) { + throw "The shared-cache resolver omitted '$requiredName'." + } + } + + return $result +} + +function ConvertTo-MSBuildOptionString { + param( + [Parameter(Mandatory = $true)] + [Collections.IDictionary] $Contract, + [Parameter(Mandatory = $true)] + [pscustomobject] $Properties + ) + + $options = [Collections.Generic.List[string]]::new() + if (-not [string]::IsNullOrWhiteSpace($Properties.CanaryVcpkgFeatureFlags)) { + $featureFlags = $Properties.CanaryVcpkgFeatureFlags.Replace(";", ",") + $options.Add("--feature-flags=$featureFlags") + } + foreach ($feature in @($Properties.CanaryVcpkgManifestFeatures -split ";" | Where-Object { $_ })) { + $options.Add("--x-feature=$feature") + } + if ($Properties.CanaryVcpkgManifestNoDefaultFeatures -match "^(1|ON|TRUE|YES)$") { + $options.Add("--x-no-default-features") + } + foreach ($overlayPort in @($Properties.CanaryVcpkgOverlayPorts -split ";" | Where-Object { $_ })) { + $options.Add("`"--overlay-ports=$overlayPort`"") + } + foreach ($overlayTriplet in @($Properties.CanaryVcpkgOverlayTriplets -split ";" | Where-Object { $_ })) { + $options.Add("`"--overlay-triplets=$overlayTriplet`"") + } + foreach ($installOption in @($Properties.CanaryVcpkgInstallOptions -split ";" | Where-Object { $_ })) { + $options.Add($installOption) + } + $options.Add("`"--x-buildtrees-root=$($Contract['buildtrees-root'])`"") + $options.Add("`"--x-packages-root=$($Contract['packages-root'])`"") + + return $options -join " " +} + +function Invoke-ContractResolver { + param( + [Parameter(Mandatory = $true)] + [string] $BuildConfiguration, + [Parameter(Mandatory = $true)] + [pscustomobject] $Properties, + [Parameter(Mandatory = $true)] + [bool] $Prepare + ) + + if ($Properties.VcpkgEnableManifest -notmatch "^(1|ON|TRUE|YES)$") { + throw "The Solution configuration $BuildConfiguration|$Platform does not enable vcpkg manifest mode." + } + foreach ($requiredProperty in @("VcpkgTriplet", "VcpkgHostTriplet", "PlatformToolset")) { + if ([string]::IsNullOrWhiteSpace($Properties.$requiredProperty)) { + throw "The Solution configuration $BuildConfiguration|$Platform does not define $requiredProperty." + } + } + + $manifestRoot = Get-FullPath -Path $Properties.VcpkgManifestRoot -BasePath $repositoryRoot + if ($manifestRoot -ne $repositoryRoot) { + throw "The evaluated vcpkg manifest root does not match this repository: $manifestRoot" + } + + $resultPath = Join-Path ([IO.Path]::GetTempPath()) ("canary-shared-contract-{0}.txt" -f [guid]::NewGuid()) + $fingerprintInputPath = $null + try { + $resolverArguments = [Collections.Generic.List[string]]::new() + if ($Prepare) { + $resolverArguments.Add("-DCANARY_SHARED_CACHE_PREPARE_ONLY=ON") + } else { + $resolverArguments.Add("-DCANARY_SHARED_CACHE_READ_ONLY=ON") + } + foreach ($argument in @( + "-DCANARY_SHARED_CACHE_RESULT_FILE=$resultPath", + "-DCANARY_SHARED_CACHE_CONSUMER=msbuild", + "-DCANARY_SHARED_CACHE_CONSUMER_TOOL=$resolvedMSBuildPath", + "-DCANARY_SHARED_CACHE_CONSUMER_CONFIGURATION=$BuildConfiguration", + "-DCANARY_SHARED_CACHE_CONSUMER_PLATFORM=$Platform", + "-DCANARY_SHARED_CACHE_CONSUMER_LINK_CONFIGURATION=$($Properties.VcpkgConfiguration)", + "-DCANARY_SHARED_CACHE_CONSUMER_TOOLSET=$($Properties.PlatformToolset)", + "-DCANARY_SHARED_CACHE_CONSUMER_SDK=$($Properties.WindowsTargetPlatformVersion)", + "-DCMAKE_TOOLCHAIN_FILE=$resolvedVcpkgRoot/scripts/buildsystems/vcpkg.cmake", + "-DCMAKE_C_COMPILER=$compilerPath", + "-DCMAKE_CXX_COMPILER=$compilerPath", + "-DVCPKG_MANIFEST_DIR=$manifestRoot", + "-DVCPKG_TARGET_TRIPLET=$($Properties.VcpkgTriplet)", + "-DVCPKG_HOST_TRIPLET=$($Properties.VcpkgHostTriplet)", + "-DVCPKG_PLATFORM_TOOLSET=$($Properties.PlatformToolset)", + "-DVCPKG_FEATURE_FLAGS=$($Properties.CanaryVcpkgFeatureFlags)", + "-DVCPKG_MANIFEST_FEATURES=$($Properties.CanaryVcpkgManifestFeatures)", + "-DVCPKG_OVERLAY_PORTS=$($Properties.CanaryVcpkgOverlayPorts)", + "-DVCPKG_OVERLAY_TRIPLETS=$($Properties.CanaryVcpkgOverlayTriplets)", + "-DVCPKG_INSTALL_OPTIONS=$($Properties.CanaryVcpkgInstallOptions)" + )) { + $resolverArguments.Add($argument) + } + if (-not [string]::IsNullOrWhiteSpace($FingerprintInputDirectory)) { + $resolvedFingerprintInputDirectory = Get-FullPath -Path $FingerprintInputDirectory -BasePath $repositoryRoot + [IO.Directory]::CreateDirectory($resolvedFingerprintInputDirectory) | Out-Null + $fingerprintInputPath = Join-Path $resolvedFingerprintInputDirectory ("{0}-{1}.txt" -f $BuildConfiguration.ToLowerInvariant(), $Platform.ToLowerInvariant()) + $resolverArguments.Add("-DCANARY_SHARED_CACHE_FINGERPRINT_INPUT_FILE=$fingerprintInputPath") + } + if (-not [string]::IsNullOrWhiteSpace($Properties.CanaryVcpkgBuildType)) { + $resolverArguments.Add("-DVCPKG_BUILD_TYPE=$($Properties.CanaryVcpkgBuildType)") + } + if (-not [string]::IsNullOrWhiteSpace($Properties.CanaryVcpkgManifestNoDefaultFeatures)) { + $resolverArguments.Add("-DVCPKG_MANIFEST_NO_DEFAULT_FEATURES=$($Properties.CanaryVcpkgManifestNoDefaultFeatures)") + } + $resolverArguments.Add("-P") + $resolverArguments.Add($resolverModule) + + Push-Location $repositoryRoot + try { + $resolverOutput = & $resolvedCMakePath @resolverArguments 2>&1 + if ($LASTEXITCODE -ne 0) { + throw "Shared-cache contract resolution failed for $BuildConfiguration|$Platform.`n$($resolverOutput -join [Environment]::NewLine)" + } + } finally { + Pop-Location + } + + if (-not (Test-Path -LiteralPath $resultPath -PathType Leaf)) { + throw "The shared-cache resolver did not produce a result for $BuildConfiguration|$Platform.`n$($resolverOutput -join [Environment]::NewLine)" + } + return Convert-ContractResult -Path $resultPath + } finally { + if ([IO.File]::Exists($resultPath)) { + [IO.File]::Delete($resultPath) + } + } +} + +$repositoryRoot = Get-FullPath -Path (Join-Path $PSScriptRoot "..") +$resolverModule = Join-Path $repositoryRoot "cmake\SharedBuildCache.cmake" +if (-not (Test-Path -LiteralPath $resolverModule -PathType Leaf)) { + throw "Shared-cache resolver was not found: $resolverModule" +} + +if ([string]::IsNullOrWhiteSpace($ProjectFile)) { + foreach ($relativeProjectPath in @( + "vcproj\canary.vcxproj", + "vc18\otclient.vcxproj", + "vc17\otclient.vcxproj" + )) { + $projectCandidate = Join-Path $repositoryRoot $relativeProjectPath + if (Test-Path -LiteralPath $projectCandidate -PathType Leaf) { + $ProjectFile = $projectCandidate + break + } + } +} +if ([string]::IsNullOrWhiteSpace($ProjectFile)) { + throw "No maintained Visual Studio project was found. Pass -ProjectFile explicitly." +} +$ProjectFile = Get-FullPath -Path $ProjectFile -BasePath $repositoryRoot +if (-not (Test-Path -LiteralPath $ProjectFile -PathType Leaf)) { + throw "Visual Studio project was not found: $ProjectFile" +} + +if ([string]::IsNullOrWhiteSpace($OutputProps)) { + $OutputProps = Join-Path (Split-Path -Parent $ProjectFile) ".canary-shared-cache\SharedVcpkgCache.props" +} +$OutputProps = Get-FullPath -Path $OutputProps -BasePath $repositoryRoot + +if ([string]::IsNullOrWhiteSpace($CacheRoot)) { + $CacheRoot = Get-EnvironmentValue -Name "CANARY_SHARED_CACHE_ROOT" +} +if ([string]::IsNullOrWhiteSpace($CacheRoot)) { + throw "CANARY_SHARED_CACHE_ROOT is not configured. Run tools/configure_shared_build_cache.ps1 first." +} +$CacheRoot = Get-FullPath -Path $CacheRoot +Assert-LocalFixedVolume -Path $CacheRoot -Description "The shared cache" +[Environment]::SetEnvironmentVariable("CANARY_SHARED_CACHE_ROOT", $CacheRoot, "Process") +[Environment]::SetEnvironmentVariable("CANARY_SHARED_CACHE_LOCAL_FILESYSTEM_VERIFIED", "ON", "Process") +[Environment]::SetEnvironmentVariable("CANARY_SHARED_CACHE_VERIFIED_ROOT", $CacheRoot, "Process") + +if ([string]::IsNullOrWhiteSpace($VcpkgRoot)) { + $VcpkgRoot = Get-EnvironmentValue -Name "VCPKG_ROOT" +} +if ([string]::IsNullOrWhiteSpace($VcpkgRoot)) { + throw "VCPKG_ROOT is not configured." +} +$resolvedVcpkgRoot = Get-FullPath -Path $VcpkgRoot +if (-not (Test-Path -LiteralPath (Join-Path $resolvedVcpkgRoot "vcpkg.exe") -PathType Leaf)) { + throw "The active vcpkg executable was not found below: $resolvedVcpkgRoot" +} +[Environment]::SetEnvironmentVariable("VCPKG_ROOT", $resolvedVcpkgRoot, "Process") + +if ([string]::IsNullOrWhiteSpace($VisualStudioPath)) { + $VisualStudioPath = Get-EnvironmentValue -Name "CANARY_VCPKG_VISUAL_STUDIO_PATH" +} +if ([string]::IsNullOrWhiteSpace($VisualStudioPath)) { + throw "CANARY_VCPKG_VISUAL_STUDIO_PATH is not configured. Run tools/configure_shared_build_cache.ps1 first." +} +$VisualStudioPath = Get-FullPath -Path $VisualStudioPath +[Environment]::SetEnvironmentVariable("CANARY_VCPKG_VISUAL_STUDIO_PATH", $VisualStudioPath, "Process") +Import-VisualStudioEnvironment -Root $VisualStudioPath -PreservedVcpkgRoot $resolvedVcpkgRoot + +$resolvedCMakePath = Resolve-Executable -Candidate $CMakePath -Name "cmake.exe" +if ([string]::IsNullOrWhiteSpace($resolvedCMakePath)) { + $bundledCMake = Join-Path $VisualStudioPath "Common7\IDE\CommonExtensions\Microsoft\CMake\CMake\bin\cmake.exe" + if (Test-Path -LiteralPath $bundledCMake -PathType Leaf) { + $resolvedCMakePath = Get-FullPath -Path $bundledCMake + } +} +if ([string]::IsNullOrWhiteSpace($resolvedCMakePath)) { + throw "cmake.exe was not found on PATH or in the selected Visual Studio installation. Pass -CMakePath explicitly." +} +if ([string]::IsNullOrWhiteSpace($MSBuildPath)) { + $MSBuildPath = Get-EnvironmentValue -Name "CANARY_SOLUTION_MSBUILD_PATH" +} +$resolvedMSBuildPath = Resolve-Executable -Candidate $MSBuildPath -Name "MSBuild.exe" +if ([string]::IsNullOrWhiteSpace($resolvedMSBuildPath)) { + $bundledMSBuild = Join-Path $VisualStudioPath "MSBuild\Current\Bin\amd64\MSBuild.exe" + if (Test-Path -LiteralPath $bundledMSBuild -PathType Leaf) { + $resolvedMSBuildPath = Get-FullPath -Path $bundledMSBuild + } +} +if ([string]::IsNullOrWhiteSpace($resolvedMSBuildPath)) { + throw "MSBuild.exe was not found on PATH or in the selected Visual Studio installation. Pass -MSBuildPath explicitly." +} +Assert-MSBuildPropertyQuerySupport -Executable $resolvedMSBuildPath +$compilerCommand = Get-Command cl.exe -ErrorAction SilentlyContinue | Select-Object -First 1 +if ($null -eq $compilerCommand) { + throw "cl.exe was not found after initializing the selected Visual Studio developer environment." +} +$compilerPath = $compilerCommand.Source + +$Platform = ([string] $Platform).Trim() +Assert-SafeMSBuildDimension -Value $Platform -Description "Solution platform" + +[string[]] $configurations = if ($Configurations.Count -gt 0) { + @($Configurations) +} elseif ($Configuration -eq "All") { + try { + [xml] $projectConfigurationDocument = Get-Content -LiteralPath $ProjectFile -Raw + } catch { + throw "The Visual Studio project is malformed and its configurations cannot be discovered: $ProjectFile. $($_.Exception.Message)" + } + @( + $projectConfigurationDocument.SelectNodes("//*[local-name()='ProjectConfiguration']") | + ForEach-Object { [string] $_.Include } | + Where-Object { $_.EndsWith("|$Platform", [StringComparison]::OrdinalIgnoreCase) } | + ForEach-Object { ($_ -split '\|', 2)[0] } | + Select-Object -Unique + ) +} else { + @($Configuration) +} +$configurations = @( + $configurations | + ForEach-Object { $_ -split '[,;]' } | + ForEach-Object { $_.Trim() } | + Where-Object { -not [string]::IsNullOrWhiteSpace($_) } | + Select-Object -Unique +) +if ($configurations.Count -eq 0) { + throw "The Visual Studio project declares no configurations for platform $Platform." +} +foreach ($configurationName in $configurations) { + Assert-SafeMSBuildDimension -Value $configurationName -Description "Solution configuration" +} + +if ($AuditOnly) { + if (-not (Test-Path -LiteralPath $OutputProps -PathType Leaf)) { + Write-Output "Solution cache fallback is local; no generated shared-cache props exist." + return + } + try { + [xml] $existingProps = Get-Content -LiteralPath $OutputProps -Raw + } catch { + throw "Generated Solution cache props are malformed: $OutputProps. Regenerate the file and reload the project. $($_.Exception.Message)" + } + $activePropertyGroups = @( + $existingProps.SelectNodes("/*[local-name()='Project']/*[local-name()='PropertyGroup']") | + Where-Object { $_.CanarySharedVcpkgActive -eq "true" } + ) + foreach ($expectedConfiguration in $configurations) { + $matchingGroups = @( + $activePropertyGroups | + Where-Object { $_.CanarySharedVcpkgConfiguration -eq $expectedConfiguration } + ) + if ($matchingGroups.Count -ne 1) { + throw "Generated Solution cache props must contain exactly one current contract for $expectedConfiguration|$Platform. Regenerate the file and reload the project." + } + } + foreach ($propertyGroup in $activePropertyGroups) { + if ($propertyGroup.CanarySharedVcpkgActive -ne "true") { + continue + } + $configurationName = [string] $propertyGroup.CanarySharedVcpkgConfiguration + $properties = Get-MSBuildProperties -Executable $resolvedMSBuildPath -Project $ProjectFile -BuildConfiguration $configurationName -BuildPlatform $Platform + $contract = Invoke-ContractResolver -BuildConfiguration $configurationName -Properties $properties -Prepare $false + if ( + $contract["dependency-fingerprint"] -ne [string] $propertyGroup.CanarySharedVcpkgDependencyFingerprint -or + $contract["consumer-fingerprint"] -ne [string] $propertyGroup.CanarySharedVcpkgConsumerFingerprint -or + (Get-FullPath -Path $contract["installed-root"]) -ne (Get-FullPath -Path ([string] $propertyGroup.VcpkgInstalledDir)) + ) { + throw "Generated Solution cache props are stale for $configurationName|$Platform. Run this script without -AuditOnly and reload the project." + } + } + Write-Output "Solution cache audit complete: all generated contracts are current." + return +} + +if ($ValidateOnly) { + foreach ($requiredExpected in @( + @{ Name = "ExpectedDependencyFingerprint"; Value = $ExpectedDependencyFingerprint }, + @{ Name = "ExpectedConsumerFingerprint"; Value = $ExpectedConsumerFingerprint }, + @{ Name = "ExpectedInstalledRoot"; Value = $ExpectedInstalledRoot } + )) { + if ([string]::IsNullOrWhiteSpace($requiredExpected.Value)) { + throw "$($requiredExpected.Name) is required with -ValidateOnly." + } + } + if (@($configurations).Count -ne 1) { + throw "-ValidateOnly requires one concrete Configuration." + } + $properties = Get-MSBuildProperties -Executable $resolvedMSBuildPath -Project $ProjectFile -BuildConfiguration $configurations[0] -BuildPlatform $Platform + $contract = Invoke-ContractResolver -BuildConfiguration $configurations[0] -Properties $properties -Prepare $true + $actualValues = [ordered]@{ + ManifestRoot = [Environment]::GetEnvironmentVariable("CANARY_SHARED_VCPKG_ACTUAL_MANIFEST_ROOT", "Process") + TargetTriplet = [Environment]::GetEnvironmentVariable("CANARY_SHARED_VCPKG_ACTUAL_TARGET_TRIPLET", "Process") + HostTriplet = [Environment]::GetEnvironmentVariable("CANARY_SHARED_VCPKG_ACTUAL_HOST_TRIPLET", "Process") + Configuration = [Environment]::GetEnvironmentVariable("CANARY_SHARED_VCPKG_ACTUAL_CONFIGURATION", "Process") + Toolset = [Environment]::GetEnvironmentVariable("CANARY_SHARED_VCPKG_ACTUAL_TOOLSET", "Process") + SDK = [Environment]::GetEnvironmentVariable("CANARY_SHARED_VCPKG_ACTUAL_SDK", "Process") + InstallOptions = [Environment]::GetEnvironmentVariable("CANARY_SHARED_VCPKG_ACTUAL_INSTALL_OPTIONS", "Process") + } + foreach ($actualValue in $actualValues.GetEnumerator()) { + if ([string]::IsNullOrWhiteSpace($actualValue.Value)) { + throw "The evaluated MSBuild value '$($actualValue.Key)' was not supplied to the canonical validator." + } + } + if ((Get-FullPath -Path $actualValues.ManifestRoot) -ne (Get-FullPath -Path $properties.VcpkgManifestRoot -BasePath $repositoryRoot)) { + throw "The evaluated vcpkg manifest root differs from the canonical Solution contract." + } + $canonicalValues = [ordered]@{ + TargetTriplet = [string] $contract["target-triplet"] + HostTriplet = [string] $contract["host-triplet"] + Configuration = [string] $properties.VcpkgConfiguration + Toolset = [string] $properties.PlatformToolset + SDK = [string] $properties.WindowsTargetPlatformVersion + InstallOptions = ConvertTo-MSBuildOptionString -Contract $contract -Properties $properties + } + foreach ($canonicalValue in $canonicalValues.GetEnumerator()) { + if ([string] $actualValues[$canonicalValue.Key] -cne [string] $canonicalValue.Value) { + throw "The evaluated MSBuild value '$($canonicalValue.Key)' differs from the canonical Solution contract." + } + } + if ($contract["dependency-fingerprint"] -ne $ExpectedDependencyFingerprint) { + throw "The Solution dependency fingerprint changed. Regenerate SharedVcpkgCache.props and reload the project." + } + if ($contract["consumer-fingerprint"] -ne $ExpectedConsumerFingerprint) { + throw "The Solution consumer fingerprint changed. Regenerate SharedVcpkgCache.props and reload the project." + } + if ((Get-FullPath -Path $contract["installed-root"]) -ne (Get-FullPath -Path $ExpectedInstalledRoot)) { + throw "The Solution installed root does not match its validated fingerprint." + } + Write-Output "Validated shared Solution dependency contract $($contract['dependency-fingerprint'])." + return +} + +$resolvedContracts = [Collections.Generic.List[object]]::new() +foreach ($buildConfiguration in $configurations) { + $properties = Get-MSBuildProperties -Executable $resolvedMSBuildPath -Project $ProjectFile -BuildConfiguration $buildConfiguration -BuildPlatform $Platform + $contract = Invoke-ContractResolver -BuildConfiguration $buildConfiguration -Properties $properties -Prepare (-not $WhatIfPreference) + $resolvedContracts.Add([pscustomobject]@{ + Configuration = $buildConfiguration + Properties = $properties + Contract = $contract + Options = ConvertTo-MSBuildOptionString -Contract $contract -Properties $properties + }) +} + +$propertyGroups = foreach ($entry in $resolvedContracts) { + $condition = "'`$(Configuration)|`$(Platform)' == '$($entry.Configuration)|$Platform'" + $escapedCondition = [Security.SecurityElement]::Escape($condition) + $values = [ordered]@{ + CanarySharedVcpkgActive = "true" + CanarySharedCacheRoot = $CacheRoot + CanarySharedVcpkgSchema = $entry.Contract["schema"] + CanarySharedVcpkgConfiguration = $entry.Configuration + CanarySharedVcpkgDependencyFingerprint = $entry.Contract["dependency-fingerprint"] + CanarySharedVcpkgConsumerFingerprint = $entry.Contract["consumer-fingerprint"] + CanarySharedVcpkgExpectedManifestRoot = $repositoryRoot + CanarySharedVcpkgExpectedTargetTriplet = $entry.Contract["target-triplet"] + CanarySharedVcpkgExpectedHostTriplet = $entry.Contract["host-triplet"] + CanarySharedVcpkgExpectedConfiguration = $entry.Properties.VcpkgConfiguration + CanarySharedVcpkgExpectedToolset = $entry.Properties.PlatformToolset + CanarySharedVcpkgExpectedSDK = $entry.Properties.WindowsTargetPlatformVersion + CanarySharedVcpkgExpectedInstallOptions = $entry.Options + CanarySharedVcpkgBuildtreesRoot = $entry.Contract["buildtrees-root"] + CanarySharedVcpkgPackagesRoot = $entry.Contract["packages-root"] + CanaryVcpkgVisualStudioPath = $VisualStudioPath + CanaryVcpkgDependencyCMakeDirectory = [IO.Path]::GetDirectoryName($resolvedCMakePath) + VcpkgRoot = $resolvedVcpkgRoot + VcpkgInstalledDir = $entry.Contract["installed-root"] + VcpkgAdditionalInstallOptions = $entry.Options + } + $lines = foreach ($value in $values.GetEnumerator()) { + $escapedValue = [Security.SecurityElement]::Escape([string] $value.Value) + " <$($value.Key)>$escapedValue" + } + " `n$($lines -join "`n")`n " +} +$propsContent = @" + + + +$($propertyGroups -join "`n") + +"@ + +if ($PSCmdlet.ShouldProcess($OutputProps, "write generated shared Solution cache props")) { + $propsDirectory = Split-Path -Parent $OutputProps + [void] (New-Item -ItemType Directory -Path $propsDirectory -Force) + $temporaryProps = Join-Path $propsDirectory ("SharedVcpkgCache.{0}.tmp" -f [guid]::NewGuid()) + try { + [IO.File]::WriteAllText($temporaryProps, $propsContent, [Text.UTF8Encoding]::new($false)) + [IO.File]::Move($temporaryProps, $OutputProps, $true) + } finally { + if ([IO.File]::Exists($temporaryProps)) { + [IO.File]::Delete($temporaryProps) + } + } +} + +$resolvedContracts | ForEach-Object { + [pscustomobject]@{ + Configuration = "$($_.Configuration)|$Platform" + Dependency = $_.Contract["dependency-fingerprint"] + Consumer = $_.Contract["consumer-fingerprint"] + Installed = $_.Contract["installed-root"] + } +} | Format-Table -AutoSize -Wrap | Out-String | Write-Output + +if ($WhatIfPreference) { + Write-Output "Preview only: no pool directories or generated props were changed." +} else { + Write-Output "Shared Solution cache props generated. Reload the Visual Studio project before building." +} diff --git a/vc18/SharedVcpkgCache.targets b/vc18/SharedVcpkgCache.targets new file mode 100644 index 000000000..5acb6ff9b --- /dev/null +++ b/vc18/SharedVcpkgCache.targets @@ -0,0 +1,77 @@ + + + + + + <_CanaryVcpkgManifestFeature Include="$(CanaryVcpkgManifestFeatures)" Condition="'$(CanaryVcpkgManifestFeatures)'!=''" /> + <_CanaryVcpkgOverlayPort Include="$(CanaryVcpkgOverlayPorts)" Condition="'$(CanaryVcpkgOverlayPorts)'!=''" /> + <_CanaryVcpkgOverlayTriplet Include="$(CanaryVcpkgOverlayTriplets)" Condition="'$(CanaryVcpkgOverlayTriplets)'!=''" /> + <_CanaryVcpkgInstallOption Include="$(CanaryVcpkgInstallOptions)" Condition="'$(CanaryVcpkgInstallOptions)'!=''" /> + + + <_CanaryVcpkgFeatureFlagOptions Condition="'$(CanaryVcpkgFeatureFlags)'!=''">--feature-flags=$([System.String]::Copy('$(CanaryVcpkgFeatureFlags)').Replace(';', ',')) + <_CanaryVcpkgManifestFeatureOptions>@(_CanaryVcpkgManifestFeature->'--x-feature=%(Identity)', ' ') + <_CanaryVcpkgNoDefaultFeatureOptions Condition="'$(CanaryVcpkgManifestNoDefaultFeatures)'=='1' Or '$(CanaryVcpkgManifestNoDefaultFeatures)'=='ON' Or '$(CanaryVcpkgManifestNoDefaultFeatures)'=='TRUE' Or '$(CanaryVcpkgManifestNoDefaultFeatures)'=='YES'">--x-no-default-features + <_CanaryVcpkgOverlayPortOptions>@(_CanaryVcpkgOverlayPort->'"--overlay-ports=%(Identity)"', ' ') + <_CanaryVcpkgOverlayTripletOptions>@(_CanaryVcpkgOverlayTriplet->'"--overlay-triplets=%(Identity)"', ' ') + <_CanaryVcpkgInstallOptions>@(_CanaryVcpkgInstallOption, ' ') + $(_CanaryVcpkgFeatureFlagOptions) $(_CanaryVcpkgManifestFeatureOptions) $(_CanaryVcpkgNoDefaultFeatureOptions) $(_CanaryVcpkgOverlayPortOptions) $(_CanaryVcpkgOverlayTripletOptions) $(_CanaryVcpkgInstallOptions) "--x-buildtrees-root=$(MSBuildProjectDirectory)\.vcpkg-buildtrees\$(Configuration)-$(Platform)" "--x-packages-root=$(MSBuildProjectDirectory)\.vcpkg-packages\$(Configuration)-$(Platform)" + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/vc18/otclient.vcxproj b/vc18/otclient.vcxproj index 9e8be7fc3..c6238fd92 100644 --- a/vc18/otclient.vcxproj +++ b/vc18/otclient.vcxproj @@ -71,6 +71,12 @@ true + + $(VCPKG_ROOT) + $(MSBuildThisFileDirectory)..\vcpkg + $(MSBuildThisFileDirectory)..\..\..\vcpkg + + @@ -156,7 +162,10 @@ true true - --overlay-triplets="$(MSBuildProjectDirectory)\..\cmake\triplets" --binarysource=clear --binarysource=files,$(VcpkgRoot)\binary-cache,readwrite + versions + release + $(MSBuildProjectDirectory)\..\cmake\triplets + --clean-packages-after-build;--clean-buildtrees-after-build x64-windows @@ -168,6 +177,8 @@ x64-windows-static $(MSBuildProjectDirectory)\..\vcpkg_installed\msbuild-x64-debug + $(PREPROCESSOR_DEFS);_SCL_SECURE_NO_WARNINGS;%(PreprocessorDefinitions) @@ -749,32 +760,29 @@ - - $(VcpkgInstalledDir)\$(VcpkgTriplet)\tools\protobuf\protoc.exe - $(SourcePath)protobuf + + $(VcpkgInstalledDir)\$(VcpkgHostTriplet)\tools\protobuf\protoc.exe + $([System.IO.Path]::GetFullPath('$(MSBuildProjectDirectory)\..\src\protobuf')) - - $(VcpkgInstalledDir)\$(VcpkgTriplet)\tools\protobuf\protoc.exe - $(SourcePath)protobuf - - + + + false + NotUsing + + - - - - - false - NotUsing - - - false - NotUsing - - + + +