otclient-redemption/tools/configure_shared_solution_cache.ps1

725 lines
31 KiB
PowerShell
Raw Permalink Normal View History

build(cache): share vcpkg dependencies across CMake and Visual Studio (#1800) 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.
2026-08-13 18:47:47 -03:00
#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</$($value.Key)>"
}
" <PropertyGroup Label=`"CanarySharedVcpkgCache`" Condition=`"$escapedCondition`">`n$($lines -join "`n")`n </PropertyGroup>"
}
$propsContent = @"
<?xml version="1.0" encoding="utf-8"?>
<!-- Generated by tools/configure_shared_solution_cache.ps1. Do not edit. -->
<Project>
$($propertyGroups -join "`n")
</Project>
"@
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."
}