diff --git a/.github/actions/spelling/allow.txt b/.github/actions/spelling/allow.txt index 3666150db0..4559c9503f 100644 --- a/.github/actions/spelling/allow.txt +++ b/.github/actions/spelling/allow.txt @@ -180,6 +180,7 @@ Minimatch Moq motw mrm +msb msdata MSHCTX MSHLFLAGS @@ -197,6 +198,7 @@ ncacn Nelon netstandard newid +NNN NOCASE NOCLOSEPROCESS nodiscard diff --git a/.github/instructions/manifest-schema-versioning.instructions.md b/.github/instructions/manifest-schema-versioning.instructions.md new file mode 100644 index 0000000000..e89802c57c --- /dev/null +++ b/.github/instructions/manifest-schema-versioning.instructions.md @@ -0,0 +1,79 @@ +--- +applyTo: "schemas/JSON/manifests/**,src/ManifestSchema/**" +--- + +# Manifest Schema Versioning + +The `schemas/JSON/manifests/` directory holds JSON schemas for WinGet package manifests. +`latest/` is the in-development schema; each `v{X.Y.0}/` directory is a frozen snapshot. +The binary version is defined in `src/binver/binver/version.h` as `{VERSION_MAJOR}.{VERSION_MINOR}.0`. +See `schemas/JSON/manifests/README.md` for full documentation. + +## Script + +`schemas/JSON/manifests/Checkpoint-LatestManifestSchema.ps1` automates most steps in both workflows. + +```powershell +# Freeze only (no version bump, C++ changes are manual). +.\schemas\JSON\manifests\Checkpoint-LatestManifestSchema.ps1 + +# Freeze current latest/, then advance schema to match the binary version (automates C++ source edits). +.\schemas\JSON\manifests\Checkpoint-LatestManifestSchema.ps1 -BumpVersion + +# Dry run. +.\schemas\JSON\manifests\Checkpoint-LatestManifestSchema.ps1 -BumpVersion -WhatIf +``` + +## Workflow A: Starting a new schema version (`-BumpVersion`) + +Run when `version.h` has been advanced to a new minor version and the `latest/` schema version +does not yet match. The script automates steps 1–7 below. Step 8 (test manifests) is always manual. + +| Step | File(s) | Change | +|------|---------|--------| +| 1 | `schemas/JSON/manifests/v{OLD}/` | **Create** frozen copy of current `latest/` (only if the folder doesn't exist). | +| 2 | `schemas/JSON/manifests/latest/*.json` | **Replace** every occurrence of the old version string with the new one (`$id` and `description` fields). | +| 3 | `src/AppInstallerCommonCore/Public/winget/ManifestCommon.h` | **Add** `constexpr std::string_view s_ManifestVersionV{MAJOR}_{MINOR} = "{VERSION}"sv;` before the "Any new manifest version" comment. | +| 4 | `src/ManifestSchema/ManifestSchema.h` | **Add** five `IDX_MANIFEST_SCHEMA_V{MAJOR}_{MINOR}_*` constants (Singleton/Version/Installer/DefaultLocale/Locale) at the next available IDs (must stay below 300). | +| 5 | `src/ManifestSchema/ManifestSchema.rc` | **Append** five new resource entries pointing to `latest/`. | +| 6 | `src/AppInstallerCommonCore/Manifest/ManifestSchemaValidation.cpp` | **Prepend** a new `if (manifestVersion >= ManifestVer{ s_ManifestVersionV{MAJOR}_{MINOR} })` block; old top block becomes `else if`. | +| 7 | `src/WinGetUtilInterop/Manifest/ManifestVersion.cs` | **Add** `public const string ManifestVersionV{MAJOR}_{MINOR} = "{VERSION}";` | + +## Workflow B: Freezing `latest/` at a release + +Run when a WinGet release approaches and the in-flight schema must be locked in. +The script (without `-BumpVersion`) automates steps 1–4 below. Steps 5–8 are only needed if +the C++ infrastructure was not set up in a prior PR. + +| Step | File(s) | Change | +|------|---------|--------| +| 1 | `schemas/JSON/manifests/v{VERSION}/` | **Create** — copy each `latest/manifest.*.latest.json` to `manifest.*.{VERSION}.json`. | +| 2 | `src/ManifestSchema/ManifestSchema.rc` | **Update** five resource entries from `latest/` paths to `v{VERSION}/` paths. | +| 3 | `src/ManifestSchema/ManifestSchema.vcxitems` | **Add** five `` entries. | +| 4 | `src/ManifestSchema/ManifestSchema.vcxitems.filters` | **Add** `` with a new GUID, and five `` items with `schema\v{VERSION}`. | +| 5 | `src/AppInstallerCommonCore/Public/winget/ManifestCommon.h` | **Add** version constant (if not already present). | +| 6 | `src/ManifestSchema/ManifestSchema.h` | **Add** five IDX constants (if not already present). | +| 7 | `src/AppInstallerCommonCore/Manifest/ManifestSchemaValidation.cpp` | **Prepend** new if-block (if not already present). | +| 8 | `src/WinGetUtilInterop/Manifest/ManifestVersion.cs` | **Add** version constant (if not already present). | + +## ManifestSchemaValidation.cpp pattern + +```cpp +if (manifestVersion >= ManifestVer{ s_ManifestVersionV1_N }) +{ + resourceMap = { + { ManifestTypeEnum::Singleton, IDX_MANIFEST_SCHEMA_V1_N_SINGLETON }, + { ManifestTypeEnum::Version, IDX_MANIFEST_SCHEMA_V1_N_VERSION }, + { ManifestTypeEnum::Installer, IDX_MANIFEST_SCHEMA_V1_N_INSTALLER }, + { ManifestTypeEnum::DefaultLocale, IDX_MANIFEST_SCHEMA_V1_N_DEFAULTLOCALE }, + { ManifestTypeEnum::Locale, IDX_MANIFEST_SCHEMA_V1_N_LOCALE }, + }; +} +else if (manifestVersion >= ManifestVer{ s_ManifestVersionV1_ }) +``` + +## Key invariants + +- `latest/` `` entries in `ManifestSchema.vcxitems` are **never removed**; versioned entries are added alongside them. +- `ManifestSchema.rc` entries for the **current** version always point to `latest/`; entries for all **prior** versions point to their `v{VERSION}/` directories. +- Resource IDs in `ManifestSchema.h` are sequential groups of 5, must stay below 300. diff --git a/schemas/JSON/manifests/Checkpoint-LatestManifestSchema.ps1 b/schemas/JSON/manifests/Checkpoint-LatestManifestSchema.ps1 new file mode 100644 index 0000000000..8f630f02ed --- /dev/null +++ b/schemas/JSON/manifests/Checkpoint-LatestManifestSchema.ps1 @@ -0,0 +1,498 @@ +<# +.SYNOPSIS + Branches the 'latest' manifest schemas and/or bumps the schema version to match the binary. + +.DESCRIPTION + Without -BumpVersion: + Freezes the current 'latest/' schemas into a versioned folder (e.g. v1.28.0/), and + updates ManifestSchema.rc, ManifestSchema.vcxitems, and ManifestSchema.vcxitems.filters + to reference the now-versioned files. Use this when a WinGet release is approaching and + the in-flight schema needs to be locked in. + + With -BumpVersion: + Reads the target version from src/binver/binver/version.h (MAJOR.MINOR.0) and compares + it against the version embedded in the 'latest/' schema $id fields. When they differ: + 1. Branches the current 'latest/' schema if a versioned folder does not yet exist. + 2. Updates every $id and description string in the 'latest/' JSON files to the new version. + 3. Adds the new version constant to ManifestCommon.h. + 4. Adds five new IDX_MANIFEST_SCHEMA_* constants to ManifestSchema.h. + 5. Appends new resource entries (pointing to 'latest/') to ManifestSchema.rc. + 6. Prepends a new version block to the if-chain in ManifestSchemaValidation.cpp. + 7. Adds the new version constant to ManifestVersion.cs. + When they already match, reports that no bump is needed and exits. + + C++ test manifests (ManifestV*-Singleton.yaml etc.), the test project files, and + YamlManifest.cpp always require manual updates -- see README.md. + +.PARAMETER BumpVersion + Also advance the 'latest/' schema version to match src/binver/binver/version.h and + update all associated source files. + +.PARAMETER RepoRoot + Path to the repository root. Defaults to three directories above this script, which is + correct when the script lives at schemas/JSON/manifests/Checkpoint-LatestManifestSchema.ps1. + +.EXAMPLE + # Freeze the in-flight schema at its current version number. + .\schemas\JSON\manifests\Checkpoint-LatestManifestSchema.ps1 + +.EXAMPLE + # Freeze then advance the schema version to match the binary. + .\schemas\JSON\manifests\Checkpoint-LatestManifestSchema.ps1 -BumpVersion + +.EXAMPLE + .\schemas\JSON\manifests\Checkpoint-LatestManifestSchema.ps1 -BumpVersion -WhatIf +#> +[CmdletBinding(SupportsShouldProcess)] +param( + [switch] $BumpVersion, + [string] $RepoRoot = (Resolve-Path (Join-Path $PSScriptRoot '..\..\..') -ErrorAction Stop).Path +) + +Set-StrictMode -Version Latest +$ErrorActionPreference = 'Stop' + +# --------------------------------------------------------------------------- +# Helpers +# --------------------------------------------------------------------------- + +function Save-XmlFile { + param([xml]$Xml, [string]$Path) + $settings = [System.Xml.XmlWriterSettings]@{ + Indent = $true + IndentChars = ' ' + Encoding = New-Object System.Text.UTF8Encoding $true + } + $writer = [System.Xml.XmlWriter]::Create($Path, $settings) + try { $Xml.Save($writer) } + finally { $writer.Close() } +} + +function Save-TextFile { + param([string]$Content, [string]$Path) + $Content | Out-File -FilePath $Path -Encoding utf8 -NoNewline +} + +# Derive the C++ identifier suffix from a version string: "1.29.0" -> "V1_29" +function Get-CppSuffix { + param([string]$Version) + $parts = $Version -split '\.' + return "V$($parts[0])_$($parts[1])" +} + +# --------------------------------------------------------------------------- +# Resolve paths +# --------------------------------------------------------------------------- + +$manifestsDir = Join-Path $RepoRoot 'schemas\JSON\manifests' +$latestDir = Join-Path $manifestsDir 'latest' +$manifestSchemaDir = Join-Path $RepoRoot 'src\ManifestSchema' +$rcFile = Join-Path $manifestSchemaDir 'ManifestSchema.rc' +$vcxitemsFile = Join-Path $manifestSchemaDir 'ManifestSchema.vcxitems' +$filtersFile = Join-Path $manifestSchemaDir 'ManifestSchema.vcxitems.filters' +$manifestCommonH = Join-Path $RepoRoot 'src\AppInstallerCommonCore\Public\winget\ManifestCommon.h' +$schemaValidationCpp = Join-Path $RepoRoot 'src\AppInstallerCommonCore\Manifest\ManifestSchemaValidation.cpp' +$manifestSchemaH = Join-Path $RepoRoot 'src\ManifestSchema\ManifestSchema.h' +$manifestVersionCs = Join-Path $RepoRoot 'src\WinGetUtilInterop\Manifest\ManifestVersion.cs' +$binverH = Join-Path $RepoRoot 'src\binver\binver\version.h' + +foreach ($path in $latestDir, $rcFile, $vcxitemsFile, $filtersFile) { + if (-not (Test-Path $path)) { + throw "Required path not found: '$path'. Verify -RepoRoot is correct." + } +} + +# --------------------------------------------------------------------------- +# Read schema version from latest/ +# --------------------------------------------------------------------------- + +$installerSchema = Join-Path $latestDir 'manifest.installer.latest.json' +$schemaJson = Get-Content $installerSchema -Raw | ConvertFrom-Json +$idField = $schemaJson.'$id' + +if ($idField -notmatch '(\d+\.\d+\.\d+)\.schema\.json$') { + throw "Could not parse version from `$id field: '$idField'" +} +$schemaVersion = $Matches[1] +$versionedDir = Join-Path $manifestsDir "v$schemaVersion" + +Write-Host "Current schema version: $schemaVersion" + +# --------------------------------------------------------------------------- +# If -BumpVersion: read binary version and decide what to do +# --------------------------------------------------------------------------- + +$newVersion = $null + +if ($BumpVersion) { + foreach ($path in $binverH, $manifestCommonH, $schemaValidationCpp, $manifestSchemaH, $manifestVersionCs) { + if (-not (Test-Path $path)) { + throw "Required path not found for -BumpVersion: '$path'. Verify -RepoRoot is correct." + } + } + + $binverContent = Get-Content $binverH -Raw + if ($binverContent -notmatch '#define VERSION_MAJOR\s+(\d+)') { throw "Could not parse VERSION_MAJOR from version.h" } + $binMajor = $Matches[1] + if ($binverContent -notmatch '#define VERSION_MINOR\s+(\d+)') { throw "Could not parse VERSION_MINOR from version.h" } + $binMinor = $Matches[1] + $newVersion = "$binMajor.$binMinor.0" + + Write-Host "Binary version: $newVersion" + + if ($schemaVersion -eq $newVersion) {sXml + Write-Host '' + Write-Host "Schema version already matches the binary version ($newVersion). No bump needed." + exit 0 + } + + Write-Host "Schema will be bumped: $schemaVersion -> $newVersion" +} + +Write-Host '' + +# --------------------------------------------------------------------------- +# Branch function: freeze current latest/ into v{schemaVersion}/ +# --------------------------------------------------------------------------- + +$schemaTypes = @('singleton', 'version', 'installer', 'defaultLocale', 'locale') +$rcRelBase = '..\..\schemas\JSON\manifests' +$vcxitemsRelBase = '$(MSBuildThisFileDirectory)..\..\schemas\JSON\manifests' +$ns = 'http://schemas.microsoft.com/developer/msbuild/2003' + +function Invoke-CheckpointLatest { + param([string]$Version, [string]$VersionedDir) + + $alreadyBranched = (Test-Path $VersionedDir) -and + (@(Get-ChildItem $VersionedDir -Filter "*.json" -ErrorAction SilentlyContinue).Count -ge $schemaTypes.Count) + + if ($alreadyBranched) { + Write-Host "=== Branch: v$Version already exists -- skipping schema file copy ===" + } else { + # Step 1: Schema files + Write-Host '=== Checkpoint Step 1: Schema files ===' + if ($PSCmdlet.ShouldProcess($VersionedDir, 'Create directory')) { + New-Item -ItemType Directory -Path $VersionedDir -Force | Out-Null + } + foreach ($type in $schemaTypes) { + $src = Join-Path $latestDir "manifest.$type.latest.json" + $dest = Join-Path $VersionedDir "manifest.$type.$Version.json" + if ($PSCmdlet.ShouldProcess("manifest.$type.latest.json -> manifest.$type.$Version.json", 'Copy')) { + Copy-Item -Path $src -Destination $dest -Force + Write-Host " Copied manifest.$type.$Version.json" + } + } + } + + # Step 2: ManifestSchema.rc -- redirect latest/ entries to versioned paths + Write-Host '' + Write-Host '=== Checkpoint Step 2: ManifestSchema.rc ===' + $rcContent = Get-Content $rcFile -Raw + $rcModified = $false + + foreach ($type in $schemaTypes) { + $latestPath = ("$rcRelBase\latest\manifest.$type.latest.json").Replace('\', '\\') + $versionedPath = ("$rcRelBase\v$Version\manifest.$type.$Version.json").Replace('\', '\\') + + if ($rcContent.Contains($latestPath)) { + if ($PSCmdlet.ShouldProcess($rcFile, "Redirect manifest.$type from latest/ to v$Version/")) { + $rcContent = $rcContent.Replace($latestPath, $versionedPath) + $rcModified = $true + Write-Host " Updated path for manifest.$type" + } + } elseif ($rcContent.Contains($versionedPath)) { + Write-Host " manifest.$type already points to v$Version/ - no change needed." + } else { + Write-Warning " manifest.${type}: neither latest/ nor v${Version}/ path found in RC file. Manual edit required." + } + } + + if ($rcModified -and $PSCmdlet.ShouldProcess($rcFile, 'Save')) { + Save-TextFile $rcContent $rcFile + Write-Host " Saved $rcFile" + } + + # Step 3: ManifestSchema.vcxitems -- add versioned entries + Write-Host '' + Write-Host '=== Checkpoint Step 3: ManifestSchema.vcxitems ===' + + [xml]$vcxitemsXml = Get-Content $vcxitemsFile -Raw + $namespaceManager = New-Object System.Xml.XmlNamespaceManager($vcxitemsXml.NameTable) + $namespaceManager.AddNamespace('msb', $ns) + + $noneGroup = $vcxitemsXml.SelectSingleNode('//msb:ItemGroup[msb:None]', $namespaceManager) + if (-not $noneGroup) { throw 'Could not locate containing in ManifestSchema.vcxitems.' } + + $vcxModified = $false + foreach ($type in $schemaTypes) { + $versionedInclude = "$vcxitemsRelBase\v$Version\manifest.$type.$Version.json" + if ($vcxitemsXml.SelectSingleNode("//msb:None[@Include='$versionedInclude']", $namespaceManager)) { + Write-Host " manifest.$type v$Version already present - no change needed." + continue + } + if ($PSCmdlet.ShouldProcess($vcxitemsFile, "Add for manifest.$type v$Version")) { + $el = $vcxitemsXml.CreateElement('None', $ns) + $el.SetAttribute('Include', $versionedInclude) + $noneGroup.AppendChild($el) | Out-Null + $vcxModified = $true + Write-Host " Added for manifest.$type.$Version.json" + } + } + + if ($vcxModified -and $PSCmdlet.ShouldProcess($vcxitemsFile, 'Save')) { + Save-XmlFile $vcxitemsXml $vcxitemsFile + Write-Host " Saved $vcxitemsFile" + } + + # Step 4: ManifestSchema.vcxitems.filters -- add filter + entries + Write-Host '' + Write-Host '=== Checkpoint Step 4: ManifestSchema.vcxitems.filters ===' + + [xml]$filtersXml = Get-Content $filtersFile -Raw + $filtersNamespaceManager = New-Object System.Xml.XmlNamespaceManager($filtersXml.NameTable) + $filtersNamespaceManager.AddNamespace('msb', $ns) + + $filterName = "schema\v$Version" + $filterGroup = $filtersXml.SelectSingleNode('//msb:ItemGroup[msb:Filter]', $filtersNamespaceManager) + $existingFilter = $filtersXml.SelectSingleNode("//msb:Filter[@Include='$filterName']", $filtersNamespaceManager) + $filtersModified = $false + + if (-not $existingFilter) { + if ($PSCmdlet.ShouldProcess($filtersFile, "Add for $filterName")) { + $newGuid = [System.Guid]::NewGuid().ToString().ToLower() + $filterEl = $filtersXml.CreateElement('Filter', $ns) + $filterEl.SetAttribute('Include', $filterName) + $guidEl = $filtersXml.CreateElement('UniqueIdentifier', $ns) + $guidEl.InnerText = "{$newGuid}" + $filterEl.AppendChild($guidEl) | Out-Null + $filterGroup.AppendChild($filterEl) | Out-Null + $filtersModified = $true + Write-Host " Added for $filterName (GUID: {$newGuid})" + } + } else { + Write-Host " for $filterName already present." + } + + $noneFilterGroup = $filtersXml.SelectSingleNode('//msb:ItemGroup[msb:None]', $filtersNamespaceManager) + if (-not $noneFilterGroup) { + $noneFilterGroup = $filtersXml.CreateElement('ItemGroup', $ns) + $filtersXml.DocumentElement.AppendChild($noneFilterGroup) | Out-Null + } + + foreach ($type in $schemaTypes) { + $versionedInclude = "$vcxitemsRelBase\v$Version\manifest.$type.$Version.json" + if ($filtersXml.SelectSingleNode("//msb:None[@Include='$versionedInclude']", $filtersNamespaceManager)) { + Write-Host " manifest.$type v$Version filter entry already present." + continue + } + if ($PSCmdlet.ShouldProcess($filtersFile, "Add filter entry for manifest.$type v$Version")) { + $noneEl = $filtersXml.CreateElement('None', $ns) + $noneEl.SetAttribute('Include', $versionedInclude) + $filterChild = $filtersXml.CreateElement('Filter', $ns) + $filterChild.InnerText = $filterName + $noneEl.AppendChild($filterChild) | Out-Null + $noneFilterGroup.AppendChild($noneEl) | Out-Null + $filtersModified = $true + Write-Host " Added filter entry for manifest.$type.$Version.json" + } + } + + if ($filtersModified -and $PSCmdlet.ShouldProcess($filtersFile, 'Save')) { + Save-XmlFile $filtersXml $filtersFile + Write-Host " Saved $filtersFile" + } +} + +# --------------------------------------------------------------------------- +# Run checkpoint step +# --------------------------------------------------------------------------- + +Invoke-CheckpointLatest -Version $schemaVersion -VersionedDir $versionedDir + +# --------------------------------------------------------------------------- +# If not bumping, print manual-step reminder and exit +# --------------------------------------------------------------------------- + +if (-not $BumpVersion) { + Write-Host '' + Write-Host '=== Done ===' + exit 0 +} + +# --------------------------------------------------------------------------- +# BumpVersion: update latest/ JSON files and all source files +# --------------------------------------------------------------------------- + +$oldVersion = $schemaVersion +$oldSuffix = Get-CppSuffix $oldVersion +$newSuffix = Get-CppSuffix $newVersion +$parts = $newVersion -split '\.' +$newMajor = $parts[0] +$newMinor = $parts[1] + +# Bump Step 1: Update $id and description in all latest/ JSON files +Write-Host '' +Write-Host '=== Bump Step 1: Update latest/ schema files ===' +foreach ($type in $schemaTypes) { + $jsonFile = Join-Path $latestDir "manifest.$type.latest.json" + $content = Get-Content $jsonFile -Raw + if ($content.Contains($oldVersion)) { + if ($PSCmdlet.ShouldProcess($jsonFile, "Replace $oldVersion with $newVersion")) { + $updated = $content.Replace($oldVersion, $newVersion) + Save-TextFile $updated $jsonFile + Write-Host " Updated manifest.$type.latest.json ($oldVersion -> $newVersion)" + } + } else { + Write-Warning " manifest.$type.latest.json: '$oldVersion' not found. Inspect the file manually." + } +} + +# Bump Step 2: ManifestCommon.h -- add new version constant +Write-Host '' +Write-Host '=== Bump Step 2: ManifestCommon.h ===' +$mcContent = Get-Content $manifestCommonH -Raw + +$newConstant = " // V$newMajor.$newMinor manifest version`n constexpr std::string_view s_ManifestVersion${newSuffix} = `"$newVersion`"sv;`n" +$insertBefore = ' // Any new manifest version must also be added to' + +if ($mcContent.Contains("s_ManifestVersion${newSuffix}")) { + Write-Host " s_ManifestVersion${newSuffix} already present - no change needed." +} elseif (-not $mcContent.Contains($insertBefore)) { + Write-Warning " Could not locate insertion point in ManifestCommon.h. Manual edit required." +} else { + if ($PSCmdlet.ShouldProcess($manifestCommonH, "Add s_ManifestVersion${newSuffix}")) { + $mcContent = $mcContent.Replace($insertBefore, "$newConstant`n$insertBefore") + Save-TextFile $mcContent $manifestCommonH + Write-Host " Added s_ManifestVersion${newSuffix} = `"$newVersion`"sv" + } +} + +# Bump Step 3: ManifestSchema.h -- add five new IDX constants +Write-Host '' +Write-Host '=== Bump Step 3: ManifestSchema.h ===' +$manifestSchemaHeaderContent = Get-Content $manifestSchemaH -Raw + +if ($manifestSchemaHeaderContent.Contains("IDX_MANIFEST_SCHEMA_${newSuffix}_LOCALE")) { + Write-Host " IDX_MANIFEST_SCHEMA_${newSuffix}_* already present - no change needed." +} else { + # Find the last defined IDX constant ID number to determine the next available IDs. + $allIds = [System.Text.RegularExpressions.Regex]::Matches($manifestSchemaHeaderContent, '#define IDX_MANIFEST_SCHEMA_\w+\s+(\d+)') | + ForEach-Object { [int]$_.Groups[1].Value } + if (-not $allIds) { throw 'Could not parse existing IDX constants from ManifestSchema.h.' } + $nextId = ($allIds | Measure-Object -Maximum).Maximum + 1 + + if ($nextId + 4 -ge 300) { + Write-Warning " Next available ID would be $nextId; approaching the 300 limit. Manual review required." + } + + $newBlock = @" +#define IDX_MANIFEST_SCHEMA_${newSuffix}_SINGLETON $nextId +#define IDX_MANIFEST_SCHEMA_${newSuffix}_VERSION $($nextId + 1) +#define IDX_MANIFEST_SCHEMA_${newSuffix}_INSTALLER $($nextId + 2) +#define IDX_MANIFEST_SCHEMA_${newSuffix}_DEFAULTLOCALE $($nextId + 3) +#define IDX_MANIFEST_SCHEMA_${newSuffix}_LOCALE $($nextId + 4) + + +"@ + # Insert before the "Packages schema starts at 300" comment + $insertBefore = '// Packages schema starts at 300' + if (-not $manifestSchemaHeaderContent.Contains($insertBefore)) { + Write-Warning " Could not locate insertion point in ManifestSchema.h. Manual edit required." + } elseif ($PSCmdlet.ShouldProcess($manifestSchemaH, "Add IDX_MANIFEST_SCHEMA_${newSuffix}_* constants")) { + $manifestSchemaHeaderContent = $manifestSchemaHeaderContent.Replace($insertBefore, "$newBlock$insertBefore") + Save-TextFile $manifestSchemaHeaderContent $manifestSchemaH + Write-Host " Added IDX_MANIFEST_SCHEMA_${newSuffix}_* (IDs $nextId-$($nextId + 4))" + } +} + +# Bump Step 4: ManifestSchema.rc -- append new resource entries pointing to latest/ +Write-Host '' +Write-Host '=== Bump Step 4: ManifestSchema.rc ===' +$rcContent = Get-Content $rcFile -Raw + +if ($rcContent.Contains("IDX_MANIFEST_SCHEMA_${newSuffix}_SINGLETON")) { + Write-Host " IDX_MANIFEST_SCHEMA_${newSuffix}_* already present in RC file - no change needed." +} else { + $rcAppend = @" + + +IDX_MANIFEST_SCHEMA_${newSuffix}_SINGLETON MANIFESTSCHEMA_RESOURCE_TYPE "..\\..\\schemas\\JSON\\manifests\\latest\\manifest.singleton.latest.json" +IDX_MANIFEST_SCHEMA_${newSuffix}_VERSION MANIFESTSCHEMA_RESOURCE_TYPE "..\\..\\schemas\\JSON\\manifests\\latest\\manifest.version.latest.json" +IDX_MANIFEST_SCHEMA_${newSuffix}_INSTALLER MANIFESTSCHEMA_RESOURCE_TYPE "..\\..\\schemas\\JSON\\manifests\\latest\\manifest.installer.latest.json" +IDX_MANIFEST_SCHEMA_${newSuffix}_DEFAULTLOCALE MANIFESTSCHEMA_RESOURCE_TYPE "..\\..\\schemas\\JSON\\manifests\\latest\\manifest.defaultLocale.latest.json" +IDX_MANIFEST_SCHEMA_${newSuffix}_LOCALE MANIFESTSCHEMA_RESOURCE_TYPE "..\\..\\schemas\\JSON\\manifests\\latest\\manifest.locale.latest.json" +"@ + if ($PSCmdlet.ShouldProcess($rcFile, "Append IDX_MANIFEST_SCHEMA_${newSuffix}_* entries")) { + $rcContent = $rcContent.TrimEnd() + $rcAppend + "`n" + Save-TextFile $rcContent $rcFile + Write-Host " Appended IDX_MANIFEST_SCHEMA_${newSuffix}_* entries (pointing to latest/)" + } +} + +# Bump Step 5: ManifestSchemaValidation.cpp -- prepend new version block at top of if-chain +Write-Host '' +Write-Host '=== Bump Step 5: ManifestSchemaValidation.cpp ===' +$cppContent = Get-Content $schemaValidationCpp -Raw + +if ($cppContent.Contains("s_ManifestVersion${newSuffix}")) { + Write-Host " s_ManifestVersion${newSuffix} already present - no change needed." +} else { + # The current top of the if-chain begins with "if (manifestVersion >= ManifestVer{ s_ManifestVersionOLD })" + $oldIfLine = " if (manifestVersion >= ManifestVer{ s_ManifestVersion${oldSuffix} })" + if (-not $cppContent.Contains($oldIfLine)) { + Write-Warning " Could not locate '$oldIfLine' in ManifestSchemaValidation.cpp. Manual edit required." + } else { + $newBlock = @" + if (manifestVersion >= ManifestVer{ s_ManifestVersion${newSuffix} }) + { + resourceMap = { + { ManifestTypeEnum::Singleton, IDX_MANIFEST_SCHEMA_${newSuffix}_SINGLETON }, + { ManifestTypeEnum::Version, IDX_MANIFEST_SCHEMA_${newSuffix}_VERSION }, + { ManifestTypeEnum::Installer, IDX_MANIFEST_SCHEMA_${newSuffix}_INSTALLER }, + { ManifestTypeEnum::DefaultLocale, IDX_MANIFEST_SCHEMA_${newSuffix}_DEFAULTLOCALE }, + { ManifestTypeEnum::Locale, IDX_MANIFEST_SCHEMA_${newSuffix}_LOCALE }, + }; + } + else if (manifestVersion >= ManifestVer{ s_ManifestVersion${oldSuffix} }) +"@ + if ($PSCmdlet.ShouldProcess($schemaValidationCpp, "Prepend s_ManifestVersion${newSuffix} block")) { + $cppContent = $cppContent.Replace($oldIfLine, $newBlock) + Save-TextFile $cppContent $schemaValidationCpp + Write-Host " Prepended s_ManifestVersion${newSuffix} block; old block demoted to 'else if'" + } + } +} + +# Bump Step 6: ManifestVersion.cs -- add new version constant +Write-Host '' +Write-Host '=== Bump Step 6: ManifestVersion.cs ===' +$csContent = Get-Content $manifestVersionCs -Raw + +if ($csContent.Contains("ManifestVersion${newSuffix}")) { + Write-Host " ManifestVersion${newSuffix} already present - no change needed." +} else { + # Insert before the closing #pragma restore line + $insertBefore = '#pragma warning restore SA1310' + $newConstant = @" + /// + /// V$newMajor.$newMinor manifest version. + /// + public const string ManifestVersion${newSuffix} = "$newVersion"; + + +"@ + if (-not $csContent.Contains($insertBefore)) { + Write-Warning " Could not locate insertion point in ManifestVersion.cs. Manual edit required." + } elseif ($PSCmdlet.ShouldProcess($manifestVersionCs, "Add ManifestVersion${newSuffix}")) { + $csContent = $csContent.Replace($insertBefore, "$newConstant$insertBefore") + Save-TextFile $csContent $manifestVersionCs + Write-Host " Added ManifestVersion${newSuffix} = `"$newVersion`"" + } +} + +# --------------------------------------------------------------------------- +# Summary +# --------------------------------------------------------------------------- + +Write-Host '' +Write-Host '=== Done ===' diff --git a/schemas/JSON/manifests/README.md b/schemas/JSON/manifests/README.md new file mode 100644 index 0000000000..2e9a4832be --- /dev/null +++ b/schemas/JSON/manifests/README.md @@ -0,0 +1,214 @@ +# WinGet Manifest Schemas + +This directory contains JSON schemas for WinGet package manifests. + +## Directory Structure + +| Directory | Contents | +|-----------|----------| +| `latest/` | The in-development schema. File names use `latest` as the version token. The `$id` field in each file contains the **current version string** (e.g., `1.29.0`). | +| `v{X.Y.0}/` | Frozen snapshots. File names use the numeric version. Content is identical to `latest/` at the moment of branching. | +| `preview/` | Legacy v0.1.0 preview schema. | + +Each version directory contains five schema files: + +| File (using `latest/` as an example) | Manifest type | +|---------------------------------------|---------------| +| `manifest.singleton.latest.json` | Single-file manifest | +| `manifest.version.latest.json` | Multi-file: version manifest | +| `manifest.installer.latest.json` | Multi-file: installer manifest | +| `manifest.defaultLocale.latest.json` | Multi-file: default locale manifest | +| `manifest.locale.latest.json` | Multi-file: locale manifest | + +The version string embedded in the `$id` of each `latest/` file is the authoritative schema +version. The binary version is defined in `src/binver/binver/version.h` as +`{VERSION_MAJOR}.{VERSION_MINOR}.0`. + +--- + +## Helper script + +`Checkpoint-LatestManifestSchema.ps1` (in this directory) automates most of both workflows below. +Run from any directory; it locates the repo root automatically. + +```powershell +# Freeze the current latest/ schema at its version (no source-file changes). +.\schemas\JSON\manifests\Checkpoint-LatestManifestSchema.ps1 + +# Freeze latest/, then advance to match the binary version and update all source files. +.\schemas\JSON\manifests\Checkpoint-LatestManifestSchema.ps1 -BumpVersion + +# Preview changes without writing anything. +.\schemas\JSON\manifests\Checkpoint-LatestManifestSchema.ps1 -BumpVersion -WhatIf +``` + +--- + +## Workflow A: Advancing to a new schema version + +Use this when starting work on a new set of schema changes (e.g. after `version.h` has been +bumped to a new minor version). The goal is to ensure `latest/` carries the new version number +before any schema edits are made. + +**Run the script:** + +```powershell +.\schemas\JSON\manifests\Checkpoint-LatestManifestSchema.ps1 -BumpVersion +``` + +The script performs the following automatically: + +1. Reads the target version from `src/binver/binver/version.h` (`MAJOR.MINOR.0`). +2. Reads the current schema version from `latest/manifest.installer.latest.json` (`$id` field). +3. If the versions already match, exits with no changes. +4. If they differ, checkpoints the current schema (see Workflow B steps 1–4 below). +5. Replaces every occurrence of the old version string in all five `latest/` JSON files (`$id` + and `description` fields). +6. Adds a new version constant to `ManifestCommon.h`. +7. Adds five new `IDX_MANIFEST_SCHEMA_*` resource-ID constants to `ManifestSchema.h`. +8. Appends new resource entries (pointing to `latest/`) to `ManifestSchema.rc`. +9. Prepends a new `if (manifestVersion >= ...)` block at the top of the version-check chain in + `ManifestSchemaValidation.cpp`, converting the old top block to an `else if`. +10. Adds a new version constant to `ManifestVersion.cs`. + +--- + +## Workflow B: Freezing `latest/` into a numbered version + +Use this when a WinGet release is approaching and the in-flight schema needs to be locked in — +i.e., `latest/` is frozen at its current version number and subsequent changes will target the +next version. + +The version to freeze is embedded in the `$id` field of each `latest/` file — for example: + +``` +"$id": "https://aka.ms/winget-manifest.installer.1.28.0.schema.json" + ^^^^^^ + This is the version. +``` + +**Run the script (automates steps 1–4):** + +```powershell +.\schemas\JSON\manifests\Checkpoint-LatestManifestSchema.ps1 +``` + +### Step 1 — Create the versioned schema directory + +Create `schemas/JSON/manifests/v{VERSION}/` and copy each `latest/` file with the version +token substituted for `latest`: + +| Source (`latest/`) | Destination (`v1.28.0/` example) | +|---------------------|----------------------------------| +| `manifest.singleton.latest.json` | `manifest.singleton.1.28.0.json` | +| `manifest.version.latest.json` | `manifest.version.1.28.0.json` | +| `manifest.installer.latest.json` | `manifest.installer.1.28.0.json` | +| `manifest.defaultLocale.latest.json` | `manifest.defaultLocale.1.28.0.json` | +| `manifest.locale.latest.json` | `manifest.locale.1.28.0.json` | + +The JSON content does **not** need editing; the `$id` already contains the version string. + +### Step 2 — Update `src/ManifestSchema/ManifestSchema.rc` + +Redirect the five resource entries for this version from `latest/` to the versioned paths: + +``` +# Before +IDX_MANIFEST_SCHEMA_V1_28_SINGLETON MANIFESTSCHEMA_RESOURCE_TYPE "..\..\schemas\JSON\manifests\latest\manifest.singleton.latest.json" + +# After +IDX_MANIFEST_SCHEMA_V1_28_SINGLETON MANIFESTSCHEMA_RESOURCE_TYPE "..\..\schemas\JSON\manifests\v1.28.0\manifest.singleton.1.28.0.json" +``` + +Repeat for all five manifest types. + +### Step 3 — Update `src/ManifestSchema/ManifestSchema.vcxitems` + +Add five `` entries for the new versioned files inside the existing `` that +contains the `` elements. The `latest/` entries should remain unchanged. + +```xml + + + + + +``` + +### Step 4 — Update `src/ManifestSchema/ManifestSchema.vcxitems.filters` + +Add a `` entry for the new version directory, then add five `` items that +reference the versioned files and assign them to that filter: + +```xml + + + {new-guid-here} + + + + + schema\v1.28.0 + + +``` + +--- + +## C++ source changes + +These files are updated automatically by `-BumpVersion`. They are listed here for reference +when doing the work manually or reviewing what the script changes. + +### `src/AppInstallerCommonCore/Public/winget/ManifestCommon.h` + +Add a version constant before the "Any new manifest version" comment: + +```cpp +// V1.N manifest version +constexpr std::string_view s_ManifestVersionV1_N = "1.N.0"sv; +``` + +### `src/AppInstallerCommonCore/Manifest/ManifestSchemaValidation.cpp` + +Prepend a new `if` block at the top of the version-check chain so that manifests at this +version or higher use the new schema resources. The previous top-level `if` becomes `else if`: + +```cpp +if (manifestVersion >= ManifestVer{ s_ManifestVersionV1_N }) +{ + resourceMap = { + { ManifestTypeEnum::Singleton, IDX_MANIFEST_SCHEMA_V1_N_SINGLETON }, + { ManifestTypeEnum::Version, IDX_MANIFEST_SCHEMA_V1_N_VERSION }, + { ManifestTypeEnum::Installer, IDX_MANIFEST_SCHEMA_V1_N_INSTALLER }, + { ManifestTypeEnum::DefaultLocale, IDX_MANIFEST_SCHEMA_V1_N_DEFAULTLOCALE }, + { ManifestTypeEnum::Locale, IDX_MANIFEST_SCHEMA_V1_N_LOCALE }, + }; +} +else if (manifestVersion >= ManifestVer{ s_ManifestVersionV1_ }) +// ... +``` + +### `src/ManifestSchema/ManifestSchema.h` + +Add five resource-ID constants, continuing the numeric sequence. IDs must stay below 300 +(per the comment in that file): + +```c +#define IDX_MANIFEST_SCHEMA_V1_N_SINGLETON NNN +#define IDX_MANIFEST_SCHEMA_V1_N_VERSION NNN+1 +#define IDX_MANIFEST_SCHEMA_V1_N_INSTALLER NNN+2 +#define IDX_MANIFEST_SCHEMA_V1_N_DEFAULTLOCALE NNN+3 +#define IDX_MANIFEST_SCHEMA_V1_N_LOCALE NNN+4 +``` + +### `src/WinGetUtilInterop/Manifest/ManifestVersion.cs` + +Add a version constant: + +```csharp +/// +/// V1.N manifest version. +/// +public const string ManifestVersionV1_N = "1.N.0"; +```