From 4aced04f327cdf975c89dfc64fe43d92d9e4bcab Mon Sep 17 00:00:00 2001 From: Kaleb Luedtke Date: Sun, 21 Jun 2026 10:58:34 -0500 Subject: [PATCH 1/2] Add PACKAGEVERSION token expansion support - Expand in supported manifest fields for YAML and REST parsing - Add/adjust YAML and REST tests and test data coverage - Update release notes with explicit supported fields Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- doc/ReleaseNotes.md | 13 +++- .../AppInstallerCLITests.vcxproj | 3 + .../RestInterface_1_4.cpp | 32 +++++----- .../Manifest-Good-PackageVersionToken.yaml | 28 +++++++++ src/AppInstallerCLITests/YamlManifest.cpp | 18 ++++++ .../Manifest/Manifest.cpp | 59 +++++++++++++++++++ .../Manifest/YamlParser.cpp | 1 + .../Public/winget/Manifest.h | 10 +++- .../1_0/Json/ManifestDeserializer_1_0.cpp | 1 + 9 files changed, 147 insertions(+), 18 deletions(-) create mode 100644 src/AppInstallerCLITests/TestData/Manifest-Good-PackageVersionToken.yaml diff --git a/doc/ReleaseNotes.md b/doc/ReleaseNotes.md index 57c970aeb6..cc155108c4 100644 --- a/doc/ReleaseNotes.md +++ b/doc/ReleaseNotes.md @@ -1,6 +1,17 @@ ## New in v1.29 -Nothing yet. + +### Manifest `` token expansion + +Added `` token which will use the `PackageVersion` from the manifest in place of the token. This applies to the following manifest fields: + +* `NestedInstallerFiles.RelativeFilePath` +* `ProductCode` +* `AppsAndFeaturesEntries.DisplayName` +* `AppsAndFeaturesEntries.ProductCode` +* `InstallationMetadata.DefaultInstallLocation` +* `InstallationMetadata.Files.RelativeFilePath` +* `ReleaseNotesUrl` ## Bug Fixes diff --git a/src/AppInstallerCLITests/AppInstallerCLITests.vcxproj b/src/AppInstallerCLITests/AppInstallerCLITests.vcxproj index ba0ced5853..a4fe9e1e42 100644 --- a/src/AppInstallerCLITests/AppInstallerCLITests.vcxproj +++ b/src/AppInstallerCLITests/AppInstallerCLITests.vcxproj @@ -705,6 +705,9 @@ true + + true + true diff --git a/src/AppInstallerCLITests/RestInterface_1_4.cpp b/src/AppInstallerCLITests/RestInterface_1_4.cpp index fe5f2ecca0..51fd3e3407 100644 --- a/src/AppInstallerCLITests/RestInterface_1_4.cpp +++ b/src/AppInstallerCLITests/RestInterface_1_4.cpp @@ -67,7 +67,7 @@ namespace ], "Moniker": "FooBarMoniker", "ReleaseNotes": "Default release notes", - "ReleaseNotesUrl": "https://DefaultReleaseNotes.net", + "ReleaseNotesUrl": "https://DefaultReleaseNotes.net/", "Agreements": [{ "AgreementLabel": "DefaultLabel", "Agreement": "DefaultText", @@ -103,7 +103,7 @@ namespace "BarFr" ], "ReleaseNotes": "Release notes", - "ReleaseNotesUrl": "https://ReleaseNotes.net", + "ReleaseNotesUrl": "https://ReleaseNotes.net/", "Agreements": [{ "AgreementLabel": "Label", "Agreement": "Text", @@ -171,7 +171,7 @@ namespace "FooBarBaz" ] }, - "ProductCode": "5b6e0f8a-3bbf-4a17-aefd-024c2b3e075d", + "ProductCode": "5b6e0f8a-", "ReleaseDate": "2021-01-01", "InstallerAbortsTerminal": true, "InstallLocationRequired": true, @@ -179,10 +179,10 @@ namespace "UnsupportedOSArchitectures": [ "arm" ], "ElevationRequirement": "elevatesSelf", "AppsAndFeaturesEntries": [{ - "DisplayName": "DisplayName", + "DisplayName": "DisplayName-", "DisplayVersion": "DisplayVersion", "Publisher": "Publisher", - "ProductCode": "ProductCode", + "ProductCode": "ProductCode-", "UpgradeCode": "UpgradeCode", "InstallerType": "exe" }], @@ -198,13 +198,13 @@ namespace "DisplayInstallWarnings": true, "UnsupportedArguments": [ "log" ], "NestedInstallerFiles": [{ - "RelativeFilePath": "test\\app.exe", + "RelativeFilePath": "test\\app..exe", "PortableCommandAlias": "test.exe" }], "InstallationMetadata": { - "DefaultInstallLocation": "%TEMP%\\DefaultInstallLocation", + "DefaultInstallLocation": "%TEMP%\\DefaultInstallLocation\\", "Files": [{ - "RelativeFilePath": "test\\app.exe", + "RelativeFilePath": "test\\app..exe", "FileSha256": "011048877dfaef109801b3f3ab2b60afc74f3fc4f7b3430e0c897f5da1df84b6", "FileType": "launch", "InvocationParameter": "/parameter", @@ -241,7 +241,7 @@ namespace REQUIRE(manifest.DefaultLocalization.Get().at(1) == "Foo"); REQUIRE(manifest.DefaultLocalization.Get().at(2) == "Bar"); REQUIRE(manifest.DefaultLocalization.Get() == "Default release notes"); - REQUIRE(manifest.DefaultLocalization.Get() == "https://DefaultReleaseNotes.net"); + REQUIRE(manifest.DefaultLocalization.Get() == "https://DefaultReleaseNotes.net/3.0.0abc"); REQUIRE(manifest.DefaultLocalization.Get().size() == 1); REQUIRE(manifest.DefaultLocalization.Get().at(0).Label == "DefaultLabel"); REQUIRE(manifest.DefaultLocalization.Get().at(0).AgreementText == "DefaultText"); @@ -273,7 +273,7 @@ namespace REQUIRE(frenchLocalization.Get().at(1) == "FooFr"); REQUIRE(frenchLocalization.Get().at(2) == "BarFr"); REQUIRE(frenchLocalization.Get() == "Release notes"); - REQUIRE(frenchLocalization.Get() == "https://ReleaseNotes.net"); + REQUIRE(frenchLocalization.Get() == "https://ReleaseNotes.net/3.0.0abc"); REQUIRE(frenchLocalization.Get().size() == 1); REQUIRE(frenchLocalization.Get().at(0).Label == "Label"); REQUIRE(frenchLocalization.Get().at(0).AgreementText == "Text"); @@ -320,7 +320,7 @@ namespace REQUIRE(actualInstaller.Dependencies.HasExactDependency(DependencyType::Package, "Foo.Baz", "2.0.0")); REQUIRE(actualInstaller.Dependencies.HasExactDependency(DependencyType::External, "FooBarBaz")); REQUIRE(actualInstaller.PackageFamilyName == ""); - REQUIRE(actualInstaller.ProductCode == "5b6e0f8a-3bbf-4a17-aefd-024c2b3e075d"); + REQUIRE(actualInstaller.ProductCode == "5b6e0f8a-3.0.0abc"); REQUIRE(actualInstaller.ReleaseDate == "2021-01-01"); REQUIRE(actualInstaller.InstallerAbortsTerminal); REQUIRE(actualInstaller.InstallLocationRequired); @@ -329,10 +329,10 @@ namespace REQUIRE(actualInstaller.UnsupportedOSArchitectures.size() == 1); REQUIRE(actualInstaller.UnsupportedOSArchitectures.at(0) == Architecture::Arm); REQUIRE(actualInstaller.AppsAndFeaturesEntries.size() == 1); - REQUIRE(actualInstaller.AppsAndFeaturesEntries.at(0).DisplayName == "DisplayName"); + REQUIRE(actualInstaller.AppsAndFeaturesEntries.at(0).DisplayName == "DisplayName-3.0.0abc"); REQUIRE(actualInstaller.AppsAndFeaturesEntries.at(0).DisplayVersion == "DisplayVersion"); REQUIRE(actualInstaller.AppsAndFeaturesEntries.at(0).Publisher == "Publisher"); - REQUIRE(actualInstaller.AppsAndFeaturesEntries.at(0).ProductCode == "ProductCode"); + REQUIRE(actualInstaller.AppsAndFeaturesEntries.at(0).ProductCode == "ProductCode-3.0.0abc"); REQUIRE(actualInstaller.AppsAndFeaturesEntries.at(0).UpgradeCode == "UpgradeCode"); REQUIRE(actualInstaller.AppsAndFeaturesEntries.at(0).InstallerType == InstallerTypeEnum::Exe); REQUIRE(actualInstaller.Markets.AllowedMarkets.size() == 1); @@ -344,11 +344,11 @@ namespace REQUIRE(actualInstaller.UnsupportedArguments.size() == 1); REQUIRE(actualInstaller.UnsupportedArguments.at(0) == UnsupportedArgumentEnum::Log); REQUIRE(actualInstaller.NestedInstallerFiles.size() == 1); - REQUIRE(actualInstaller.NestedInstallerFiles.at(0).RelativeFilePath == "test\\app.exe"); + REQUIRE(actualInstaller.NestedInstallerFiles.at(0).RelativeFilePath == "test\\app.3.0.0abc.exe"); REQUIRE(actualInstaller.NestedInstallerFiles.at(0).PortableCommandAlias == "test.exe"); - REQUIRE(actualInstaller.InstallationMetadata.DefaultInstallLocation == "%TEMP%\\DefaultInstallLocation"); + REQUIRE(actualInstaller.InstallationMetadata.DefaultInstallLocation == "%TEMP%\\DefaultInstallLocation\\3.0.0abc"); REQUIRE(actualInstaller.InstallationMetadata.Files.size() == 1); - REQUIRE(actualInstaller.InstallationMetadata.Files.at(0).RelativeFilePath == "test\\app.exe"); + REQUIRE(actualInstaller.InstallationMetadata.Files.at(0).RelativeFilePath == "test\\app.3.0.0abc.exe"); REQUIRE(actualInstaller.InstallationMetadata.Files.at(0).FileType == InstalledFileTypeEnum::Launch); REQUIRE(actualInstaller.InstallationMetadata.Files.at(0).FileSha256 == AppInstaller::Utility::SHA256::ConvertToBytes("011048877dfaef109801b3f3ab2b60afc74f3fc4f7b3430e0c897f5da1df84b6")); REQUIRE(actualInstaller.InstallationMetadata.Files.at(0).InvocationParameter == "/parameter"); diff --git a/src/AppInstallerCLITests/TestData/Manifest-Good-PackageVersionToken.yaml b/src/AppInstallerCLITests/TestData/Manifest-Good-PackageVersionToken.yaml new file mode 100644 index 0000000000..7f3d17dd59 --- /dev/null +++ b/src/AppInstallerCLITests/TestData/Manifest-Good-PackageVersionToken.yaml @@ -0,0 +1,28 @@ +# yaml-language-server: $schema=https://aka.ms/winget-manifest.singleton.1.4.0.schema.json + +PackageIdentifier: microsoft.msixsdk +PackageVersion: 1.2.3.4 +PackageLocale: en-US +PackageName: AppInstaller Test Installer +Publisher: Microsoft Corporation +License: Test +ShortDescription: Test manifest for PACKAGEVERSION token expansion. +ReleaseNotesUrl: https://example.com/releases/ +Installers: + - Architecture: x64 + InstallerUrl: https://ThisIsNotUsed + InstallerType: zip + InstallerSha256: 65DB2F2AC2686C7F2FD69D4A4C6683B888DC55BFA20A0E32CA9F838B51689A3B + ProductCode: Product- + AppsAndFeaturesEntries: + - DisplayName: DisplayName- + ProductCode: ArpProduct- + NestedInstallerType: exe + NestedInstallerFiles: + - RelativeFilePath: setup\AppInstallerTestExeInstaller..exe + InstallationMetadata: + DefaultInstallLocation: C:\Program Files\Test\ + Files: + - RelativeFilePath: app\AppInstallerTestExeInstaller..exe +ManifestType: singleton +ManifestVersion: 1.4.0 diff --git a/src/AppInstallerCLITests/YamlManifest.cpp b/src/AppInstallerCLITests/YamlManifest.cpp index 7574007329..f52633b9a3 100644 --- a/src/AppInstallerCLITests/YamlManifest.cpp +++ b/src/AppInstallerCLITests/YamlManifest.cpp @@ -826,6 +826,24 @@ TEST_CASE("ReadGoodManifestWithSpaces", "[ManifestValidation]") REQUIRE(manifest.DefaultInstallerInfo.FileExtensions == MultiValue{ "appx", "appxbundle", "msix", "msixbundle" }); } +TEST_CASE("ReadManifest_ExpandPackageVersionToken", "[ManifestValidation]") +{ + Manifest manifest = YamlParser::CreateFromPath(TestDataFile("Manifest-Good-PackageVersionToken.yaml")); + + REQUIRE(manifest.Version == "1.2.3.4"); + REQUIRE(manifest.DefaultLocalization.Get() == "https://example.com/releases/1.2.3.4"); + REQUIRE(manifest.Installers.size() == 1); + REQUIRE(manifest.Installers[0].NestedInstallerFiles.size() == 1); + REQUIRE(manifest.Installers[0].NestedInstallerFiles[0].RelativeFilePath == "setup\\AppInstallerTestExeInstaller.1.2.3.4.exe"); + REQUIRE(manifest.Installers[0].ProductCode == "Product-1.2.3.4"); + REQUIRE(manifest.Installers[0].AppsAndFeaturesEntries.size() == 1); + REQUIRE(manifest.Installers[0].AppsAndFeaturesEntries[0].DisplayName == "DisplayName-1.2.3.4"); + REQUIRE(manifest.Installers[0].AppsAndFeaturesEntries[0].ProductCode == "ArpProduct-1.2.3.4"); + REQUIRE(manifest.Installers[0].InstallationMetadata.DefaultInstallLocation == "C:\\Program Files\\Test\\1.2.3.4"); + REQUIRE(manifest.Installers[0].InstallationMetadata.Files.size() == 1); + REQUIRE(manifest.Installers[0].InstallationMetadata.Files[0].RelativeFilePath == "app\\AppInstallerTestExeInstaller.1.2.3.4.exe"); +} + TEST_CASE("ReadGoodManifests", "[ManifestValidation]") { ManifestTestCase TestCases[] = diff --git a/src/AppInstallerCommonCore/Manifest/Manifest.cpp b/src/AppInstallerCommonCore/Manifest/Manifest.cpp index 32612b1147..76edb2e1dc 100644 --- a/src/AppInstallerCommonCore/Manifest/Manifest.cpp +++ b/src/AppInstallerCommonCore/Manifest/Manifest.cpp @@ -9,6 +9,25 @@ namespace AppInstaller::Manifest { namespace { + bool ReplacePackageVersionTokenInValue(std::string& value, const std::string& packageVersion) + { + return Utility::FindAndReplace(value, std::string{ PACKAGE_VERSION_TOKEN }, packageVersion); + } + + // ReleaseNotesUrl is stored in ManifestLocalization's typed map; unlike plain struct fields, + // it must be read via Get<> and written back via Add<> after token expansion. + void ExpandPackageVersionTokenInReleaseNotesUrl(ManifestLocalization& localization, const std::string& packageVersion) + { + if (localization.Contains(Localization::ReleaseNotesUrl)) + { + auto releaseNotesUrl = localization.Get(); + if (ReplacePackageVersionTokenInValue(releaseNotesUrl, packageVersion)) + { + localization.Add(std::move(releaseNotesUrl)); + } + } + } + void AddFoldedStringToSetIfNotEmpty(std::set& set, const string_t& value) { if (!value.empty()) @@ -18,6 +37,46 @@ namespace AppInstaller::Manifest } } + void ExpandManifestPackageVersionTokens(Manifest& manifest) + { + if (manifest.Version.empty()) + { + return; + } + + const std::string packageVersion = manifest.Version; + + for (auto& installer : manifest.Installers) + { + for (auto& nestedInstallerFile : installer.NestedInstallerFiles) + { + ReplacePackageVersionTokenInValue(nestedInstallerFile.RelativeFilePath, packageVersion); + } + + ReplacePackageVersionTokenInValue(installer.ProductCode, packageVersion); + + for (auto& arpEntry : installer.AppsAndFeaturesEntries) + { + ReplacePackageVersionTokenInValue(arpEntry.DisplayName, packageVersion); + ReplacePackageVersionTokenInValue(arpEntry.ProductCode, packageVersion); + } + + ReplacePackageVersionTokenInValue(installer.InstallationMetadata.DefaultInstallLocation, packageVersion); + + for (auto& installedFile : installer.InstallationMetadata.Files) + { + ReplacePackageVersionTokenInValue(installedFile.RelativeFilePath, packageVersion); + } + } + + ExpandPackageVersionTokenInReleaseNotesUrl(manifest.DefaultLocalization, packageVersion); + + for (auto& localization : manifest.Localizations) + { + ExpandPackageVersionTokenInReleaseNotesUrl(localization, packageVersion); + } + } + void Manifest::ApplyLocale(const std::string& locale) { CurrentLocalization = DefaultLocalization; diff --git a/src/AppInstallerCommonCore/Manifest/YamlParser.cpp b/src/AppInstallerCommonCore/Manifest/YamlParser.cpp index afedeb17e2..657a85bb11 100644 --- a/src/AppInstallerCommonCore/Manifest/YamlParser.cpp +++ b/src/AppInstallerCommonCore/Manifest/YamlParser.cpp @@ -473,6 +473,7 @@ namespace AppInstaller::Manifest::YamlParser auto errors = ManifestYamlPopulator::PopulateManifest(manifestDoc, manifest, manifestVersion, validateOption, shadowNode); std::move(errors.begin(), errors.end(), std::inserter(resultErrors, resultErrors.end())); + ExpandManifestPackageVersionTokens(manifest); // Extra semantic validations after basic validation and field population if (validateOption.FullValidation) diff --git a/src/AppInstallerCommonCore/Public/winget/Manifest.h b/src/AppInstallerCommonCore/Public/winget/Manifest.h index 725b4dd56c..6ad1c77f9b 100644 --- a/src/AppInstallerCommonCore/Public/winget/Manifest.h +++ b/src/AppInstallerCommonCore/Public/winget/Manifest.h @@ -7,10 +7,18 @@ #include #include +#include #include namespace AppInstaller::Manifest { + struct Manifest; + + inline constexpr std::string_view PACKAGE_VERSION_TOKEN = ""; + + // Expands supported manifest string tokens using package-level manifest data. + void ExpandManifestPackageVersionTokens(Manifest& manifest); + // Representation of the parsed manifest file. struct Manifest { @@ -72,4 +80,4 @@ namespace AppInstaller::Manifest std::function extractStringFromInstaller = {}, std::function extractStringFromAppsAndFeaturesEntry = {}) const; }; -} \ No newline at end of file +} diff --git a/src/AppInstallerRepositoryCore/Rest/Schema/1_0/Json/ManifestDeserializer_1_0.cpp b/src/AppInstallerRepositoryCore/Rest/Schema/1_0/Json/ManifestDeserializer_1_0.cpp index 081f2f50e0..7ab44aa8f9 100644 --- a/src/AppInstallerRepositoryCore/Rest/Schema/1_0/Json/ManifestDeserializer_1_0.cpp +++ b/src/AppInstallerRepositoryCore/Rest/Schema/1_0/Json/ManifestDeserializer_1_0.cpp @@ -253,6 +253,7 @@ namespace AppInstaller::Repository::Rest::Schema::V1_0::Json } } + ExpandManifestPackageVersionTokens(manifest); manifests.emplace_back(std::move(manifest)); } From 78119e62fd1876f0e5a7ae16a3fa8bcda92adec9 Mon Sep 17 00:00:00 2001 From: Kaleb Luedtke Date: Sun, 21 Jun 2026 11:02:40 -0500 Subject: [PATCH 2/2] Allow PACKAGEVERSION in spellcheck Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- .github/actions/spelling/allow.txt | 1 + 1 file changed, 1 insertion(+) diff --git a/.github/actions/spelling/allow.txt b/.github/actions/spelling/allow.txt index 3666150db0..3ed41d7ec0 100644 --- a/.github/actions/spelling/allow.txt +++ b/.github/actions/spelling/allow.txt @@ -222,6 +222,7 @@ OUTOFPROC packagefamilyname packageidentifier packagename +PACKAGEVERSION PACKAGESSCHEMA paket Params