diff --git a/doc/ReleaseNotes.md b/doc/ReleaseNotes.md index fcf8c6e1d3..72afc0f4d3 100644 --- a/doc/ReleaseNotes.md +++ b/doc/ReleaseNotes.md @@ -2,6 +2,7 @@ * MCP server available; run `winget mcp` for assistance on configuring your client. * App Installer now uses WinUI 3. The package dependency on WinUI 2 has been replaced by a dependency on the Windows App Runtime 1.7. * Manifest schema and validation updated to v1.12. This version update adds "Font" as an InstallerType and NestedInstallerType. +* Added UninstallerSwitches and UninstallerSuccessCodes to the manifest schema for v1.12. ## Bug Fixes * Manifest validation no longer fails using `UTF-8 BOM` encoding when the schema header is on the first line diff --git a/schemas/JSON/manifests/latest/manifest.installer.latest.json b/schemas/JSON/manifests/latest/manifest.installer.latest.json index 2f634646d9..d615f2d84e 100644 --- a/schemas/JSON/manifests/latest/manifest.installer.latest.json +++ b/schemas/JSON/manifests/latest/manifest.installer.latest.json @@ -217,6 +217,60 @@ "uniqueItems": true, "description": "List of additional non-zero installer success exit codes other than known default values by winget" }, + "UninstallerSwitches": { + "type": "object", + "properties": { + "Silent": { + "type": [ "string", "null" ], + "minLength": 1, + "maxLength": 512, + "description": "Silent is the value that should be passed to the uninstaller when user chooses a silent or quiet uninstall" + }, + "SilentWithProgress": { + "type": [ "string", "null" ], + "minLength": 1, + "maxLength": 512, + "description": "SilentWithProgress is the value that should be passed to the uninstaller when user chooses a non-interactive uninstall" + }, + "Interactive": { + "type": [ "string", "null" ], + "minLength": 1, + "maxLength": 512, + "description": "Interactive is the value that should be passed to the uninstaller when user chooses an interactive uninstall" + }, + "Log": { + "type": [ "string", "null" ], + "minLength": 1, + "maxLength": 512, + "description": "Log is the value passed to the uninstaller for custom log file path. token can be included in the switch value so that winget will replace the token with user provided path" + }, + "Custom": { + "type": [ "string", "null" ], + "minLength": 1, + "maxLength": 2048, + "description": "Custom switches will be passed directly to the uninstaller by winget" + } + } + }, + "UninstallerReturnCode": { + "type": "integer", + "format": "long", + "not": { + "enum": [ 0 ] + }, + "minimum": -2147483648, + "maximum": 4294967295, + "description": "An exit code that can be returned by the uninstaller after execution" + }, + "UninstallerSuccessCodes": { + "type": [ "array", "null" ], + "items": { + "$ref": "#/definitions/UninstallerReturnCode" + }, + "maxItems": 16, + "uniqueItems": true, + "description": "List of additional non-zero uninstaller success exit codes other than known default values by winget" + }, "ExpectedReturnCodes": { "type": [ "array", "null" ], "items": { @@ -765,6 +819,12 @@ }, "Authentication": { "$ref": "#/definitions/Authentication" + }, + "UninstallerSwitches": { + "$ref": "#/definitions/UninstallerSwitches" + }, + "UninstallerSuccessCodes": { + "$ref": "#/definitions/UninstallerSuccessCodes" } }, "required": [ @@ -890,6 +950,12 @@ "Authentication": { "$ref": "#/definitions/Authentication" }, + "UninstallerSwitches": { + "$ref": "#/definitions/UninstallerSwitches" + }, + "UninstallerSuccessCodes": { + "$ref": "#/definitions/UninstallerSuccessCodes" + }, "Installers": { "type": "array", "items": { diff --git a/schemas/JSON/manifests/latest/manifest.singleton.latest.json b/schemas/JSON/manifests/latest/manifest.singleton.latest.json index 9a44532d61..edea765ed9 100644 --- a/schemas/JSON/manifests/latest/manifest.singleton.latest.json +++ b/schemas/JSON/manifests/latest/manifest.singleton.latest.json @@ -319,6 +319,60 @@ "uniqueItems": true, "description": "List of additional non-zero installer success exit codes other than known default values by winget" }, + "UninstallerSwitches": { + "type": "object", + "properties": { + "Silent": { + "type": [ "string", "null" ], + "minLength": 1, + "maxLength": 512, + "description": "Silent is the value that should be passed to the uninstaller when user chooses a silent or quiet uninstall" + }, + "SilentWithProgress": { + "type": [ "string", "null" ], + "minLength": 1, + "maxLength": 512, + "description": "SilentWithProgress is the value that should be passed to the uninstaller when user chooses a non-interactive uninstall" + }, + "Interactive": { + "type": [ "string", "null" ], + "minLength": 1, + "maxLength": 512, + "description": "Interactive is the value that should be passed to the uninstaller when user chooses an interactive uninstall" + }, + "Log": { + "type": [ "string", "null" ], + "minLength": 1, + "maxLength": 512, + "description": "Log is the value passed to the uninstaller for custom log file path. token can be included in the switch value so that winget will replace the token with user provided path" + }, + "Custom": { + "type": [ "string", "null" ], + "minLength": 1, + "maxLength": 2048, + "description": "Custom switches will be passed directly to the uninstaller by winget" + } + } + }, + "UninstallerReturnCode": { + "type": "integer", + "format": "long", + "not": { + "enum": [ 0 ] + }, + "minimum": -2147483648, + "maximum": 4294967295, + "description": "An exit code that can be returned by the uninstaller after execution" + }, + "UninstallerSuccessCodes": { + "type": [ "array", "null" ], + "items": { + "$ref": "#/definitions/UninstallerReturnCode" + }, + "maxItems": 16, + "uniqueItems": true, + "description": "List of additional non-zero uninstaller success exit codes other than known default values by winget" + }, "ExpectedReturnCodes": { "type": [ "array", "null" ], "items": { @@ -866,6 +920,12 @@ }, "Authentication": { "$ref": "#/definitions/Authentication" + }, + "UninstallerSwitches": { + "$ref": "#/definitions/UninstallerSwitches" + }, + "UninstallerSuccessCodes": { + "$ref": "#/definitions/UninstallerSuccessCodes" } }, "required": [ @@ -1114,6 +1174,12 @@ "Authentication": { "$ref": "#/definitions/Authentication" }, + "UninstallerSwitches": { + "$ref": "#/definitions/UninstallerSwitches" + }, + "UninstallerSuccessCodes": { + "$ref": "#/definitions/UninstallerSuccessCodes" + }, "Installers": { "type": "array", "items": { diff --git a/src/AppInstallerCLITests/RestInterface_1_12.cpp b/src/AppInstallerCLITests/RestInterface_1_12.cpp index fb55136924..f67922a695 100644 --- a/src/AppInstallerCLITests/RestInterface_1_12.cpp +++ b/src/AppInstallerCLITests/RestInterface_1_12.cpp @@ -147,6 +147,16 @@ namespace "InstallerSuccessCodes": [ 0 ], + "UninstallerSwitches": { + "Silent": "/s", + "SilentWithProgress": "/s", + "Interactive": "/i", + "Log": "/l", + "Custom": "/custom" + }, + "UninstallerSuccessCodes": [ + 1 + ], "UpgradeBehavior": "deny", "Commands": [ "command1" @@ -337,6 +347,14 @@ namespace REQUIRE(actualInstaller.Switches.at(InstallerSwitchType::Repair) == "/repair"); REQUIRE(actualInstaller.InstallerSuccessCodes.size() == 1); REQUIRE(actualInstaller.InstallerSuccessCodes.at(0) == 0); + REQUIRE(actualInstaller.UninstallerSwitches.size() == 5); + REQUIRE(actualInstaller.UninstallerSwitches.at(UninstallerSwitchType::Silent) == "/s"); + REQUIRE(actualInstaller.UninstallerSwitches.at(UninstallerSwitchType::SilentWithProgress) == "/s"); + REQUIRE(actualInstaller.UninstallerSwitches.at(UninstallerSwitchType::Interactive) == "/i"); + REQUIRE(actualInstaller.UninstallerSwitches.at(UninstallerSwitchType::Log) == "/l"); + REQUIRE(actualInstaller.UninstallerSwitches.at(UninstallerSwitchType::Custom) == "/custom"); + REQUIRE(actualInstaller.UninstallerSuccessCodes.size() == 1); + REQUIRE(actualInstaller.UninstallerSuccessCodes.at(0) == 1); REQUIRE(actualInstaller.UpdateBehavior == UpdateBehaviorEnum::Deny); REQUIRE(actualInstaller.Commands.at(0) == "command1"); REQUIRE(actualInstaller.Protocols.at(0) == "protocol1"); diff --git a/src/AppInstallerCLITests/TestData/ManifestV1_12-Singleton.yaml b/src/AppInstallerCLITests/TestData/ManifestV1_12-Singleton.yaml index 91fa227d69..9541e567bd 100644 --- a/src/AppInstallerCLITests/TestData/ManifestV1_12-Singleton.yaml +++ b/src/AppInstallerCLITests/TestData/ManifestV1_12-Singleton.yaml @@ -57,9 +57,18 @@ InstallerSwitches: InstallLocation: /dir= Upgrade: /upgrade Repair: /repair +UninstallerSwitches: + Custom: /custom + SilentWithProgress: /silentwithprogress + Silent: /silence + Interactive: /interactive + Log: /log= InstallerSuccessCodes: - 1 - 0x80070005 +UninstallerSuccessCodes: + - 2 + - 0x80070002 UpgradeBehavior: uninstallPrevious RepairBehavior: modify Commands: @@ -150,6 +159,12 @@ Installers: InstallLocation: /d= Upgrade: /u Repair: /r + UninstallerSwitches: + Custom: /c + SilentWithProgress: /sp + Silent: /s + Interactive: /i + Log: /l= UpgradeBehavior: install Commands: - makemsixPreview diff --git a/src/AppInstallerCLITests/TestData/MultiFileManifestV1_12/ManifestV1_12-MultiFile-Installer.yaml b/src/AppInstallerCLITests/TestData/MultiFileManifestV1_12/ManifestV1_12-MultiFile-Installer.yaml index b5844cc2d9..f7ffeec21c 100644 --- a/src/AppInstallerCLITests/TestData/MultiFileManifestV1_12/ManifestV1_12-MultiFile-Installer.yaml +++ b/src/AppInstallerCLITests/TestData/MultiFileManifestV1_12/ManifestV1_12-MultiFile-Installer.yaml @@ -22,9 +22,18 @@ InstallerSwitches: InstallLocation: /dir= Upgrade: /upgrade Repair: /repair +UninstallerSwitches: + Custom: /custom + SilentWithProgress: /silentwithprogress + Silent: /silence + Interactive: /interactive + Log: /log= InstallerSuccessCodes: - 1 - 0x80070005 +UninstallerSuccessCodes: + - 2 + - 0x80070002 UpgradeBehavior: uninstallPrevious RepairBehavior: modify Commands: @@ -115,6 +124,12 @@ Installers: InstallLocation: /d= Upgrade: /u Repair: /r + UninstallerSwitches: + Custom: /c + SilentWithProgress: /sp + Silent: /s + Interactive: /i + Log: /l= UpgradeBehavior: install Commands: - makemsixPreview @@ -170,6 +185,9 @@ Installers: ProductCode: "{Bar}" InstallerSwitches: Repair: /r + UninstallerSwitches: + Silent: /s + Custom: /foo UpgradeBehavior: deny RepairBehavior: uninstaller - Architecture: x86 diff --git a/src/AppInstallerCLITests/YamlManifest.cpp b/src/AppInstallerCLITests/YamlManifest.cpp index ce1b23b757..18ce5f63fa 100644 --- a/src/AppInstallerCLITests/YamlManifest.cpp +++ b/src/AppInstallerCLITests/YamlManifest.cpp @@ -266,6 +266,16 @@ namespace { REQUIRE(manifest.DefaultInstallerInfo.ArchiveBinariesDependOnPath); } + + if (manifestVer >= ManifestVer{ s_ManifestVersionV1_12 }) + { + auto defaultUninstallerSwitches = manifest.DefaultInstallerInfo.UninstallerSwitches; + REQUIRE(defaultUninstallerSwitches.at(UninstallerSwitchType::Custom) == "/custom"); + REQUIRE(defaultUninstallerSwitches.at(UninstallerSwitchType::SilentWithProgress) == "/silentwithprogress"); + REQUIRE(defaultUninstallerSwitches.at(UninstallerSwitchType::Silent) == "/silence"); + REQUIRE(defaultUninstallerSwitches.at(UninstallerSwitchType::Interactive) == "/interactive"); + REQUIRE(defaultUninstallerSwitches.at(UninstallerSwitchType::Log) == "/log="); + } } if (isSingleton || isExported) @@ -384,6 +394,16 @@ namespace REQUIRE(installer1.RepairBehavior == RepairBehaviorEnum::Modify); } + if (manifestVer >= ManifestVer{ s_ManifestVersionV1_12 }) + { + auto installer1UninstallSwitches = installer1.UninstallerSwitches; + REQUIRE(installer1UninstallSwitches.at(UninstallerSwitchType::Custom) == "/c"); + REQUIRE(installer1UninstallSwitches.at(UninstallerSwitchType::SilentWithProgress) == "/sp"); + REQUIRE(installer1UninstallSwitches.at(UninstallerSwitchType::Silent) == "/s"); + REQUIRE(installer1UninstallSwitches.at(UninstallerSwitchType::Interactive) == "/i"); + REQUIRE(installer1UninstallSwitches.at(UninstallerSwitchType::Log) == "/l="); + } + if (manifestVer >= ManifestVer{ s_ManifestVersionV1_9 }) { REQUIRE_FALSE(installer1.ArchiveBinariesDependOnPath); diff --git a/src/AppInstallerCommonCore/Manifest/ManifestCommon.cpp b/src/AppInstallerCommonCore/Manifest/ManifestCommon.cpp index 491d165722..aa4bac71c8 100644 --- a/src/AppInstallerCommonCore/Manifest/ManifestCommon.cpp +++ b/src/AppInstallerCommonCore/Manifest/ManifestCommon.cpp @@ -621,6 +621,25 @@ namespace AppInstaller::Manifest return "Unknown"sv; } + std::string_view UninstallerSwitchTypeToString(UninstallerSwitchType uninstallerSwitchType) + { + switch (uninstallerSwitchType) + { + case UninstallerSwitchType::Custom: + return "Custom"sv; + case UninstallerSwitchType::Silent: + return "Silent"sv; + case UninstallerSwitchType::SilentWithProgress: + return "SilentWithProgress"sv; + case UninstallerSwitchType::Interactive: + return "Interactive"sv; + case UninstallerSwitchType::Log: + return "Log"sv; + } + + return "Unknown"sv; + } + std::string_view ElevationRequirementToString(ElevationRequirementEnum elevationRequirement) { switch (elevationRequirement) diff --git a/src/AppInstallerCommonCore/Manifest/ManifestSchemaValidation.cpp b/src/AppInstallerCommonCore/Manifest/ManifestSchemaValidation.cpp index 54ff6bbdb8..fb26df8ff7 100644 --- a/src/AppInstallerCommonCore/Manifest/ManifestSchemaValidation.cpp +++ b/src/AppInstallerCommonCore/Manifest/ManifestSchemaValidation.cpp @@ -33,7 +33,8 @@ namespace AppInstaller::Manifest::YamlParser { "DisplayInstallWarnings"sv, YamlScalarType::Bool }, { "InstallerReturnCode"sv, YamlScalarType::Int }, { "DownloadCommandProhibited", YamlScalarType::Bool }, - { "ArchiveBinariesDependOnPath", YamlScalarType::Bool } + { "ArchiveBinariesDependOnPath", YamlScalarType::Bool }, + { "UninstallerSuccessCodes"sv, YamlScalarType::Int }, }; YamlScalarType GetManifestScalarValueType(const std::string& key) diff --git a/src/AppInstallerCommonCore/Manifest/ManifestYamlPopulator.cpp b/src/AppInstallerCommonCore/Manifest/ManifestYamlPopulator.cpp index 2d46f60403..092ff5c813 100644 --- a/src/AppInstallerCommonCore/Manifest/ManifestYamlPopulator.cpp +++ b/src/AppInstallerCommonCore/Manifest/ManifestYamlPopulator.cpp @@ -398,6 +398,17 @@ namespace AppInstaller::Manifest std::move(fields_v1_10.begin(), fields_v1_10.end(), std::inserter(result, result.end())); } + + if (m_manifestVersion.get() >= ManifestVer{ s_ManifestVersionV1_12 }) + { + std::vector fields_v1_12 = + { + { "UninstallerSwitches", [this](const YAML::Node& value, const VariantManifestPtr& v)->ValidationErrors { return ValidateAndProcessFields(value, UninstallerSwitchesFieldInfos, VariantManifestPtr(&(GetManifestInstallerPtr(v)->UninstallerSwitches))); }}, + { "UninstallerSuccessCodes", [](const YAML::Node& value, const VariantManifestPtr& v)->ValidationErrors { GetManifestInstallerPtr(v)->UninstallerSuccessCodes = ProcessInstallerSuccessCodeSequenceNode(value); return {}; } }, + }; + + std::move(fields_v1_12.begin(), fields_v1_12.end(), std::inserter(result, result.end())); + } } return result; @@ -436,6 +447,27 @@ namespace AppInstaller::Manifest return result; } + std::vector ManifestYamlPopulator::GetUninstallerSwitchesFieldProcessInfo() + { + std::vector result = {}; + + if (m_manifestVersion.get() >= ManifestVer{ s_ManifestVersionV1_12 }) + { + std::vector uninstallerSwitches_v1_12 = + { + { "Custom", [](const YAML::Node& value, const VariantManifestPtr& v)->ValidationErrors { (*variant_ptr>(v))[UninstallerSwitchType::Custom] = value.as(); return{}; } }, + { "Silent", [](const YAML::Node& value, const VariantManifestPtr& v)->ValidationErrors { (*variant_ptr>(v))[UninstallerSwitchType::Silent] = value.as(); return{}; } }, + { "SilentWithProgress", [](const YAML::Node& value, const VariantManifestPtr& v)->ValidationErrors { (*variant_ptr>(v))[UninstallerSwitchType::SilentWithProgress] = value.as(); return{}; } }, + { "Interactive", [](const YAML::Node& value, const VariantManifestPtr& v)->ValidationErrors { (*variant_ptr>(v))[UninstallerSwitchType::Interactive] = value.as(); return{}; } }, + { "Log", [](const YAML::Node& value, const VariantManifestPtr& v)->ValidationErrors { (*variant_ptr>(v))[UninstallerSwitchType::Log] = value.as(); return{}; } }, + }; + + std::move(uninstallerSwitches_v1_12.begin(), uninstallerSwitches_v1_12.end(), std::inserter(result, result.end())); + } + + return result; + } + std::vector ManifestYamlPopulator::GetExpectedReturnCodesFieldProcessInfo() { std::vector result = {}; @@ -1100,6 +1132,7 @@ namespace AppInstaller::Manifest RootFieldInfos = GetRootFieldProcessInfo(); InstallerFieldInfos = GetInstallerFieldProcessInfo(); SwitchesFieldInfos = GetSwitchesFieldProcessInfo(); + UninstallerSwitchesFieldInfos = GetUninstallerSwitchesFieldProcessInfo(); ExpectedReturnCodesFieldInfos = GetExpectedReturnCodesFieldProcessInfo(); DependenciesFieldInfos = GetDependenciesFieldProcessInfo(); PackageDependenciesFieldInfos = GetPackageDependenciesFieldProcessInfo(); diff --git a/src/AppInstallerCommonCore/Manifest/YamlWriter.cpp b/src/AppInstallerCommonCore/Manifest/YamlWriter.cpp index 4483fbdaba..1853c7f0b1 100644 --- a/src/AppInstallerCommonCore/Manifest/YamlWriter.cpp +++ b/src/AppInstallerCommonCore/Manifest/YamlWriter.cpp @@ -75,7 +75,8 @@ namespace AppInstaller::Manifest::YamlWriter constexpr std::string_view MicrosoftEntraIdScope = "Scope"sv; // Installer switches - constexpr std::string_view InstallerSwitches = "InstallerSwitches"sv; + constexpr std::string_view InstallerSwitches = "InstallerSwitches"sv; + constexpr std::string_view UninstallerSwitches = "UninstallerSwitches"sv; constexpr std::string_view Silent = "Silent"sv; constexpr std::string_view SilentWithProgress = "SilentWithProgress"sv; constexpr std::string_view Interactive = "Interactive"sv; @@ -85,7 +86,8 @@ namespace AppInstaller::Manifest::YamlWriter 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 InstallerSuccessCodes = "InstallerSuccessCodes"sv; + constexpr std::string_view UninstallerSuccessCodes = "UninstallerSuccessCodes"sv; constexpr std::string_view UpgradeBehavior = "UpgradeBehavior"sv; constexpr std::string_view Commands = "Commands"sv; constexpr std::string_view Protocols = "Protocols"sv; @@ -318,6 +320,22 @@ namespace AppInstaller::Manifest::YamlWriter WRITE_PROPERTY_IF_EXISTS(out, InstallerSwitchTypeToString(type), value); } out << YAML::EndMap; + } + + void ProcessUninstallerSwitches(YAML::Emitter& out, const std::map& uninstallerSwitches) + { + if (uninstallerSwitches.empty()) + { + return; + } + + out << YAML::Key << UninstallerSwitches; + out << YAML::BeginMap; + for (auto const& [type, value] : uninstallerSwitches) + { + WRITE_PROPERTY_IF_EXISTS(out, UninstallerSwitchTypeToString(type), value); + } + out << YAML::EndMap; } void ProcessExpectedReturnCodes(YAML::Emitter& out, const std::map& expectedReturnCodes) @@ -430,6 +448,23 @@ namespace AppInstaller::Manifest::YamlWriter } out << YAML::EndSeq; } + + + void ProcessUninstallerSuccessCodes(YAML::Emitter& out, const std::vector& uninstallerSuccessCodes) + { + if (uninstallerSuccessCodes.empty()) + { + return; + } + + out << YAML::Key << UninstallerSuccessCodes; + out << YAML::BeginSeq; + for (auto const& uninstallerSuccessCode : uninstallerSuccessCodes) + { + out << std::to_string(uninstallerSuccessCode); + } + out << YAML::EndSeq; + } void ProcessMarkets(YAML::Emitter& out, const MarketsInfo& marketsInfo) { @@ -631,7 +666,9 @@ namespace AppInstaller::Manifest::YamlWriter ProcessPlatforms(out, installer.Platform); ProcessUnsupportedArguments(out, installer.UnsupportedArguments); ProcessUnsupportedOSArchitecture(out, installer.UnsupportedOSArchitectures); - ProcessAuthentication(out, installer.AuthInfo); + ProcessAuthentication(out, installer.AuthInfo); + ProcessUninstallerSwitches(out, installer.UninstallerSwitches); + ProcessUninstallerSuccessCodes(out, installer.UninstallerSuccessCodes); } void ProcessInstaller(YAML::Emitter& out, const ManifestInstaller& installer) diff --git a/src/AppInstallerCommonCore/Public/winget/ManifestCommon.h b/src/AppInstallerCommonCore/Public/winget/ManifestCommon.h index f987b7705d..36d4e64fe2 100644 --- a/src/AppInstallerCommonCore/Public/winget/ManifestCommon.h +++ b/src/AppInstallerCommonCore/Public/winget/ManifestCommon.h @@ -128,6 +128,15 @@ namespace AppInstaller::Manifest Repair, }; + enum class UninstallerSwitchType + { + Custom, + Silent, + SilentWithProgress, + Interactive, + Log, + }; + enum class RepairBehaviorEnum { Unknown, @@ -404,6 +413,8 @@ namespace AppInstaller::Manifest std::string_view InstallerSwitchTypeToString(InstallerSwitchType installerSwitchType); + std::string_view UninstallerSwitchTypeToString(UninstallerSwitchType uninstallerSwitchType); + std::string_view ElevationRequirementToString(ElevationRequirementEnum elevationRequirement); std::string_view InstallModeToString(InstallModeEnum installMode); diff --git a/src/AppInstallerCommonCore/Public/winget/ManifestInstaller.h b/src/AppInstallerCommonCore/Public/winget/ManifestInstaller.h index 9812933410..31ccbe8844 100644 --- a/src/AppInstallerCommonCore/Public/winget/ManifestInstaller.h +++ b/src/AppInstallerCommonCore/Public/winget/ManifestInstaller.h @@ -61,6 +61,10 @@ namespace AppInstaller::Manifest std::vector InstallerSuccessCodes; + std::map UninstallerSwitches; + + std::vector UninstallerSuccessCodes; + struct ExpectedReturnCodeInfo { ExpectedReturnCodeEnum ReturnResponseEnum = ExpectedReturnCodeEnum::Unknown; diff --git a/src/AppInstallerCommonCore/Public/winget/ManifestYamlPopulator.h b/src/AppInstallerCommonCore/Public/winget/ManifestYamlPopulator.h index db7775afe2..e2c2be39a7 100644 --- a/src/AppInstallerCommonCore/Public/winget/ManifestYamlPopulator.h +++ b/src/AppInstallerCommonCore/Public/winget/ManifestYamlPopulator.h @@ -8,7 +8,7 @@ namespace AppInstaller::Manifest { // Add here new manifest pointer types. - using VariantManifestPtr = std::variant*, AppInstaller::Authentication::AuthenticationInfo*, AppInstaller::Authentication::MicrosoftEntraIdAuthenticationInfo*>; + using VariantManifestPtr = std::variant*, AppInstaller::Authentication::AuthenticationInfo*, AppInstaller::Authentication::MicrosoftEntraIdAuthenticationInfo*, std::map*>; struct ManifestYamlPopulator { @@ -43,6 +43,7 @@ namespace AppInstaller::Manifest std::vector RootFieldInfos; std::vector InstallerFieldInfos; std::vector SwitchesFieldInfos; + std::vector UninstallerSwitchesFieldInfos; std::vector ExpectedReturnCodesFieldInfos; std::vector DependenciesFieldInfos; std::vector PackageDependenciesFieldInfos; @@ -65,6 +66,7 @@ namespace AppInstaller::Manifest std::vector GetRootFieldProcessInfo(); std::vector GetInstallerFieldProcessInfo(bool forRootFields = false); std::vector GetSwitchesFieldProcessInfo(); + std::vector GetUninstallerSwitchesFieldProcessInfo(); std::vector GetExpectedReturnCodesFieldProcessInfo(); std::vector GetDependenciesFieldProcessInfo(); std::vector GetPackageDependenciesFieldProcessInfo(); diff --git a/src/AppInstallerRepositoryCore/Rest/Schema/1_12/Json/ManifestDeserializer.h b/src/AppInstallerRepositoryCore/Rest/Schema/1_12/Json/ManifestDeserializer.h index eeffe76067..30d3d3487d 100644 --- a/src/AppInstallerRepositoryCore/Rest/Schema/1_12/Json/ManifestDeserializer.h +++ b/src/AppInstallerRepositoryCore/Rest/Schema/1_12/Json/ManifestDeserializer.h @@ -10,6 +10,10 @@ namespace AppInstaller::Repository::Rest::Schema::V1_12::Json { protected: + virtual std::map DeserializeUninstallerSwitches(const web::json::value& uninstallerSwitchesJsonObject) const; + + std::optional DeserializeInstaller(const web::json::value& installerJsonObject) const override; + Manifest::InstallerTypeEnum ConvertToInstallerType(std::string_view in) const override; Manifest::ManifestVer GetManifestVersion() const override; diff --git a/src/AppInstallerRepositoryCore/Rest/Schema/1_12/Json/ManifestDeserializer_1_12.cpp b/src/AppInstallerRepositoryCore/Rest/Schema/1_12/Json/ManifestDeserializer_1_12.cpp index a4a0beeec3..2d4980842e 100644 --- a/src/AppInstallerRepositoryCore/Rest/Schema/1_12/Json/ManifestDeserializer_1_12.cpp +++ b/src/AppInstallerRepositoryCore/Rest/Schema/1_12/Json/ManifestDeserializer_1_12.cpp @@ -9,6 +9,81 @@ using namespace AppInstaller::Manifest; namespace AppInstaller::Repository::Rest::Schema::V1_12::Json { + namespace + { + // Uninstaller switches + constexpr std::string_view UninstallerSwitches = "UninstallerSwitches"sv; + constexpr std::string_view Silent = "Silent"sv; + constexpr std::string_view SilentWithProgress = "SilentWithProgress"sv; + constexpr std::string_view Interactive = "Interactive"sv; + constexpr std::string_view Log = "Log"sv; + constexpr std::string_view Custom = "Custom"sv; + + // Uninstaller Success Codes + constexpr std::string_view UninstallerSuccessCodes = "UninstallerSuccessCodes"sv; + + void TryParseUninstallerSwitchField( + std::map& uninstallerSwitches, + UninstallerSwitchType switchType, + const web::json::value& switchesJsonObject, + std::string_view switchJsonFieldName) + { + auto value = JSON::GetRawStringValueFromJsonNode(switchesJsonObject, JSON::GetUtilityString(switchJsonFieldName)); + + if (JSON::IsValidNonEmptyStringValue(value)) + { + uninstallerSwitches[switchType] = value.value(); + } + } + } + + std::map ManifestDeserializer::DeserializeUninstallerSwitches(const web::json::value& uninstallerSwitchesJsonObject) const + { + std::map uninstallerSwitches; + + TryParseUninstallerSwitchField(uninstallerSwitches, UninstallerSwitchType::Silent, uninstallerSwitchesJsonObject, Silent); + TryParseUninstallerSwitchField(uninstallerSwitches, UninstallerSwitchType::SilentWithProgress, uninstallerSwitchesJsonObject, SilentWithProgress); + TryParseUninstallerSwitchField(uninstallerSwitches, UninstallerSwitchType::Interactive, uninstallerSwitchesJsonObject, Interactive); + TryParseUninstallerSwitchField(uninstallerSwitches, UninstallerSwitchType::Log, uninstallerSwitchesJsonObject, Log); + TryParseUninstallerSwitchField(uninstallerSwitches, UninstallerSwitchType::Custom, uninstallerSwitchesJsonObject, Custom); + + return uninstallerSwitches; + } + + std::optional ManifestDeserializer::DeserializeInstaller(const web::json::value& installerJsonObject) const + { + auto result = V1_10::Json::ManifestDeserializer::DeserializeInstaller(installerJsonObject); + + if (result) + { + auto& installer = result.value(); + + // Uninstaller Switches + std::optional> uninstallerSwitches = + JSON::GetJsonValueFromNode(installerJsonObject, JSON::GetUtilityString(UninstallerSwitches)); + if (uninstallerSwitches) + { + installer.UninstallerSwitches = DeserializeUninstallerSwitches(uninstallerSwitches.value().get()); + } + + // Uninstaller SuccessCodes + std::optional> uninstallSuccessCodes = JSON::GetRawJsonArrayFromJsonNode(installerJsonObject, JSON::GetUtilityString(UninstallerSuccessCodes)); + if (uninstallSuccessCodes) + { + for (auto& code : uninstallSuccessCodes.value().get()) + { + std::optional codeValue = JSON::GetRawIntValueFromJsonValue(code); + if (codeValue) + { + installer.UninstallerSuccessCodes.emplace_back(std::move(codeValue.value())); + } + } + } + } + + return result; + } + Manifest::InstallerTypeEnum ManifestDeserializer::ConvertToInstallerType(std::string_view in) const { std::string inStrLower = Utility::ToLower(in); diff --git a/src/WinGetUtilInterop.UnitTests/ManifestUnitTest/V1ManifestReadTest.cs b/src/WinGetUtilInterop.UnitTests/ManifestUnitTest/V1ManifestReadTest.cs index 1c3eefaca4..59596d4c36 100644 --- a/src/WinGetUtilInterop.UnitTests/ManifestUnitTest/V1ManifestReadTest.cs +++ b/src/WinGetUtilInterop.UnitTests/ManifestUnitTest/V1ManifestReadTest.cs @@ -197,6 +197,21 @@ private void ValidateManifestFields(Manifest manifest, TestManifestVersion manif Assert.Equal(2, manifest.InstallerSuccessCodes.Count); Assert.Equal(1, manifest.InstallerSuccessCodes[0]); Assert.Equal(0x80070005, manifest.InstallerSuccessCodes[1]); + + if (manifestVersion >= TestManifestVersion.V1_12_0) + { + var defaultUninstallerSwitches = manifest.UninstallerSwitches; + Assert.Equal("/custom", defaultUninstallerSwitches.Custom); + Assert.Equal("/silentwithprogress", defaultUninstallerSwitches.SilentWithProgress); + Assert.Equal("/silence", defaultUninstallerSwitches.Silent); + Assert.Equal("/interactive", defaultUninstallerSwitches.Interactive); + Assert.Equal("/log=", defaultUninstallerSwitches.Log); + + Assert.Equal(2, manifest.UninstallerSuccessCodes.Count); + Assert.Equal(2, manifest.UninstallerSuccessCodes[0]); + Assert.Equal(0x80070002, manifest.UninstallerSuccessCodes[1]); + } + Assert.Equal("uninstallPrevious", manifest.UpgradeBehavior); Assert.Equal(2, manifest.Commands.Count); Assert.Equal("makemsix", manifest.Commands[0]); @@ -425,6 +440,16 @@ private void ValidateManifestFields(Manifest manifest, TestManifestVersion manif Assert.Equal("Scope", installer1.Authentication.MicrosoftEntraIdAuthenticationInfo.Scope); } + if (manifestVersion >= TestManifestVersion.V1_12_0) + { + var installer1UninstallerSwitches = installer1.UninstallerSwitches; + Assert.Equal("/c", installer1UninstallerSwitches.Custom); + Assert.Equal("/sp", installer1UninstallerSwitches.SilentWithProgress); + Assert.Equal("/s", installer1UninstallerSwitches.Silent); + Assert.Equal("/i", installer1UninstallerSwitches.Interactive); + Assert.Equal("/l=", installer1UninstallerSwitches.Log); + } + // Additional Localizations Assert.Single(manifest.Localization); ManifestLocalization localization1 = manifest.Localization[0]; diff --git a/src/WinGetUtilInterop.UnitTests/TestCollateral/V1_12ManifestMerged.yaml b/src/WinGetUtilInterop.UnitTests/TestCollateral/V1_12ManifestMerged.yaml index dd92b92ab9..d695c91398 100644 --- a/src/WinGetUtilInterop.UnitTests/TestCollateral/V1_12ManifestMerged.yaml +++ b/src/WinGetUtilInterop.UnitTests/TestCollateral/V1_12ManifestMerged.yaml @@ -55,9 +55,18 @@ InstallerSwitches: InstallLocation: /dir= Upgrade: /upgrade Repair: /repair +UninstallerSwitches: + Custom: /custom + SilentWithProgress: /silentwithprogress + Silent: /silence + Interactive: /interactive + Log: /log= InstallerSuccessCodes: - 1 - 0x80070005 +UninstallerSuccessCodes: + - 2 + - 0x80070002 UpgradeBehavior: uninstallPrevious Commands: - makemsix @@ -189,6 +198,12 @@ Installers: InstallLocation: /d= Upgrade: /u Repair: /r + UninstallerSwitches: + Custom: /c + SilentWithProgress: /sp + Silent: /s + Interactive: /i + Log: /l= UpgradeBehavior: install Commands: - makemsixPreview diff --git a/src/WinGetUtilInterop/Manifest/V1/Manifest.cs b/src/WinGetUtilInterop/Manifest/V1/Manifest.cs index 9df3343f84..012dfeb905 100644 --- a/src/WinGetUtilInterop/Manifest/V1/Manifest.cs +++ b/src/WinGetUtilInterop/Manifest/V1/Manifest.cs @@ -196,7 +196,12 @@ public class Manifest /// /// Gets or sets the default list of additional installer success codes. /// - public List InstallerSuccessCodes { get; set; } + public List InstallerSuccessCodes { get; set; } + + /// + /// Gets or sets the default list of additional installer success codes. + /// + public List UninstallerSuccessCodes { get; set; } /// /// Gets or sets the default upgrade behavior. @@ -247,7 +252,12 @@ public class Manifest /// Gets or sets the default installer switches. /// [YamlMember(Alias = "InstallerSwitches")] - public InstallerSwitches Switches { get; set; } + public InstallerSwitches Switches { get; set; } + + /// + /// Gets or sets UninstallerSwitches. + /// + public UninstallerSwitches UninstallerSwitches { get; set; } /// /// Gets or sets the default installer markets info. diff --git a/src/WinGetUtilInterop/Manifest/V1/ManifestInstaller.cs b/src/WinGetUtilInterop/Manifest/V1/ManifestInstaller.cs index 172bb0de58..3df4a45459 100644 --- a/src/WinGetUtilInterop/Manifest/V1/ManifestInstaller.cs +++ b/src/WinGetUtilInterop/Manifest/V1/ManifestInstaller.cs @@ -78,7 +78,12 @@ public class ManifestInstaller /// /// Gets or sets the list of additional installer success codes. /// - public List InstallerSuccessCodes { get; set; } + public List InstallerSuccessCodes { get; set; } + + /// + /// Gets or sets the list of additional uninstaller success codes. + /// + public List UninstallerSuccessCodes { get; set; } /// /// Gets or sets the upgrade behavior. @@ -129,7 +134,12 @@ public class ManifestInstaller /// Gets or sets the installer switches. If present, has more precedence than root. /// [YamlMember(Alias = "InstallerSwitches")] - public InstallerSwitches Switches { get; set; } + public InstallerSwitches Switches { get; set; } + + /// + /// Gets or sets UninstallerSwitches. + /// + public UninstallerSwitches UninstallerSwitches { get; set; } /// /// Gets or sets the installer markets info. @@ -278,7 +288,8 @@ public bool Equals(ManifestInstaller other) (this.InstallerLocale == other.InstallerLocale) && (this.Scope == other.Scope) && (this.InstallerType == other.InstallerType) && - (this.Switches == other.Switches) && + (this.Switches == other.Switches) && + (this.UninstallerSwitches == other.UninstallerSwitches) && (this.NestedInstallerType == other.NestedInstallerType) && (this.NestedInstallerFiles == other.NestedInstallerFiles); } @@ -296,7 +307,8 @@ public override int GetHashCode() this.InstallerLocale, this.Scope, this.InstallerType, - this.Switches, + this.Switches, + this.UninstallerSwitches, this.NestedInstallerType, this.NestedInstallerFiles).GetHashCode(); } diff --git a/src/WinGetUtilInterop/Manifest/V1/UninstallerSwitches.cs b/src/WinGetUtilInterop/Manifest/V1/UninstallerSwitches.cs new file mode 100644 index 0000000000..5273ac79b0 --- /dev/null +++ b/src/WinGetUtilInterop/Manifest/V1/UninstallerSwitches.cs @@ -0,0 +1,119 @@ +// ----------------------------------------------------------------------------- +// +// Copyright (c) Microsoft Corporation. Licensed under the MIT License. +// +// ----------------------------------------------------------------------------- + +namespace Microsoft.WinGetUtil.Models.V1 +{ + using System; + + /// + /// Class that contains different types of uninstaller switches like Custom, Silent and Interactive. + /// + public class UninstallerSwitches : IEquatable + { + /// + /// Gets or sets the Custom installer switch. + /// + public string Custom { get; set; } + + /// + /// Gets or sets the Silent installer switch. + /// + public string Silent { get; set; } + + /// + /// Gets or sets the SilentWithProgress installer switch. + /// + public string SilentWithProgress { get; set; } + + /// + /// Gets or sets the Interactive installer switch. + /// + public string Interactive { get; set; } + + /// + /// Gets or sets the Log installer switch. + /// + public string Log { get; set; } + + /// + /// Override op_Equality. + /// + /// left hand side. + /// right hand side. + /// boolean indicating if the objects are equals. + public static bool operator ==(UninstallerSwitches lhs, UninstallerSwitches rhs) + { + return Equals(lhs, rhs); + } + + /// + /// Override op_Inequality. + /// + /// left hand side. + /// right hand side. + /// boolean indicating if the objects are not equals. + public static bool operator !=(UninstallerSwitches lhs, UninstallerSwitches rhs) + { + return !Equals(lhs, rhs); + } + + /// + /// Override object.Equals. + /// + /// other object. + /// boolean indicating if the objects are equals. + public override bool Equals(object other) + { + return (other is UninstallerSwitches) && this.Equals(other as UninstallerSwitches); + } + + /// + /// Implemented IEquitable. + /// + /// other object. + /// boolean indicating if the objects are equals. + public bool Equals(UninstallerSwitches other) + { + // If parameter is null, return false. + if (ReferenceEquals(other, null)) + { + return false; + } + + // Optimization for a common success case. + if (ReferenceEquals(this, other)) + { + return true; + } + + // If run-time types are not exactly the same, return false. + if (this.GetType() != other.GetType()) + { + return false; + } + + return (this.Custom == other.Custom) && + (this.Silent == other.Silent) && + (this.SilentWithProgress == other.SilentWithProgress) && + (this.Interactive == other.Interactive) && + (this.Log == other.Log); + } + + /// + /// Override object.GetHashCode. Implement if needed. + /// to create the hash. + /// + /// resulting hash. + public override int GetHashCode() + { + return (this.Custom, + this.Silent, + this.SilentWithProgress, + this.Interactive, + this.Log).GetHashCode(); + } + } +}