winget-cli

Unnamed repository; edit this file 'description' to name the repository.
Log | Files | Refs | README | LICENSE

commit 4993e4ba5ba49a2fdba2d3e049cf5b8b0d6e1e51
parent b4d8c969230e2ec8079dbb870523eacbf0925938
Author: Madhusudhan-MSFT <53235553+Madhusudhan-MSFT@users.noreply.github.com>
Date:   Tue, 16 Jan 2024 16:05:58 -0800

Repair switch support for V1.7 YAML manifest (#4041)

Add YAML manifest entries for winget repair behavior

Manifest Entries includes:
- The `Repair `switch, within `InstallerSwitch,` allows for custom
repair of a package.
- The `RepairBehavior` enum field, present in the Installer & root level
field, determines the repair behavior for the source package used to
repair a package.
- Updated the test manifest (ManifestV1_7-MultiFile-Installer.yaml) and
the test code that validate the Repair switch and RepairBehavior fields.

**[How Validated:]**
- Made the necessary changes to the manifest
- Ran the ManifestValidation tests that are part of AppInstallerCLITests
and verified that all the tests passed

**[Test Result:]**
```
Filters: [ManifestValidation]
===============================================================================
All tests passed (4008 assertions in 30 test cases)

Filters: [ManifestCreation]
===============================================================================
All tests passed (1309 assertions in 7 test cases)
```
**TODO's:**

- [ ] Determine the interdependency validation between the
"RepairBehavior" and "Repair" fields. Should this be a semantic
validation or a validation at the time of workflow execution as an early
validation step?



<!-- To check a checkbox place an "x" between the brackets. e.g: [x] -->

- [x] I have signed the [Contributor License
Agreement](https://cla.opensource.microsoft.com/microsoft/winget-pkgs).
- [x] This pull request is related to an issue.

Related to : 

*  #148 

-----

###### Microsoft Reviewers: [Open in
CodeFlow](https://microsoft.github.io/open-pr/?codeflow=https://github.com/microsoft/winget-cli/pull/4041)
Diffstat:
Mschemas/JSON/manifests/v1.7.0/manifest.installer.1.7.0.json | 21+++++++++++++++++++++
Mschemas/JSON/manifests/v1.7.0/manifest.singleton.1.7.0.json | 21+++++++++++++++++++++
Msrc/AppInstallerCLITests/TestData/ManifestV1_7-Singleton.yaml | 3+++
Msrc/AppInstallerCLITests/TestData/MultiFileManifestV1_7/ManifestV1_7-MultiFile-Installer.yaml | 13+++++++++++++
Msrc/AppInstallerCLITests/YamlManifest.cpp | 68+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++---
Msrc/AppInstallerCommonCore/Manifest/ManifestCommon.cpp | 38++++++++++++++++++++++++++++++++++++++
Msrc/AppInstallerCommonCore/Manifest/ManifestYamlPopulator.cpp | 15+++++++++++++++
Msrc/AppInstallerCommonCore/Manifest/YamlWriter.cpp | 3+++
Msrc/AppInstallerCommonCore/Public/winget/ManifestCommon.h | 13+++++++++++++
Msrc/AppInstallerCommonCore/Public/winget/ManifestInstaller.h | 2++
10 files changed, 194 insertions(+), 3 deletions(-)

diff --git a/schemas/JSON/manifests/v1.7.0/manifest.installer.1.7.0.json b/schemas/JSON/manifests/v1.7.0/manifest.installer.1.7.0.json @@ -187,6 +187,12 @@ "minLength": 1, "maxLength": 2048, "description": "Custom switches will be passed directly to the installer by winget" + }, + "Repair" : { + "type": [ "string", "null" ], + "minLength": 1, + "maxLength": 512, + "description": "The 'Repair' value must be passed to the installer, ModifyPath ARP command, or uninstaller ARP command when the user opts for a repair." } } }, @@ -581,6 +587,15 @@ "type": [ "boolean", "null" ], "description": "Indicates whether the installer is prohibited from being downloaded for offline installation." }, + "RepairBehavior": { + "type": [ "string", "null" ], + "enum": [ + "modify", + "uninstaller", + "installer" + ], + "description": "The repair method" + }, "Installer": { "type": "object", "properties": { @@ -698,6 +713,9 @@ }, "DownloadCommandProhibited": { "$ref": "#/definitions/DownloadCommandProhibited" + }, + "RepairBehavior": { + "$ref": "#/definitions/RepairBehavior" } }, "required": [ @@ -814,6 +832,9 @@ "DownloadCommandProhibited": { "$ref": "#/definitions/DownloadCommandProhibited" }, + "RepairBehavior": { + "$ref": "#/definitions/RepairBehavior" + }, "Installers": { "type": "array", "items": { diff --git a/schemas/JSON/manifests/v1.7.0/manifest.singleton.1.7.0.json b/schemas/JSON/manifests/v1.7.0/manifest.singleton.1.7.0.json @@ -287,6 +287,12 @@ "minLength": 1, "maxLength": 2048, "description": "Custom switches will be passed directly to the installer by winget" + }, + "Repair": { + "type": [ "string", "null" ], + "minLength": 1, + "maxLength": 512, + "description": "The 'Repair' value must be passed to the installer, ModifyPath ARP command, or uninstaller ARP command when the user opts for a repair" } } }, @@ -680,6 +686,15 @@ "type": [ "boolean", "null" ], "description": "Indicates whether the installer is prohibited from being downloaded for offline installation." }, + "RepairBehavior": { + "type": [ "string", "null" ], + "enum": [ + "modify", + "uninstaller", + "installer" + ], + "description": "The repair method" + }, "Installer": { "type": "object", "properties": { @@ -797,6 +812,9 @@ }, "DownloadCommandProhibited": { "$ref": "#/definitions/DownloadCommandProhibited" + }, + "RepairBehavior": { + "$ref": "#/definitions/RepairBehavior" } }, "required": [ @@ -1036,6 +1054,9 @@ "DownloadCommandProhibited": { "$ref": "#/definitions/DownloadCommandProhibited" }, + "RepairBehavior": { + "$ref": "#/definitions/RepairBehavior" + }, "Installers": { "type": "array", "items": { diff --git a/src/AppInstallerCLITests/TestData/ManifestV1_7-Singleton.yaml b/src/AppInstallerCLITests/TestData/ManifestV1_7-Singleton.yaml @@ -54,10 +54,12 @@ InstallerSwitches: Log: /log=<LOGPATH> InstallLocation: /dir=<INSTALLPATH> Upgrade: /upgrade + Repair: /repair InstallerSuccessCodes: - 1 - 0x80070005 UpgradeBehavior: uninstallPrevious +RepairBehavior: modify Commands: - makemsix - makeappx @@ -144,6 +146,7 @@ Installers: Log: /l=<LOGPATH> InstallLocation: /d=<INSTALLPATH> Upgrade: /u + Repair: /r UpgradeBehavior: install Commands: - makemsixPreview diff --git a/src/AppInstallerCLITests/TestData/MultiFileManifestV1_7/ManifestV1_7-MultiFile-Installer.yaml b/src/AppInstallerCLITests/TestData/MultiFileManifestV1_7/ManifestV1_7-MultiFile-Installer.yaml @@ -19,10 +19,12 @@ InstallerSwitches: Log: /log=<LOGPATH> InstallLocation: /dir=<INSTALLPATH> Upgrade: /upgrade + Repair: /repair InstallerSuccessCodes: - 1 - 0x80070005 UpgradeBehavior: uninstallPrevious +RepairBehavior: modify Commands: - makemsix - makeappx @@ -109,6 +111,7 @@ Installers: Log: /l=<LOGPATH> InstallLocation: /d=<INSTALLPATH> Upgrade: /u + Repair: /r UpgradeBehavior: install Commands: - makemsixPreview @@ -161,7 +164,10 @@ Installers: InstallerUrl: https://www.microsoft.com/msixsdk/msixsdkx64.exe InstallerSha256: 69D84CA8899800A5575CE31798293CD4FEBAB1D734A07C2E51E56A28E0DF8C82 ProductCode: "{Bar}" + InstallerSwitches: + Repair: /r UpgradeBehavior: deny + RepairBehavior: uninstaller - Architecture: x86 InstallerType: portable InstallerUrl: https://www.microsoft.com/msixsdk/msixsdkx86.exe @@ -191,5 +197,12 @@ Installers: FileType: other InvocationParameter: "/arg2" DisplayName: "DisplayName2" + - Architecture: x64 + InstallerType: burn + InstallerUrl: https://www.microsoft.com/msixsdk/msixsdkx64.exe + InstallerSha256: 69D84CA8899800A5575CE31798293CD4FEBAB1D734A07C2E51E56A28E0DF8C82 + ProductCode: "{Bar}" + UpgradeBehavior: deny + RepairBehavior: modify ManifestType: installer ManifestVersion: 1.7.0 \ No newline at end of file diff --git a/src/AppInstallerCLITests/YamlManifest.cpp b/src/AppInstallerCLITests/YamlManifest.cpp @@ -527,6 +527,12 @@ void VerifyV1ManifestContent(const Manifest& manifest, bool isSingleton, Manifes { REQUIRE(manifest.DefaultInstallerInfo.DownloadCommandProhibited); } + + if (manifestVer >= ManifestVer{ s_ManifestVersionV1_7 }) + { + REQUIRE(defaultSwitches.at(InstallerSwitchType::Repair) == "/repair"); + REQUIRE(manifest.DefaultInstallerInfo.RepairBehavior == RepairBehaviorEnum::Modify); + } } if (isSingleton || isExported) @@ -535,7 +541,11 @@ void VerifyV1ManifestContent(const Manifest& manifest, bool isSingleton, Manifes } else { - if (manifestVer >= ManifestVer{ s_ManifestVersionV1_4 }) + if (manifestVer >= ManifestVer{ s_ManifestVersionV1_7 }) + { + REQUIRE(manifest.Installers.size() == 5); + } + else if (manifestVer >= ManifestVer{ s_ManifestVersionV1_4 }) { REQUIRE(manifest.Installers.size() == 4); } @@ -631,6 +641,12 @@ void VerifyV1ManifestContent(const Manifest& manifest, bool isSingleton, Manifes REQUIRE_FALSE(installer1.DownloadCommandProhibited); } + if (manifestVer >= ManifestVer { s_ManifestVersionV1_7}) + { + REQUIRE(installer1.Switches.at(InstallerSwitchType::Repair) == "/r"); + REQUIRE(installer1.RepairBehavior == RepairBehaviorEnum::Modify); + } + if (!isSingleton) { if (!isExported) @@ -708,6 +724,21 @@ void VerifyV1ManifestContent(const Manifest& manifest, bool isSingleton, Manifes REQUIRE(installer2.DownloadCommandProhibited); REQUIRE(installer2.UpdateBehavior == UpdateBehaviorEnum::Deny); } + + if (manifestVer >= ManifestVer{ s_ManifestVersionV1_7 }) + { + REQUIRE(installer2.RepairBehavior == RepairBehaviorEnum::Uninstaller); + REQUIRE(installer2.Switches.at(InstallerSwitchType::Repair) == "/r"); + + ManifestInstaller installer5 = manifest.Installers.at(4); + REQUIRE(installer5.BaseInstallerType == InstallerTypeEnum::Burn); + REQUIRE(installer5.Arch == Architecture::X64); + REQUIRE(installer5.Url == "https://www.microsoft.com/msixsdk/msixsdkx64.exe"); + REQUIRE(installer5.Sha256 == SHA256::ConvertToBytes("69D84CA8899800A5575CE31798293CD4FEBAB1D734A07C2E51E56A28E0DF8C82")); + REQUIRE(installer5.ProductCode == "{Bar}"); + REQUIRE(installer5.Switches.at(InstallerSwitchType::Repair) == "/repair"); + REQUIRE(installer5.RepairBehavior == RepairBehaviorEnum::Modify); + } } // Localization @@ -792,7 +823,7 @@ TEST_CASE("ValidateV1_1GoodManifestAndVerifyContents", "[ManifestValidation]") TempDirectory singletonDirectory{ "SingletonManifest" }; CopyTestDataFilesToFolder({ "ManifestV1_1-Singleton.yaml" }, singletonDirectory); Manifest singletonManifest = YamlParser::CreateFromPath(singletonDirectory, validateOption); - VerifyV1ManifestContent(singletonManifest, true, ManifestVer{s_ManifestVersionV1_1}); + VerifyV1ManifestContent(singletonManifest, true, ManifestVer{ s_ManifestVersionV1_1 }); TempDirectory multiFileDirectory{ "MultiFileManifest" }; CopyTestDataFilesToFolder({ @@ -1121,6 +1152,37 @@ TEST_CASE("WriteV1_6SingletonManifestAndVerifyContents", "[ManifestCreation]") VerifyV1ManifestContent(generatedMultiFileManifest, false, ManifestVer{ s_ManifestVersionV1_6 }, true); } +TEST_CASE("WriteV1_7SingletonManifestAndVerifyContents", "[ManifestCreation]") +{ + TempDirectory singletonDirectory{ "SingletonManifest" }; + CopyTestDataFilesToFolder({ "ManifestV1_7-Singleton.yaml" }, singletonDirectory); + Manifest singletonManifest = YamlParser::CreateFromPath(singletonDirectory); + + TempDirectory exportedSingletonDirectory{ "exportedSingleton" }; + std::filesystem::path generatedSingletonManifestPath = exportedSingletonDirectory.GetPath() / "testSingletonManifest.yaml"; + YamlWriter::OutputYamlFile(singletonManifest, singletonManifest.Installers[0], generatedSingletonManifestPath); + + REQUIRE(std::filesystem::exists(generatedSingletonManifestPath)); + Manifest generatedSingletonManifest = YamlParser::CreateFromPath(exportedSingletonDirectory); + VerifyV1ManifestContent(generatedSingletonManifest, true, ManifestVer{ s_ManifestVersionV1_7 }, true); + + TempDirectory multiFileDirectory{ "MultiFileManifest" }; + CopyTestDataFilesToFolder({ + "ManifestV1_7-MultiFile-Version.yaml", + "ManifestV1_7-MultiFile-Installer.yaml", + "ManifestV1_7-MultiFile-DefaultLocale.yaml", + "ManifestV1_7-MultiFile-Locale.yaml" }, multiFileDirectory); + + Manifest multiFileManifest = YamlParser::CreateFromPath(multiFileDirectory); + TempDirectory exportedMultiFileDirectory{ "exportedMultiFile" }; + std::filesystem::path generatedMultiFileManifestPath = exportedMultiFileDirectory.GetPath() / "testMultiFileManifest.yaml"; + YamlWriter::OutputYamlFile(multiFileManifest, multiFileManifest.Installers[0], generatedMultiFileManifestPath); + + REQUIRE(std::filesystem::exists(generatedMultiFileManifestPath)); + Manifest generatedMultiFileManifest = YamlParser::CreateFromPath(exportedMultiFileDirectory); + VerifyV1ManifestContent(generatedMultiFileManifest, false, ManifestVer{ s_ManifestVersionV1_7 }, true); +} + YamlManifestInfo CreateYamlManifestInfo(std::string testDataFile) { YamlManifestInfo result; @@ -1469,7 +1531,7 @@ TEST_CASE("ManifestArpVersionRange", "[ManifestValidation]") { Manifest manifestNoArp = YamlParser::CreateFromPath(TestDataFile("Manifest-Good-NoArpVersionDeclared.yaml")); REQUIRE(manifestNoArp.GetArpVersionRange().IsEmpty()); - + Manifest manifestSingleArp = YamlParser::CreateFromPath(TestDataFile("Manifest-Good-SingleArpVersionDeclared.yaml")); auto arpRangeSingleArp = manifestSingleArp.GetArpVersionRange(); REQUIRE(arpRangeSingleArp.GetMinVersion().ToString() == "11.0"); diff --git a/src/AppInstallerCommonCore/Manifest/ManifestCommon.cpp b/src/AppInstallerCommonCore/Manifest/ManifestCommon.cpp @@ -576,6 +576,8 @@ namespace AppInstaller::Manifest return "InstallLocation"sv; case InstallerSwitchType::Update: return "Upgrade"sv; + case InstallerSwitchType::Repair: + return "Repair"sv; } return "Unknown"sv; @@ -652,6 +654,21 @@ namespace AppInstaller::Manifest return "unknown"sv; } + std::string_view RepairBehaviorToString(RepairBehaviorEnum repairBehavior) + { + switch (repairBehavior) + { + case AppInstaller::Manifest::RepairBehaviorEnum::Modify: + return "modify"sv; + case AppInstaller::Manifest::RepairBehaviorEnum::Installer: + return "installer"sv; + case AppInstaller::Manifest::RepairBehaviorEnum::Uninstaller: + return "uninstaller"sv; + } + + return "unknown"sv; + } + std::string_view ScopeToString(ScopeEnum scope) { switch (scope) @@ -962,6 +979,27 @@ namespace AppInstaller::Manifest } } + RepairBehaviorEnum ConvertToRepairBehaviorEnum(std::string_view in) + { + std::string inStrLower = Utility::ToLower(in); + RepairBehaviorEnum result = RepairBehaviorEnum::Unknown; + + if (inStrLower == "installer") + { + result = RepairBehaviorEnum::Installer; + } + else if (inStrLower == "uninstaller") + { + result = RepairBehaviorEnum::Uninstaller; + } + else if (inStrLower == "modify") + { + result = RepairBehaviorEnum::Modify; + } + + return result; + } + std::map<DWORD, ExpectedReturnCodeEnum> GetDefaultKnownReturnCodes(InstallerTypeEnum installerType) { switch (installerType) diff --git a/src/AppInstallerCommonCore/Manifest/ManifestYamlPopulator.cpp b/src/AppInstallerCommonCore/Manifest/ManifestYamlPopulator.cpp @@ -329,6 +329,16 @@ namespace AppInstaller::Manifest std::move(fields_v1_6.begin(), fields_v1_6.end(), std::inserter(result, result.end())); } + + if (manifestVersion >= ManifestVer{ s_ManifestVersionV1_7 }) + { + std::vector<FieldProcessInfo> fields_v1_7 = + { + { "RepairBehavior", [this](const YAML::Node& value)->ValidationErrors { m_p_installer->RepairBehavior = ConvertToRepairBehaviorEnum(value.as<std::string>()); return {}; } }, + }; + + std::move(fields_v1_7.begin(), fields_v1_7.end(), std::inserter(result, result.end())); + } } return result; @@ -357,6 +367,11 @@ namespace AppInstaller::Manifest else if (manifestVersion.Major() == 1) { result.emplace_back("Upgrade", [this](const YAML::Node& value)->ValidationErrors { (*m_p_switches)[InstallerSwitchType::Update] = value.as<std::string>(); return{}; }); + + if (manifestVersion >= ManifestVer{ s_ManifestVersionV1_7 }) + { + result.emplace_back("Repair", [this](const YAML::Node& value)->ValidationErrors { (*m_p_switches)[InstallerSwitchType::Repair] = value.as<std::string>(); return{}; }); + }; } return result; diff --git a/src/AppInstallerCommonCore/Manifest/YamlWriter.cpp b/src/AppInstallerCommonCore/Manifest/YamlWriter.cpp @@ -66,6 +66,7 @@ namespace AppInstaller::Manifest::YamlWriter constexpr std::string_view DisplayName = "DisplayName"sv; constexpr std::string_view MinimumOSVersion = "MinimumOSVersion"sv; constexpr std::string_view DownloadCommandProhibited = "DownloadCommandProhibited"sv; + constexpr std::string_view RepairBehavior = "RepairBehavior"sv; // Installer switches constexpr std::string_view InstallerSwitches = "InstallerSwitches"sv; @@ -76,6 +77,7 @@ namespace AppInstaller::Manifest::YamlWriter constexpr std::string_view Log = "Log"sv; constexpr std::string_view Upgrade = "Upgrade"sv; constexpr std::string_view Custom = "Custom"sv; + constexpr std::string_view Repair = "Repair"sv; constexpr std::string_view InstallerSuccessCodes = "InstallerSuccessCodes"sv; constexpr std::string_view UpgradeBehavior = "UpgradeBehavior"sv; @@ -570,6 +572,7 @@ namespace AppInstaller::Manifest::YamlWriter WRITE_PROPERTY_IF_EXISTS(out, MinimumOSVersion, installer.MinOSVersion); WRITE_PROPERTY_IF_EXISTS(out, ProductCode, installer.ProductCode); WRITE_PROPERTY_IF_EXISTS(out, UpgradeBehavior, UpdateBehaviorToString(installer.UpdateBehavior)); + WRITE_PROPERTY_IF_EXISTS(out, RepairBehavior, RepairBehaviorToString(installer.RepairBehavior)); ProcessSequence(out, Capabilities, installer.Capabilities); ProcessSequence(out, Commands, installer.Commands); diff --git a/src/AppInstallerCommonCore/Public/winget/ManifestCommon.h b/src/AppInstallerCommonCore/Public/winget/ManifestCommon.h @@ -112,6 +112,15 @@ namespace AppInstaller::Manifest Log, InstallLocation, Update, + Repair, + }; + + enum class RepairBehaviorEnum + { + Unknown, + Modify, + Installer, + Uninstaller, }; enum class ScopeEnum @@ -370,6 +379,8 @@ namespace AppInstaller::Manifest IconResolutionEnum ConvertToIconResolutionEnum(std::string_view in); + RepairBehaviorEnum ConvertToRepairBehaviorEnum(std::string_view in); + std::string_view InstallerTypeToString(InstallerTypeEnum installerType); std::string_view InstallerSwitchTypeToString(InstallerSwitchType installerSwitchType); @@ -382,6 +393,8 @@ namespace AppInstaller::Manifest std::string_view UpdateBehaviorToString(UpdateBehaviorEnum updateBehavior); + std::string_view RepairBehaviorToString(RepairBehaviorEnum repairBehavior); + std::string_view PlatformToString(PlatformEnum platform); std::string_view ScopeToString(ScopeEnum scope); diff --git a/src/AppInstallerCommonCore/Public/winget/ManifestInstaller.h b/src/AppInstallerCommonCore/Public/winget/ManifestInstaller.h @@ -70,6 +70,8 @@ namespace AppInstaller::Manifest UpdateBehaviorEnum UpdateBehavior = UpdateBehaviorEnum::Install; + RepairBehaviorEnum RepairBehavior = RepairBehaviorEnum::Unknown; + std::vector<string_t> Commands; std::vector<string_t> Protocols;