diff --git a/schemas/JSON/settings/settings.schema.0.2.json b/schemas/JSON/settings/settings.schema.0.2.json index 7e19310932..50cb8d1894 100644 --- a/schemas/JSON/settings/settings.schema.0.2.json +++ b/schemas/JSON/settings/settings.schema.0.2.json @@ -118,7 +118,7 @@ "description": "Uninstall settings", "type": "object", "properties": { - "purgePortableApp": { + "purgePortablePackage": { "description": "Controls whether the default behavior for uninstall removes all files and directories relevant to this package. Only applies to the portable installerType.", "type": "boolean", "default": false diff --git a/src/AppInstallerCLICore/AppInstallerCLICore.vcxproj b/src/AppInstallerCLICore/AppInstallerCLICore.vcxproj index 6fb2a6d77a..71fd710f8a 100644 --- a/src/AppInstallerCLICore/AppInstallerCLICore.vcxproj +++ b/src/AppInstallerCLICore/AppInstallerCLICore.vcxproj @@ -281,7 +281,7 @@ - + @@ -333,7 +333,7 @@ - + diff --git a/src/AppInstallerCLICore/AppInstallerCLICore.vcxproj.filters b/src/AppInstallerCLICore/AppInstallerCLICore.vcxproj.filters index 2655bde1cd..5353087471 100644 --- a/src/AppInstallerCLICore/AppInstallerCLICore.vcxproj.filters +++ b/src/AppInstallerCLICore/AppInstallerCLICore.vcxproj.filters @@ -176,7 +176,7 @@ Public - + Workflows @@ -322,7 +322,7 @@ Workflows - + Workflows diff --git a/src/AppInstallerCLICore/Argument.cpp b/src/AppInstallerCLICore/Argument.cpp index 480581a54b..4ca1348064 100644 --- a/src/AppInstallerCLICore/Argument.cpp +++ b/src/AppInstallerCLICore/Argument.cpp @@ -113,7 +113,7 @@ namespace AppInstaller::CLI void Argument::ValidatePackageSelectionArgumentSupplied(const Execution::Args& args) { - for (Args::Type type : { Args::Type::Query, Args::Type::Manifest, Args::Type::Id, Args::Type::Name, Args::Type::Moniker, Args::Type::Tag, Args::Type::Command }) + for (Args::Type type : { Args::Type::Query, Args::Type::Manifest, Args::Type::Id, Args::Type::Name, Args::Type::Moniker, Args::Type::ProductCode, Args::Type::Tag, Args::Type::Command }) { if (args.Contains(type)) { diff --git a/src/AppInstallerCLICore/Commands/UninstallCommand.cpp b/src/AppInstallerCLICore/Commands/UninstallCommand.cpp index 7ba9907a88..bddfd0fa0e 100644 --- a/src/AppInstallerCLICore/Commands/UninstallCommand.cpp +++ b/src/AppInstallerCLICore/Commands/UninstallCommand.cpp @@ -21,12 +21,16 @@ namespace AppInstaller::CLI Argument::ForType(Args::Type::Id), Argument::ForType(Args::Type::Name), Argument::ForType(Args::Type::Moniker), + Argument::ForType(Args::Type::ProductCode), Argument::ForType(Args::Type::Version), Argument::ForType(Args::Type::Channel), Argument::ForType(Args::Type::Source), Argument::ForType(Args::Type::Exact), Argument::ForType(Args::Type::Interactive), Argument::ForType(Args::Type::Silent), + Argument::ForType(Args::Type::HashOverride), // TODO: Replace with proper name when behavior changes. + Argument::ForType(Args::Type::Purge), + Argument::ForType(Args::Type::Preserve), Argument::ForType(Args::Type::Log), Argument::ForType(Args::Type::CustomHeader), Argument::ForType(Args::Type::AcceptSourceAgreements), @@ -70,6 +74,7 @@ namespace AppInstaller::CLI case Execution::Args::Type::Version: case Execution::Args::Type::Channel: case Execution::Args::Type::Source: + case Execution::Args::Type::ProductCode: context << Workflow::CompleteWithSingleSemanticsForValueUsingExistingSource(valueType); break; @@ -90,6 +95,7 @@ namespace AppInstaller::CLI execArgs.Contains(Execution::Args::Type::Id) || execArgs.Contains(Execution::Args::Type::Name) || execArgs.Contains(Execution::Args::Type::Moniker) || + execArgs.Contains(Execution::Args::Type::ProductCode) || execArgs.Contains(Execution::Args::Type::Version) || execArgs.Contains(Execution::Args::Type::Channel) || execArgs.Contains(Execution::Args::Type::Source) || @@ -97,6 +103,11 @@ namespace AppInstaller::CLI { throw CommandException(Resource::String::BothManifestAndSearchQueryProvided, ""); } + + if (execArgs.Contains(Execution::Args::Type::Purge) && execArgs.Contains(Execution::Args::Type::Preserve)) + { + throw CommandException(Resource::String::BothPurgeAndPreserveFlagsProvided, ""); + } } void UninstallCommand::ExecuteInternal(Execution::Context& context) const diff --git a/src/AppInstallerCLICore/Commands/UpgradeCommand.cpp b/src/AppInstallerCLICore/Commands/UpgradeCommand.cpp index 344b33f103..56e7ad45dd 100644 --- a/src/AppInstallerCLICore/Commands/UpgradeCommand.cpp +++ b/src/AppInstallerCLICore/Commands/UpgradeCommand.cpp @@ -93,6 +93,7 @@ namespace AppInstaller::CLI Argument::ForType(Args::Type::Exact), Argument::ForType(Args::Type::Interactive), Argument::ForType(Args::Type::Silent), + Argument::ForType(Args::Type::Purge), Argument::ForType(Args::Type::Log), Argument::ForType(Args::Type::Override), Argument::ForType(Args::Type::InstallLocation), diff --git a/src/AppInstallerCLICore/ExecutionContextData.h b/src/AppInstallerCLICore/ExecutionContextData.h index 3498e7513c..c8d6363576 100644 --- a/src/AppInstallerCLICore/ExecutionContextData.h +++ b/src/AppInstallerCLICore/ExecutionContextData.h @@ -4,6 +4,7 @@ #include #include #include +#include #include "CompletionData.h" #include "PackageCollection.h" #include "Workflows/WorkflowBase.h" @@ -52,6 +53,7 @@ namespace AppInstaller::CLI::Execution Dependencies, DependencySource, AllowedArchitectures, + PortableARPEntry, Max }; @@ -214,5 +216,11 @@ namespace AppInstaller::CLI::Execution { using value_t = std::vector; }; + + template <> + struct DataMapping + { + using value_t = Registry::Portable::PortableARPEntry; + }; } } diff --git a/src/AppInstallerCLICore/Resources.h b/src/AppInstallerCLICore/Resources.h index 1d63dbbd27..cd12634abb 100644 --- a/src/AppInstallerCLICore/Resources.h +++ b/src/AppInstallerCLICore/Resources.h @@ -34,6 +34,7 @@ namespace AppInstaller::CLI::Resource WINGET_DEFINE_RESOURCE_STRINGID(AvailableSubcommands); WINGET_DEFINE_RESOURCE_STRINGID(AvailableUpgrades); WINGET_DEFINE_RESOURCE_STRINGID(BothManifestAndSearchQueryProvided); + WINGET_DEFINE_RESOURCE_STRINGID(BothPurgeAndPreserveFlagsProvided); WINGET_DEFINE_RESOURCE_STRINGID(Cancelled); WINGET_DEFINE_RESOURCE_STRINGID(ChannelArgumentDescription); WINGET_DEFINE_RESOURCE_STRINGID(Command); @@ -82,6 +83,7 @@ namespace AppInstaller::CLI::Resource WINGET_DEFINE_RESOURCE_STRINGID(FeaturesProperty); WINGET_DEFINE_RESOURCE_STRINGID(FeaturesStatus); WINGET_DEFINE_RESOURCE_STRINGID(FileArgumentDescription); + WINGET_DEFINE_RESOURCE_STRINGID(FilesRemainInInstallDirectory); WINGET_DEFINE_RESOURCE_STRINGID(FlagContainAdjoinedError); WINGET_DEFINE_RESOURCE_STRINGID(GetManifestResultVersionNotFound); WINGET_DEFINE_RESOURCE_STRINGID(HashCommandLongDescription); @@ -210,6 +212,9 @@ namespace AppInstaller::CLI::Resource WINGET_DEFINE_RESOURCE_STRINGID(PoliciesEnabled); WINGET_DEFINE_RESOURCE_STRINGID(PoliciesPolicy); WINGET_DEFINE_RESOURCE_STRINGID(PoliciesState); + WINGET_DEFINE_RESOURCE_STRINGID(PortableHashMismatchOverridden); + WINGET_DEFINE_RESOURCE_STRINGID(PortableHashMismatchOverrideRequired); + WINGET_DEFINE_RESOURCE_STRINGID(PortableInstallFailed); WINGET_DEFINE_RESOURCE_STRINGID(PortableRegistryCollisionOverridden); WINGET_DEFINE_RESOURCE_STRINGID(PositionArgumentDescription); WINGET_DEFINE_RESOURCE_STRINGID(PreserveArgumentDescription); @@ -218,6 +223,7 @@ namespace AppInstaller::CLI::Resource WINGET_DEFINE_RESOURCE_STRINGID(PromptOptionNo); WINGET_DEFINE_RESOURCE_STRINGID(PromptOptionYes); WINGET_DEFINE_RESOURCE_STRINGID(PurgeArgumentDescription); + WINGET_DEFINE_RESOURCE_STRINGID(PurgeInstallDirectory); WINGET_DEFINE_RESOURCE_STRINGID(QueryArgumentDescription); WINGET_DEFINE_RESOURCE_STRINGID(RainbowArgumentDescription); WINGET_DEFINE_RESOURCE_STRINGID(RenameArgumentDescription); @@ -340,6 +346,7 @@ namespace AppInstaller::CLI::Resource WINGET_DEFINE_RESOURCE_STRINGID(TooManyAdminSettingArgumentsError); WINGET_DEFINE_RESOURCE_STRINGID(TooManyArgError); WINGET_DEFINE_RESOURCE_STRINGID(TooManyBehaviorsError); + WINGET_DEFINE_RESOURCE_STRINGID(UnableToPurgeInstallDirectory); WINGET_DEFINE_RESOURCE_STRINGID(UnexpectedErrorExecutingCommand); WINGET_DEFINE_RESOURCE_STRINGID(UninstallAbandoned); WINGET_DEFINE_RESOURCE_STRINGID(UninstallCommandLongDescription); diff --git a/src/AppInstallerCLICore/Workflows/InstallFlow.cpp b/src/AppInstallerCLICore/Workflows/InstallFlow.cpp index 7005af6088..09ced34096 100644 --- a/src/AppInstallerCLICore/Workflows/InstallFlow.cpp +++ b/src/AppInstallerCLICore/Workflows/InstallFlow.cpp @@ -9,7 +9,7 @@ #include "ShellExecuteInstallerHandler.h" #include "MSStoreInstallerHandler.h" #include "MsiInstallFlow.h" -#include "PortableInstallFlow.h" +#include "PortableFlow.h" #include "WorkflowBase.h" #include "Workflows/DependenciesFlow.h" #include @@ -269,6 +269,13 @@ namespace AppInstaller::CLI::Workflow (isUpdate ? MSStoreUpdate : MSStoreInstall); break; case InstallerTypeEnum::Portable: + if (isUpdate && installer.UpdateBehavior == UpdateBehaviorEnum::UninstallPrevious) + { + context << + GetUninstallInfo << + ExecuteUninstaller; + context.ClearFlags(Execution::ContextFlag::InstallerExecutionUseUpdate); + } context << PortableInstall; break; default: diff --git a/src/AppInstallerCLICore/Workflows/PortableInstallFlow.cpp b/src/AppInstallerCLICore/Workflows/PortableFlow.cpp similarity index 52% rename from src/AppInstallerCLICore/Workflows/PortableInstallFlow.cpp rename to src/AppInstallerCLICore/Workflows/PortableFlow.cpp index 09599290e9..a4bb5355e2 100644 --- a/src/AppInstallerCLICore/Workflows/PortableInstallFlow.cpp +++ b/src/AppInstallerCLICore/Workflows/PortableFlow.cpp @@ -1,7 +1,7 @@ // Copyright (c) Microsoft Corporation. // Licensed under the MIT License. #include "pch.h" -#include "PortableInstallFlow.h" +#include "PortableFlow.h" #include "winget/Filesystem.h" #include "winget/PortableARPEntry.h" #include "AppInstallerStrings.h" @@ -10,6 +10,7 @@ using namespace AppInstaller::Manifest; using namespace AppInstaller::Utility; using namespace AppInstaller::Registry; using namespace AppInstaller::Registry::Portable; +using namespace AppInstaller::Repository; using namespace std::filesystem; namespace AppInstaller::CLI::Workflow @@ -19,7 +20,7 @@ namespace AppInstaller::CLI::Workflow constexpr std::wstring_view s_PathName = L"Path"; constexpr std::wstring_view s_PathSubkey_User = L"Environment"; constexpr std::wstring_view s_PathSubkey_Machine = L"SYSTEM\\CurrentControlSet\\Control\\Session Manager\\Environment"; - constexpr std::string_view s_LocalSource = "*Local"sv; + constexpr std::string_view s_DefaultSource = "*DefaultSource"sv; void AppendExeExtension(std::filesystem::path& value) { @@ -34,13 +35,13 @@ namespace AppInstaller::CLI::Workflow const std::string& packageId = context.Get().Id; std::string source; - if (context.Contains(Execution::Data::Source)) + if (context.Contains(Execution::Data::PackageVersion)) { - source = context.Get().GetIdentifier(); + source = context.Get()->GetSource().GetIdentifier(); } else { - source = s_LocalSource; + source = s_DefaultSource; } return MakeSuitablePathPart(packageId + "_" + source); @@ -146,7 +147,9 @@ namespace AppInstaller::CLI::Workflow return GetPortableLinksLocation(scope) / commandAlias; } - Manifest::AppsAndFeaturesEntry GetAppsAndFeaturesEntryForPortableInstall(const std::vector& appsAndFeaturesEntries, const AppInstaller::Manifest::Manifest& manifest) + Manifest::AppsAndFeaturesEntry GetAppsAndFeaturesEntryForPortableInstall( + const std::vector& appsAndFeaturesEntries, + const AppInstaller::Manifest::Manifest& manifest) { AppInstaller::Manifest::AppsAndFeaturesEntry appsAndFeaturesEntry; if (!appsAndFeaturesEntries.empty()) @@ -156,7 +159,7 @@ namespace AppInstaller::CLI::Workflow if (appsAndFeaturesEntry.DisplayName.empty()) { - appsAndFeaturesEntry.DisplayName = manifest.DefaultLocalization.Get(); + appsAndFeaturesEntry.DisplayName = manifest.CurrentLocalization.Get(); } if (appsAndFeaturesEntry.DisplayVersion.empty()) { @@ -164,15 +167,14 @@ namespace AppInstaller::CLI::Workflow } if (appsAndFeaturesEntry.Publisher.empty()) { - appsAndFeaturesEntry.Publisher = manifest.DefaultLocalization.Get(); + appsAndFeaturesEntry.Publisher = manifest.CurrentLocalization.Get(); } return appsAndFeaturesEntry; } - bool AddToPathRegistry(Execution::Context& context) + bool AddToPathRegistry(Manifest::ScopeEnum scope) { - Manifest::ScopeEnum scope = ConvertToScopeEnum(context.Args.GetArg(Execution::Args::Type::InstallScope)); const std::filesystem::path& linksDirectory = GetPortableLinksLocation(scope); Key key; @@ -208,30 +210,56 @@ namespace AppInstaller::CLI::Workflow } } - void WritePortableEntryToUninstallRegistry(Execution::Context& context) + bool RemoveFromPathRegistry(Manifest::ScopeEnum scope) { - const AppInstaller::Manifest::Manifest& manifest = context.Get(); - const Manifest::AppsAndFeaturesEntry& entry = GetAppsAndFeaturesEntryForPortableInstall(context.Get()->AppsAndFeaturesEntries, manifest); - const std::string& packageIdentifier = manifest.Id; + const std::filesystem::path& linksDirectory = GetPortableLinksLocation(scope); + + Key key; + if (scope == Manifest::ScopeEnum::Machine) + { + key = Registry::Key::Create(HKEY_LOCAL_MACHINE, std::wstring{ s_PathSubkey_Machine }); + } + else + { + key = Registry::Key::Create(HKEY_CURRENT_USER, std::wstring{ s_PathSubkey_User }); + } + + std::wstring pathName = std::wstring{ s_PathName }; + std::string portableLinksDir = Normalize(linksDirectory.u8string()); + std::string pathValue = Normalize(key[pathName]->GetValue()); + + if (pathValue.find(portableLinksDir) != std::string::npos) + { + FindAndReplace(pathValue, portableLinksDir, ""); + FindAndReplace(pathValue, ";;", ";"); + AICLI_LOG(CLI, Info, << "Removing from Path environment variable: " << portableLinksDir); + key.SetValue(pathName, ConvertToUTF16(pathValue), REG_EXPAND_SZ); + return true; + } + else + { + AICLI_LOG(CLI, Verbose, << "Path does not exist in environment variable."); + return false; + } + } + + void InitializePortableARPEntry(Execution::Context& context) + { + const std::string& packageIdentifier = context.Get().Id; std::string sourceIdentifier; - if (context.Contains(Execution::Data::Source)) + if (context.Contains(Execution::Data::PackageVersion)) { - sourceIdentifier = context.Get().GetIdentifier(); + sourceIdentifier = context.Get()->GetSource().GetIdentifier(); } else { - sourceIdentifier = s_LocalSource; + sourceIdentifier = s_DefaultSource; } + + PortableARPEntry& uninstallEntry = context.Get(); - const std::wstring& productCode = ConvertToUTF16(GetPortableProductCode(context)); - - Portable::PortableARPEntry uninstallEntry = Portable::PortableARPEntry( - ConvertToScopeEnum(context.Args.GetArg(Execution::Args::Type::InstallScope)), - context.Get()->Arch, - productCode); - - if(uninstallEntry.Exists()) + if (uninstallEntry.Exists()) { if (!uninstallEntry.IsSamePortablePackageEntry(packageIdentifier, sourceIdentifier)) { @@ -249,29 +277,17 @@ namespace AppInstaller::CLI::Workflow } } - AICLI_LOG(CLI, Info, << "Begin writing to Uninstall registry."); - uninstallEntry.SetValue(PortableValueName::DisplayName, entry.DisplayName); - uninstallEntry.SetValue(PortableValueName::DisplayVersion, entry.DisplayVersion); - uninstallEntry.SetValue(PortableValueName::Publisher, entry.Publisher); - uninstallEntry.SetValue(PortableValueName::InstallDate, Utility::GetCurrentDateForARP()); - uninstallEntry.SetValue(PortableValueName::URLInfoAbout, manifest.DefaultLocalization.Get()); - uninstallEntry.SetValue(PortableValueName::HelpLink, manifest.DefaultLocalization.Get()); - uninstallEntry.SetValue(PortableValueName::UninstallString, L"winget uninstall --product-code " + productCode); + uninstallEntry.SetValue(PortableValueName::WinGetPackageIdentifier, packageIdentifier); + uninstallEntry.SetValue(PortableValueName::WinGetSourceIdentifier, sourceIdentifier); + uninstallEntry.SetValue(PortableValueName::UninstallString, L"winget uninstall --product-code " + ConvertToUTF16(GetPortableProductCode(context))); uninstallEntry.SetValue(PortableValueName::WinGetInstallerType, ConvertToUTF16(InstallerTypeToString(InstallerTypeEnum::Portable))); - uninstallEntry.SetValue(PortableValueName::WinGetPackageIdentifier, manifest.Id); - uninstallEntry.SetValue(PortableValueName::WinGetSourceIdentifier, sourceIdentifier); - uninstallEntry.SetValue(PortableValueName::PortableTargetFullPath, GetPortableTargetFullPath(context).wstring()); - uninstallEntry.SetValue(PortableValueName::PortableSymlinkFullPath, GetPortableSymlinkFullPath(context).wstring()); - uninstallEntry.SetValue(PortableValueName::SHA256, Utility::SHA256::ConvertToWideString(context.Get().second)); - uninstallEntry.SetValue(PortableValueName::InstallLocation, GetPortableTargetDirectory(context).wstring()); - AICLI_LOG(CLI, Info, << "Writing to Uninstall registry complete."); - } - - void MovePortableExeAndCreateSymlink(Execution::Context& context) + } + + void MovePortableExe(Execution::Context& context) { + PortableARPEntry& uninstallEntry = context.Get(); const std::filesystem::path& installerPath = context.Get(); const std::filesystem::path& targetFullPath = GetPortableTargetFullPath(context); - const std::filesystem::path& symlinkFullPath = GetPortableSymlinkFullPath(context); const std::filesystem::path& targetDirectory = GetPortableTargetDirectory(context); bool isDirectoryCreated = false; @@ -281,15 +297,131 @@ namespace AppInstaller::CLI::Workflow isDirectoryCreated = true; } - Portable::PortableARPEntry uninstallEntry = Portable::PortableARPEntry( - ConvertToScopeEnum(context.Args.GetArg(Execution::Args::Type::InstallScope)), - context.Get()->Arch, - ConvertToUTF16(GetPortableProductCode(context))); - uninstallEntry.SetValue(PortableValueName::InstallDirectoryCreated, isDirectoryCreated); + if (std::filesystem::exists(targetFullPath)) + { + std::filesystem::remove(targetFullPath); + AICLI_LOG(CLI, Info, << "Removing existing portable exe at: " << targetFullPath); + } Filesystem::RenameFile(installerPath, targetFullPath); AICLI_LOG(CLI, Info, << "Portable exe moved to: " << targetFullPath); + if (!uninstallEntry[PortableValueName::InstallDirectoryCreated].has_value()) + { + uninstallEntry.SetValue(PortableValueName::InstallDirectoryCreated, isDirectoryCreated); + } + + uninstallEntry.SetValue(PortableValueName::PortableTargetFullPath, targetFullPath.wstring()); + uninstallEntry.SetValue(PortableValueName::InstallLocation, GetPortableTargetDirectory(context).wstring()); + uninstallEntry.SetValue(PortableValueName::SHA256, Utility::SHA256::ConvertToWideString(context.Get().second)); + } + + void RemovePortableExe(Execution::Context& context) + { + PortableARPEntry& uninstallEntry = context.Get(); + const auto& targetPath = uninstallEntry[PortableValueName::PortableTargetFullPath]; + + if (targetPath.has_value()) + { + const std::filesystem::path& targetPathValue = targetPath.value().GetValue(); + const auto& expectedHash = uninstallEntry[PortableValueName::SHA256]; + std::string expectedHashValue = expectedHash.has_value() ? uninstallEntry[PortableValueName::SHA256].value().GetValue() : ""; + + if (std::filesystem::exists(targetPathValue)) + { + std::ifstream inStream{ targetPathValue, std::ifstream::binary }; + const Utility::SHA256::HashBuffer& targetFileHash = SHA256::ComputeHash(inStream); + inStream.close(); + + bool overrideHashMismatch = context.Args.Contains(Execution::Args::Type::HashOverride); + + if (!SHA256::AreEqual(SHA256::ConvertToBytes(expectedHashValue), targetFileHash)) + { + if (overrideHashMismatch) + { + context.Reporter.Warn() << Resource::String::PortableHashMismatchOverridden << std::endl; + } + else + { + context.Reporter.Warn() << Resource::String::PortableHashMismatchOverrideRequired << std::endl; + AICLI_TERMINATE_CONTEXT(APPINSTALLER_CLI_ERROR_PORTABLE_UNINSTALL_FAILED); + } + } + + std::filesystem::remove(targetPathValue); + AICLI_LOG(CLI, Info, << "Successfully deleted portable exe:" << targetPathValue); + } + else + { + AICLI_LOG(CLI, Info, << "Portable exe not found; Unable to delete portable exe: " << targetPathValue); + } + } + else + { + AICLI_LOG(CLI, Info, << "The registry value for [TargetFullPath] does not exist"); + } + } + + void RemoveInstallDirectory(Execution::Context& context) + { + PortableARPEntry& uninstallEntry = context.Get(); + const auto& installDirectory = uninstallEntry[PortableValueName::InstallLocation]; + + if (installDirectory.has_value()) + { + const std::filesystem::path& installDirectoryValue = installDirectory.value().GetValue(); + if (std::filesystem::exists(installDirectoryValue)) + { + const auto& isDirectoryCreated = uninstallEntry[PortableValueName::InstallDirectoryCreated]; + const auto& isDirectoryCreatedValue = isDirectoryCreated.has_value() ? isDirectoryCreated.value().GetValue() : FALSE; + + bool isUpdate = WI_IsFlagSet(context.GetFlags(), Execution::ContextFlag::InstallerExecutionUseUpdate); + + if (context.Args.Contains(Execution::Args::Type::Purge) || + (!isUpdate && Settings::User().Get() && !context.Args.Contains(Execution::Args::Type::Preserve))) + { + if (isDirectoryCreatedValue) + { + context.Reporter.Warn() << Resource::String::PurgeInstallDirectory << std::endl; + const auto& removedFilesCount = std::filesystem::remove_all(installDirectoryValue); + AICLI_LOG(CLI, Info, << "Purged install location directory. Deleted " << removedFilesCount << " files or directories"); + } + else + { + context.Reporter.Warn() << Resource::String::UnableToPurgeInstallDirectory << std::endl; + } + + } + else if (std::filesystem::is_empty(installDirectoryValue)) + { + if (isDirectoryCreatedValue) + { + std::filesystem::remove(installDirectoryValue); + AICLI_LOG(CLI, Info, << "Install directory deleted: " << installDirectoryValue); + } + } + else + { + context.Reporter.Warn() << Resource::String::FilesRemainInInstallDirectory << installDirectoryValue << std::endl; + } + } + else + { + AICLI_LOG(CLI, Info, << "Install directory does not exist: " << installDirectoryValue); + } + } + else + { + AICLI_LOG(CLI, Info, << "The registry value for [InstallLocation] does not exist"); + } + } + + void CreatePortableSymlink(Execution::Context& context) + { + PortableARPEntry& uninstallEntry = context.Get(); + const std::filesystem::path& targetFullPath = GetPortableTargetFullPath(context); + const std::filesystem::path& symlinkFullPath = GetPortableSymlinkFullPath(context); + std::filesystem::file_status status = std::filesystem::status(symlinkFullPath); if (std::filesystem::is_directory(status)) { @@ -303,14 +435,63 @@ namespace AppInstaller::CLI::Workflow } std::filesystem::create_symlink(targetFullPath, symlinkFullPath); - AICLI_LOG(CLI, Info, << "Symlink created at: " << symlinkFullPath); - - if (AddToPathRegistry(context)) + AICLI_LOG(CLI, Info, << "Symlink created at: " << symlinkFullPath); + uninstallEntry.SetValue(PortableValueName::PortableSymlinkFullPath, symlinkFullPath.wstring()); + + Manifest::ScopeEnum scope = ConvertToScopeEnum(context.Args.GetArg(Execution::Args::Type::InstallScope)); + if (AddToPathRegistry(scope)) { context.Reporter.Warn() << Resource::String::ModifiedPathRequiresShellRestart << std::endl; } } + void RemovePortableSymlink(Execution::Context& context) + { + PortableARPEntry& uninstallEntry = context.Get(); + const auto& symlinkPath = uninstallEntry[PortableValueName::PortableSymlinkFullPath]; + if (symlinkPath.has_value()) + { + const std::filesystem::path& symlinkPathValue = symlinkPath.value().GetValue(); + + if (!std::filesystem::remove(symlinkPathValue)) + { + AICLI_LOG(CLI, Info, << "Portable symlink not found; Unable to delete portable symlink: " << symlinkPathValue); + } + } + else + { + AICLI_LOG(CLI, Info, << "The registry value for [SymlinkFullPath] does not exist"); + } + + Manifest::ScopeEnum scope = uninstallEntry.GetScope(); + + if (std::filesystem::is_empty(GetPortableLinksLocation(scope))) + { + RemoveFromPathRegistry(scope); + } + } + + void RemovePortableARPEntry(Execution::Context& context) + { + PortableARPEntry& uninstallEntry = context.Get(); + uninstallEntry.Delete(); + AICLI_LOG(CLI, Info, << "PortableARPEntry deleted."); + } + + void CommitPortableMetadataToRegistry(Execution::Context& context) + { + PortableARPEntry& uninstallEntry = context.Get(); + const AppInstaller::Manifest::Manifest& manifest = context.Get(); + const Manifest::AppsAndFeaturesEntry& entry = GetAppsAndFeaturesEntryForPortableInstall(context.Get()->AppsAndFeaturesEntries, manifest); + + uninstallEntry.SetValue(PortableValueName::DisplayName, entry.DisplayName); + uninstallEntry.SetValue(PortableValueName::DisplayVersion, entry.DisplayVersion); + uninstallEntry.SetValue(PortableValueName::Publisher, entry.Publisher); + uninstallEntry.SetValue(PortableValueName::InstallDate, Utility::GetCurrentDateForARP()); + uninstallEntry.SetValue(PortableValueName::URLInfoAbout, manifest.CurrentLocalization.Get()); + uninstallEntry.SetValue(PortableValueName::HelpLink, manifest.CurrentLocalization.Get()); + } + void EnsureValidArgsForPortableInstall(Execution::Context& context) { std::string_view renameArg = context.Args.GetArg(Execution::Args::Type::Rename); @@ -341,17 +522,26 @@ namespace AppInstaller::CLI::Workflow AICLI_TERMINATE_CONTEXT(APPINSTALLER_CLI_ERROR_PORTABLE_REPARSE_POINT_NOT_SUPPORTED); } } - } + } void PortableInstallImpl(Execution::Context& context) { + PortableARPEntry uninstallEntry = PortableARPEntry( + ConvertToScopeEnum(context.Args.GetArg(Execution::Args::Type::InstallScope)), + context.Get()->Arch, + GetPortableProductCode(context)); + + context.Add(std::move(uninstallEntry)); + try { context.Reporter.Info() << Resource::String::InstallFlowStartingPackageInstall << std::endl; context << - WritePortableEntryToUninstallRegistry << - MovePortableExeAndCreateSymlink; + InitializePortableARPEntry << + MovePortableExe << + CreatePortableSymlink << + CommitPortableMetadataToRegistry; context.Add(context.GetTerminationHR()); } @@ -363,7 +553,43 @@ namespace AppInstaller::CLI::Workflow // Reset termination to allow for ReportInstallResult to process return code. context.ResetTermination(); - // TODO: create subcontext for uninstall + // Perform cleanup only if the install fails and is not an update. + const auto& installReturnCode = context.Get(); + bool isUpdate = WI_IsFlagSet(context.GetFlags(), Execution::ContextFlag::InstallerExecutionUseUpdate); + + if (installReturnCode != 0 && installReturnCode != APPINSTALLER_CLI_ERROR_PORTABLE_PACKAGE_ALREADY_EXISTS && !isUpdate) + { + context.Reporter.Warn() << Resource::String::PortableInstallFailed << std::endl; + auto uninstallPortableContextPtr = context.CreateSubContext(); + Execution::Context& uninstallPortableContext = *uninstallPortableContextPtr; + auto previousThreadGlobals = uninstallPortableContext.SetForCurrentThread(); + + uninstallPortableContext.Add(context.Get()); + uninstallPortableContext << PortableUninstallImpl; + } + } + + void PortableUninstallImpl(Execution::Context& context) + { + try + { + context.Reporter.Info() << Resource::String::UninstallFlowStartingPackageUninstall << std::endl; + + context << + RemovePortableExe << + RemoveInstallDirectory << + RemovePortableSymlink << + RemovePortableARPEntry; + + context.Add(context.GetTerminationHR()); + } + catch (...) + { + context.Add(Workflow::HandleException(context, std::current_exception())); + } + + // Reset termination to allow for ReportUninstallResult to process return code. + context.ResetTermination(); } void EnsureSupportForPortableInstall(Execution::Context& context) diff --git a/src/AppInstallerCLICore/Workflows/PortableInstallFlow.h b/src/AppInstallerCLICore/Workflows/PortableFlow.h similarity index 68% rename from src/AppInstallerCLICore/Workflows/PortableInstallFlow.h rename to src/AppInstallerCLICore/Workflows/PortableFlow.h index 748aa667d0..1d799ab317 100644 --- a/src/AppInstallerCLICore/Workflows/PortableInstallFlow.h +++ b/src/AppInstallerCLICore/Workflows/PortableFlow.h @@ -11,5 +11,11 @@ namespace AppInstaller::CLI::Workflow // Outputs: None void PortableInstallImpl(Execution::Context& context); + // Uninstalls the portable package. + // Required Args: None + // Inputs: ProductCode, Scope, Architecture + // Outputs: None + void PortableUninstallImpl(Execution::Context& context); + void EnsureSupportForPortableInstall(Execution::Context& context); } \ No newline at end of file diff --git a/src/AppInstallerCLICore/Workflows/ShellExecuteInstallerHandler.cpp b/src/AppInstallerCLICore/Workflows/ShellExecuteInstallerHandler.cpp index a36e36992d..cab05e70f9 100644 --- a/src/AppInstallerCLICore/Workflows/ShellExecuteInstallerHandler.cpp +++ b/src/AppInstallerCLICore/Workflows/ShellExecuteInstallerHandler.cpp @@ -269,22 +269,9 @@ namespace AppInstaller::CLI::Workflow context.Reporter.Warn() << Resource::String::UninstallAbandoned << std::endl; AICLI_TERMINATE_CONTEXT(E_ABORT); } - else if (uninstallResult.value() != 0) - { - const auto installedPackageVersion = context.Get(); - Logging::Telemetry().LogUninstallerFailure( - installedPackageVersion->GetProperty(PackageVersionProperty::Id), - installedPackageVersion->GetProperty(PackageVersionProperty::Version), - "UninstallString", - uninstallResult.value()); - - context.Add(uninstallResult.value()); - context.Reporter.Error() << Resource::String::UninstallFailedWithCode << ' ' << uninstallResult.value() << std::endl; - AICLI_TERMINATE_CONTEXT(APPINSTALLER_CLI_ERROR_EXEC_UNINSTALL_COMMAND_FAILED); - } else { - context.Reporter.Info() << Resource::String::UninstallFlowUninstallSuccess << std::endl; + context.Add(uninstallResult.value()); } } @@ -309,22 +296,11 @@ namespace AppInstaller::CLI::Workflow context.Reporter.Warn() << Resource::String::UninstallAbandoned << std::endl; AICLI_TERMINATE_CONTEXT(E_ABORT); } - else if (uninstallResult.value() != 0) + else { - // TODO: Check for other success codes - const auto installedPackageVersion = context.Get(); - Logging::Telemetry().LogUninstallerFailure( - installedPackageVersion->GetProperty(PackageVersionProperty::Id), - installedPackageVersion->GetProperty(PackageVersionProperty::Version), - "MsiExec", - uninstallResult.value()); - context.Add(uninstallResult.value()); - context.Reporter.Error() << Resource::String::UninstallFailedWithCode << ' ' << uninstallResult.value() << std::endl; - AICLI_TERMINATE_CONTEXT(APPINSTALLER_CLI_ERROR_EXEC_UNINSTALL_COMMAND_FAILED); + } } - - context.Reporter.Info() << Resource::String::UninstallFlowUninstallSuccess << std::endl; } } \ No newline at end of file diff --git a/src/AppInstallerCLICore/Workflows/UninstallFlow.cpp b/src/AppInstallerCLICore/Workflows/UninstallFlow.cpp index 708753cd50..19c2d395da 100644 --- a/src/AppInstallerCLICore/Workflows/UninstallFlow.cpp +++ b/src/AppInstallerCLICore/Workflows/UninstallFlow.cpp @@ -6,6 +6,8 @@ #include "DependenciesFlow.h" #include "ShellExecuteInstallerHandler.h" #include "AppInstallerMsixInfo.h" +#include "PortableFlow.h" +#include "winget/PortableARPEntry.h" #include @@ -13,6 +15,7 @@ using namespace AppInstaller::CLI::Execution; using namespace AppInstaller::Manifest; using namespace AppInstaller::Msix; using namespace AppInstaller::Repository; +using namespace AppInstaller::Registry; namespace AppInstaller::CLI::Workflow { @@ -126,6 +129,24 @@ namespace AppInstaller::CLI::Workflow context.Add(packageFamilyNames); break; } + case InstallerTypeEnum::Portable: + { + auto productCodes = installedPackageVersion->GetMultiProperty(PackageVersionMultiProperty::ProductCode); + if (productCodes.empty()) + { + context.Reporter.Error() << Resource::String::NoUninstallInfoFound << std::endl; + AICLI_TERMINATE_CONTEXT(APPINSTALLER_CLI_ERROR_NO_UNINSTALL_INFO_FOUND); + } + + const std::string installedScope = context.Get()->GetMetadata()[Repository::PackageVersionMetadata::InstalledScope]; + const std::string installedArch = context.Get()->GetMetadata()[Repository::PackageVersionMetadata::InstalledArchitecture]; + Portable::PortableARPEntry uninstallEntry = Portable::PortableARPEntry( + ConvertToScopeEnum(installedScope), + Utility::ConvertToArchitectureEnum(installedArch), + productCodes[0]); + context.Add(uninstallEntry); + break; + } default: THROW_HR(HRESULT_FROM_WIN32(ERROR_NOT_SUPPORTED)); } @@ -140,16 +161,25 @@ namespace AppInstaller::CLI::Workflow case InstallerTypeEnum::Burn: case InstallerTypeEnum::Inno: case InstallerTypeEnum::Nullsoft: - context << Workflow::ShellExecuteUninstallImpl; + context << + Workflow::ShellExecuteUninstallImpl << + ReportUninstallerResult("UninstallString", APPINSTALLER_CLI_ERROR_EXEC_UNINSTALL_COMMAND_FAILED); break; case InstallerTypeEnum::Msi: case InstallerTypeEnum::Wix: - context << Workflow::ShellExecuteMsiExecUninstall; + context << + Workflow::ShellExecuteMsiExecUninstall << + ReportUninstallerResult("MsiExec", APPINSTALLER_CLI_ERROR_EXEC_UNINSTALL_COMMAND_FAILED); break; case InstallerTypeEnum::Msix: case InstallerTypeEnum::MSStore: context << Workflow::MsixUninstall; break; + case InstallerTypeEnum::Portable: + context << + Workflow::PortableUninstallImpl << + ReportUninstallerResult("PortableUninstall"sv, APPINSTALLER_CLI_ERROR_PORTABLE_UNINSTALL_FAILED, true); + break; default: THROW_HR(HRESULT_FROM_WIN32(ERROR_NOT_SUPPORTED)); } @@ -177,8 +207,8 @@ namespace AppInstaller::CLI::Workflow catch (const wil::ResultException& re) { context.Add(re.GetErrorCode()); - context.Reporter.Error() << Resource::String::UninstallFailedWithCode << ' ' << re.GetErrorCode() << std::endl; - AICLI_TERMINATE_CONTEXT(APPINSTALLER_CLI_ERROR_EXEC_UNINSTALL_COMMAND_FAILED); + context << ReportUninstallerResult("MSIXUninstall"sv, re.GetErrorCode(), /* isHResult */ true); + return; } } @@ -207,4 +237,39 @@ namespace AppInstaller::CLI::Workflow trackingCatalog.RecordUninstall(item.Identifier); } } + + void ReportUninstallerResult::operator()(Execution::Context& context) const + { + DWORD uninstallResult = context.Get(); + if (uninstallResult != 0) + { + const auto installedPackageVersion = context.Get(); + Logging::Telemetry().LogUninstallerFailure( + installedPackageVersion->GetProperty(PackageVersionProperty::Id), + installedPackageVersion->GetProperty(PackageVersionProperty::Version), + m_uninstallerType, + uninstallResult); + + if (m_isHResult) + { + context.Reporter.Error() << Resource::String::UninstallFailedWithCode << ' ' << GetUserPresentableMessage(uninstallResult) << std::endl; + } + else + { + context.Reporter.Error() << Resource::String::UninstallFailedWithCode << ' ' << uninstallResult << std::endl; + } + + // Show installer log path if exists + if (context.Contains(Execution::Data::LogPath) && std::filesystem::exists(context.Get())) + { + context.Reporter.Info() << Resource::String::InstallerLogAvailable << ' ' << context.Get().u8string() << std::endl; + } + + AICLI_TERMINATE_CONTEXT(m_hr); + } + else + { + context.Reporter.Info() << Resource::String::UninstallFlowUninstallSuccess << std::endl; + } + } } diff --git a/src/AppInstallerCLICore/Workflows/UninstallFlow.h b/src/AppInstallerCLICore/Workflows/UninstallFlow.h index ac7df3f185..60f197bf70 100644 --- a/src/AppInstallerCLICore/Workflows/UninstallFlow.h +++ b/src/AppInstallerCLICore/Workflows/UninstallFlow.h @@ -30,15 +30,32 @@ namespace AppInstaller::CLI::Workflow // Outputs: None void MsixUninstall(Execution::Context& context); - // Removes the Portable package. - // Required Args: None - // Inputs: ProductCode - // Outputs: None - void PortableUninstall(Execution::Context& context); - // Records the uninstall to the tracking catalog. // Required Args: None // Inputs: Package // Outputs: None void RecordUninstall(Execution::Context& context); + + // Reports the return code returned by the Uninstaller. + // Required Args: None + // Inputs: InstalledPackageVersion + // Outputs: None + struct ReportUninstallerResult : public WorkflowTask + { + ReportUninstallerResult(std::string_view uninstallerType, HRESULT hr, bool isHResult = false) : + WorkflowTask("ReportUninstallerResult"), + m_uninstallerType(uninstallerType), + m_hr(hr), + m_isHResult(isHResult) {} + + void operator()(Execution::Context& context) const override; + + private: + // Uninstaller type used when reporting failures. + std::string_view m_uninstallerType; + // Result to return if the Uninstaller failed. + HRESULT m_hr; + // Whether the Uninstaller result is an HRESULT. This guides how we show it. + bool m_isHResult; + }; } diff --git a/src/AppInstallerCLICore/Workflows/WorkflowBase.cpp b/src/AppInstallerCLICore/Workflows/WorkflowBase.cpp index d70cca7183..4e1084a834 100644 --- a/src/AppInstallerCLICore/Workflows/WorkflowBase.cpp +++ b/src/AppInstallerCLICore/Workflows/WorkflowBase.cpp @@ -133,6 +133,11 @@ namespace AppInstaller::CLI::Workflow searchRequest.Filters.emplace_back(PackageMatchFilter(PackageMatchField::Moniker, matchType, args.GetArg(Execution::Args::Type::Moniker))); } + if (args.Contains(Execution::Args::Type::ProductCode)) + { + searchRequest.Filters.emplace_back(PackageMatchFilter(PackageMatchField::ProductCode, matchType, args.GetArg(Execution::Args::Type::ProductCode))); + } + if (args.Contains(Execution::Args::Type::Tag)) { searchRequest.Filters.emplace_back(PackageMatchFilter(PackageMatchField::Tag, matchType, args.GetArg(Execution::Args::Type::Tag))); @@ -478,7 +483,6 @@ namespace AppInstaller::CLI::Workflow // Regardless of match type, always use an exact match for the system reference strings. searchRequest.Inclusions.emplace_back(PackageMatchFilter(PackageMatchField::PackageFamilyName, MatchType::Exact, query)); searchRequest.Inclusions.emplace_back(PackageMatchFilter(PackageMatchField::ProductCode, MatchType::Exact, query)); - searchRequest.Inclusions.emplace_back(PackageMatchFilter(PackageMatchField::Id, matchType, query)); searchRequest.Inclusions.emplace_back(PackageMatchFilter(PackageMatchField::Name, matchType, query)); searchRequest.Inclusions.emplace_back(PackageMatchFilter(PackageMatchField::Moniker, matchType, query)); @@ -1063,6 +1067,11 @@ namespace AppInstaller::CLI::Workflow { searchRequest.Inclusions.emplace_back(PackageMatchFilter(PackageMatchField::ProductCode, MatchType::Exact, installer.ProductCode)); } + else if (installer.InstallerType == Manifest::InstallerTypeEnum::Portable) + { + const auto& productCode = Utility::MakeSuitablePathPart(manifest.Id + "_" + source.GetIdentifier()); + searchRequest.Inclusions.emplace_back(PackageMatchFilter(PackageMatchField::ProductCode, MatchType::CaseInsensitive, Utility::Normalize(productCode))); + } if (!searchRequest.Inclusions.empty()) { @@ -1079,7 +1088,17 @@ namespace AppInstaller::CLI::Workflow // If we cannot find a package using PackageFamilyName or ProductId, try manifest Id and Name pair SearchRequest searchRequest; searchRequest.Inclusions.emplace_back(PackageMatchFilter(PackageMatchField::Id, MatchType::CaseInsensitive, manifest.Id)); + // In case there are same Ids from different sources, filter the result using package name + for (const auto& localization : manifest.Localizations) + { + const auto& localizedPackageName = localization.Get(); + if (!localizedPackageName.empty()) + { + searchRequest.Filters.emplace_back(PackageMatchField::Name, MatchType::CaseInsensitive, localizedPackageName); + } + } + searchRequest.Filters.emplace_back(PackageMatchFilter(PackageMatchField::Name, MatchType::CaseInsensitive, manifest.DefaultLocalization.Get())); context.Add(source.Search(searchRequest)); diff --git a/src/AppInstallerCLIE2ETests/AppInstallerCLIE2ETests.csproj b/src/AppInstallerCLIE2ETests/AppInstallerCLIE2ETests.csproj index 536705c5a2..00bc33df60 100644 --- a/src/AppInstallerCLIE2ETests/AppInstallerCLIE2ETests.csproj +++ b/src/AppInstallerCLIE2ETests/AppInstallerCLIE2ETests.csproj @@ -29,6 +29,8 @@ + + diff --git a/src/AppInstallerCLIE2ETests/BaseCommand.cs b/src/AppInstallerCLIE2ETests/BaseCommand.cs index ae48e3519c..a789b136bc 100644 --- a/src/AppInstallerCLIE2ETests/BaseCommand.cs +++ b/src/AppInstallerCLIE2ETests/BaseCommand.cs @@ -6,7 +6,6 @@ namespace AppInstallerCLIE2ETests using System; using System.IO; using System.Threading; - using Newtonsoft.Json; using Newtonsoft.Json.Linq; using NUnit.Framework; diff --git a/src/AppInstallerCLIE2ETests/Constants.cs b/src/AppInstallerCLIE2ETests/Constants.cs index 19ec2eddcc..296585d04c 100644 --- a/src/AppInstallerCLIE2ETests/Constants.cs +++ b/src/AppInstallerCLIE2ETests/Constants.cs @@ -27,6 +27,7 @@ public class Constants public const string DefaultMSStoreSourceUrl = @"https://storeedgefd.dsx.mp.microsoft.com/v9.0"; public const string TestSourceName = @"TestSource"; public const string TestSourceUrl = @"https://localhost:5001/TestKit"; + public const string TestSourceIdentifier = @"WingetE2E.Tests_8wekyb3d8bbwe"; public const string AICLIPackageFamilyName = "WinGetDevCLI_8wekyb3d8bbwe"; public const string AICLIPackageName = "WinGetDevCLI"; diff --git a/src/AppInstallerCLIE2ETests/InstallCommand.cs b/src/AppInstallerCLIE2ETests/InstallCommand.cs index bbfe100b9b..fc9bf572f5 100644 --- a/src/AppInstallerCLIE2ETests/InstallCommand.cs +++ b/src/AppInstallerCLIE2ETests/InstallCommand.cs @@ -3,7 +3,6 @@ namespace AppInstallerCLIE2ETests { - using Microsoft.Win32; using NUnit.Framework; using System.IO; @@ -12,7 +11,6 @@ public class InstallCommand : BaseCommand private const string InstallTestMsiInstalledFile = @"AppInstallerTestExeInstaller.exe"; private const string InstallTestMsiProductId = @"{A5D36CF1-1993-4F63-BFB4-3ACD910D36A1}"; private const string InstallTestMsixName = @"6c6338fe-41b7-46ca-8ba6-b5ad5312bb0e"; - private const string TestSourceIdentifier = @"WingetE2E.Tests_8wekyb3d8bbwe"; [OneTimeSetUp] public void OneTimeSetup() @@ -169,7 +167,7 @@ public void InstallPortableExe() string installDir = Path.Combine(System.Environment.GetEnvironmentVariable("LocalAppData"), "Microsoft", "WinGet", "Packages"); string packageId, commandAlias, fileName, packageDirName, productCode; packageId = "AppInstallerTest.TestPortableExe"; - packageDirName = productCode = packageId + "_" + TestSourceIdentifier; + packageDirName = productCode = packageId + "_" + Constants.TestSourceIdentifier; commandAlias = fileName = "AppInstallerTestExeInstaller.exe"; var result = TestCommon.RunAICLICommand("install", "AppInstallerTest.TestPortableExe"); @@ -185,7 +183,7 @@ public void InstallPortableExeWithCommand() var installDir = TestCommon.GetRandomTestDir(); string packageId, commandAlias, fileName, productCode; packageId = "AppInstallerTest.TestPortableExeWithCommand"; - productCode = packageId + "_" + TestSourceIdentifier; + productCode = packageId + "_" + Constants.TestSourceIdentifier; fileName = "AppInstallerTestExeInstaller.exe"; commandAlias = "testCommand.exe"; @@ -201,7 +199,7 @@ public void InstallPortableExeWithRename() var installDir = TestCommon.GetRandomTestDir(); string packageId, productCode, renameArgValue; packageId = "AppInstallerTest.TestPortableExeWithCommand"; - productCode = packageId + "_" + TestSourceIdentifier; + productCode = packageId + "_" + Constants.TestSourceIdentifier; renameArgValue = "testRename.exe"; var result = TestCommon.RunAICLICommand("install", $"{packageId} -l {installDir} --rename {renameArgValue}"); @@ -245,7 +243,7 @@ public void InstallPortableToExistingDirectory() string packageId, commandAlias, fileName, productCode; packageId = "AppInstallerTest.TestPortableExe"; - productCode = packageId + "_" + TestSourceIdentifier; + productCode = packageId + "_" + Constants.TestSourceIdentifier; commandAlias = fileName = "AppInstallerTestExeInstaller.exe"; var result = TestCommon.RunAICLICommand("install", $"AppInstallerTest.TestPortableExe -l {existingDir}"); @@ -254,6 +252,31 @@ public void InstallPortableToExistingDirectory() TestCommon.VerifyPortablePackage(existingDir, commandAlias, fileName, productCode, true); } + [Test] + public void InstallPortableFailsWithCleanup() + { + string winGetDir = Path.Combine(System.Environment.GetEnvironmentVariable("LocalAppData"), "Microsoft", "WinGet"); + string installDir = Path.Combine(winGetDir, "Packages"); + string packageId, commandAlias, fileName, packageDirName, productCode; + packageId = "AppInstallerTest.TestPortableExe"; + packageDirName = productCode = packageId + "_" + Constants.TestSourceIdentifier; + commandAlias = fileName = "AppInstallerTestExeInstaller.exe"; + + // Create a directory with the same name as the symlink in order to cause install to fail. + string symlinkDirectory = Path.Combine(winGetDir, "Links"); + string conflictDirectory = Path.Combine(symlinkDirectory, commandAlias); + Directory.CreateDirectory(conflictDirectory); + + var result = TestCommon.RunAICLICommand("install", "AppInstallerTest.TestPortableExe"); + + // Remove directory prior to assertions as this will impact other tests if assertions fail. + Directory.Delete(conflictDirectory, true); + + Assert.AreNotEqual(Constants.ErrorCode.S_OK, result.ExitCode); + Assert.True(result.StdOut.Contains("Unable to create symlink, path points to a directory.")); + TestCommon.VerifyPortablePackage(Path.Combine(installDir, packageDirName), commandAlias, fileName, productCode, false); + } + private bool VerifyTestExeInstalled(string installDir, string expectedContent = null) { if (!File.Exists(Path.Combine(installDir, Constants.TestExeInstalledFileName))) diff --git a/src/AppInstallerCLIE2ETests/TestCommon.cs b/src/AppInstallerCLIE2ETests/TestCommon.cs index 20413e3a25..d9155190ca 100644 --- a/src/AppInstallerCLIE2ETests/TestCommon.cs +++ b/src/AppInstallerCLIE2ETests/TestCommon.cs @@ -286,38 +286,38 @@ public static void VerifyPortablePackage( bool shouldExist) { string exePath = Path.Combine(installDir, filename); - FileInfo exeFile = new FileInfo(exePath); - Assert.AreEqual(shouldExist, exeFile.Exists, $"Expected portable exe path: {exePath}"); + bool exeExists = File.Exists(exePath); string symlinkDirectory = Path.Combine(System.Environment.GetEnvironmentVariable("LocalAppData"), "Microsoft", "WinGet", "Links"); string symlinkPath = Path.Combine(symlinkDirectory, commandAlias); - FileInfo symlinkFile = new FileInfo(symlinkPath); - Assert.AreEqual(shouldExist, symlinkFile.Exists, $"Expected portable symlink path: {symlinkPath}"); + bool symlinkExists = File.Exists(symlinkPath); + bool portableEntryExists; string subKey = @$"Software\Microsoft\Windows\CurrentVersion\Uninstall"; using (RegistryKey uninstallRegistryKey = Registry.CurrentUser.OpenSubKey(subKey, true)) { RegistryKey portableEntry = uninstallRegistryKey.OpenSubKey(productCode, true); - Assert.AreEqual(shouldExist, portableEntry != null, $"Expected {productCode} subkey in path: {subKey}"); - // TODO: Remove delete once uninstall is implemented. - uninstallRegistryKey.DeleteSubKey(productCode); + portableEntryExists = portableEntry != null; } + bool isAddedToPath; using (RegistryKey environmentRegistryKey = Registry.CurrentUser.OpenSubKey(@"Environment", true)) { string pathName = "Path"; var currentPathValue = (string)environmentRegistryKey.GetValue(pathName); var portablePathValue = symlinkDirectory + ';'; - bool isAddedToPath = currentPathValue.Contains(portablePathValue); - if (isAddedToPath) - { - string initialPathValue = currentPathValue.Replace(portablePathValue, ""); - environmentRegistryKey.SetValue(pathName, initialPathValue); - } - Assert.AreEqual(shouldExist, isAddedToPath, $"Expected path variable: {portablePathValue}"); + isAddedToPath = currentPathValue.Contains(portablePathValue); + } + + if (shouldExist) + { + RunAICLICommand("uninstall", $"--product-code {productCode}"); } - // TODO: Call uninstall command for cleanup when implemented + Assert.AreEqual(shouldExist, exeExists, $"Expected portable exe path: {exePath}"); + Assert.AreEqual(shouldExist, symlinkExists, $"Expected portable symlink path: {symlinkPath}"); + Assert.AreEqual(shouldExist, portableEntryExists, $"Expected {productCode} subkey in path: {subKey}"); + Assert.AreEqual(shouldExist, isAddedToPath, $"Expected path variable: {symlinkDirectory}"); } /// diff --git a/src/AppInstallerCLIE2ETests/TestData/Manifests/TestPortableInstaller.2.0.0.0.yaml b/src/AppInstallerCLIE2ETests/TestData/Manifests/TestPortableInstaller.2.0.0.0.yaml new file mode 100644 index 0000000000..56af85e3e1 --- /dev/null +++ b/src/AppInstallerCLIE2ETests/TestData/Manifests/TestPortableInstaller.2.0.0.0.yaml @@ -0,0 +1,14 @@ +PackageIdentifier: AppInstallerTest.TestPortableExe +PackageVersion: 2.0.0.0 +PackageName: TestPortableExe +PackageLocale: en-US +Publisher: AppInstallerTest +License: Test +ShortDescription: E2E test for portable install. +Installers: + - Architecture: x64 + InstallerUrl: https://localhost:5001/TestKit/AppInstallerTestExeInstaller/AppInstallerTestExeInstaller.exe + InstallerType: portable + InstallerSha256: +ManifestType: singleton +ManifestVersion: 1.2.0 \ No newline at end of file diff --git a/src/AppInstallerCLIE2ETests/TestData/Manifests/TestPortableInstaller.3.0.0.0_UninstallPrevious.yaml b/src/AppInstallerCLIE2ETests/TestData/Manifests/TestPortableInstaller.3.0.0.0_UninstallPrevious.yaml new file mode 100644 index 0000000000..220c564adf --- /dev/null +++ b/src/AppInstallerCLIE2ETests/TestData/Manifests/TestPortableInstaller.3.0.0.0_UninstallPrevious.yaml @@ -0,0 +1,15 @@ +PackageIdentifier: AppInstallerTest.TestPortableExe +PackageVersion: 3.0.0.0 +PackageName: TestPortableExe +PackageLocale: en-US +Publisher: AppInstallerTest +License: Test +ShortDescription: E2E test for portable install. +Installers: + - Architecture: x64 + InstallerUrl: https://localhost:5001/TestKit/AppInstallerTestExeInstaller/AppInstallerTestExeInstaller.exe + InstallerType: portable + InstallerSha256: + UpgradeBehavior: uninstallPrevious +ManifestType: singleton +ManifestVersion: 1.2.0 \ No newline at end of file diff --git a/src/AppInstallerCLIE2ETests/TestData/Manifests/TestPortableInstallerUninstallPrevious.yaml b/src/AppInstallerCLIE2ETests/TestData/Manifests/TestPortableInstallerUninstallPrevious.yaml new file mode 100644 index 0000000000..aaa1aea5b6 --- /dev/null +++ b/src/AppInstallerCLIE2ETests/TestData/Manifests/TestPortableInstallerUninstallPrevious.yaml @@ -0,0 +1,15 @@ +PackageIdentifier: AppInstallerTest.TestPortableExe +PackageVersion: 1.1.0.0 +PackageName: TestPortableExe +PackageLocale: en-US +Publisher: AppInstallerTest +License: Test +ShortDescription: E2E test for portable install. +Installers: + - Architecture: x64 + InstallerUrl: https://localhost:5001/TestKit/AppInstallerTestExeInstaller/AppInstallerTestExeInstaller.exe + InstallerType: portable + InstallerSha256: + UpgradeBehavior: uninstallPrevious +ManifestType: singleton +ManifestVersion: 1.2.0 \ No newline at end of file diff --git a/src/AppInstallerCLIE2ETests/UninstallCommand.cs b/src/AppInstallerCLIE2ETests/UninstallCommand.cs index caabaecca6..5ca4547c4d 100644 --- a/src/AppInstallerCLIE2ETests/UninstallCommand.cs +++ b/src/AppInstallerCLIE2ETests/UninstallCommand.cs @@ -60,6 +60,40 @@ public void UninstallTestMsix() Assert.True(VerifyTestMsixUninstalled()); } + [Test] + public void UninstallPortable() + { + // Uninstall a Portable + string installDir = Path.Combine(System.Environment.GetEnvironmentVariable("LocalAppData"), "Microsoft", "WinGet", "Packages"); + string packageId, commandAlias, fileName, packageDirName, productCode; + packageId = "AppInstallerTest.TestPortableExe"; + packageDirName = productCode = packageId + "_" + Constants.TestSourceIdentifier; + commandAlias = fileName = "AppInstallerTestExeInstaller.exe"; + + TestCommon.RunAICLICommand("install", $"{packageId}"); + var result = TestCommon.RunAICLICommand("uninstall", $"{packageId}"); + Assert.AreEqual(Constants.ErrorCode.S_OK, result.ExitCode); + Assert.True(result.StdOut.Contains("Successfully uninstalled")); + TestCommon.VerifyPortablePackage(Path.Combine(installDir, packageDirName), commandAlias, fileName, productCode, false); + } + + [Test] + public void UninstallPortableWithProductCode() + { + // Uninstall a Portable + string installDir = Path.Combine(System.Environment.GetEnvironmentVariable("LocalAppData"), "Microsoft", "WinGet", "Packages"); + string packageId, commandAlias, fileName, packageDirName, productCode; + packageId = "AppInstallerTest.TestPortableExe"; + packageDirName = productCode = packageId + "_" + Constants.TestSourceIdentifier; + commandAlias = fileName = "AppInstallerTestExeInstaller.exe"; + + TestCommon.RunAICLICommand("install", $"{packageId}"); + var result = TestCommon.RunAICLICommand("uninstall", $"--product-code {productCode}"); + Assert.AreEqual(Constants.ErrorCode.S_OK, result.ExitCode); + Assert.True(result.StdOut.Contains("Successfully uninstalled")); + TestCommon.VerifyPortablePackage(Path.Combine(installDir, packageDirName), commandAlias, fileName, productCode, false); + } + [Test] public void UninstallNotIndexed() { diff --git a/src/AppInstallerCLIE2ETests/UpdateCommand.cs b/src/AppInstallerCLIE2ETests/UpdateCommand.cs deleted file mode 100644 index 46135cfb03..0000000000 --- a/src/AppInstallerCLIE2ETests/UpdateCommand.cs +++ /dev/null @@ -1,24 +0,0 @@ -// Copyright (c) Microsoft Corporation. -// Licensed under the MIT License. - -namespace AppInstallerCLIE2ETests -{ - using NUnit.Framework; - - public class UpdateCommand : BaseCommand - { - //[Test] - public void UpdateTest() - { - // Example Update Command Test - // TODO: Modify test once final behavior of Update Command is established - var installDir = TestCommon.GetRandomTestDir(); - TestCommon.RunAICLICommand("install", $"AppInstallerTest.OutdatedTestExeInstaller --silent -l {installDir}"); - var result = TestCommon.RunAICLICommand("update", "AppInstallerTest.TestExeInstaller"); - Assert.AreEqual(Constants.ErrorCode.S_OK, result.ExitCode); - Assert.True(result.StdOut.Contains("AppInstallerTest.TestExeInstaller updated")); - result = TestCommon.RunAICLICommand("list", ""); - Assert.True(result.StdOut.Contains("Version: 1.2.3.4")); - } - } -} diff --git a/src/AppInstallerCLIE2ETests/UpgradeCommand.cs b/src/AppInstallerCLIE2ETests/UpgradeCommand.cs new file mode 100644 index 0000000000..08e1979a8d --- /dev/null +++ b/src/AppInstallerCLIE2ETests/UpgradeCommand.cs @@ -0,0 +1,115 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +namespace AppInstallerCLIE2ETests +{ + using Microsoft.Win32; + using NUnit.Framework; + using System.IO; + + public class UpgradeCommand : BaseCommand + { + private const string UninstallSubKey = @"Software\Microsoft\Windows\CurrentVersion\Uninstall"; + private const string WinGetPackageIdentifier = "WinGetPackageIdentifier"; + private const string WinGetSourceIdentifier = "WinGetSourceIdentifier"; + + [OneTimeSetUp] + public void OneTimeSetup() + { + ConfigureFeature("portableInstall", true); + } + + [Test] + public void UpgradePortable() + { + string installDir = Path.Combine(System.Environment.GetEnvironmentVariable("LocalAppData"), "Microsoft", "WinGet", "Packages"); + string packageId, commandAlias, fileName, packageDirName, productCode; + packageId = "AppInstallerTest.TestPortableExe"; + packageDirName = productCode = packageId + "_" + Constants.TestSourceIdentifier; + commandAlias = fileName = "AppInstallerTestExeInstaller.exe"; + + var result = TestCommon.RunAICLICommand("install", "AppInstallerTest.TestPortableExe -v 1.0.0.0"); + Assert.AreEqual(Constants.ErrorCode.S_OK, result.ExitCode); + Assert.True(result.StdOut.Contains("Successfully installed")); + + var result2 = TestCommon.RunAICLICommand("upgrade", $"{packageId} -v 2.0.0.0"); + Assert.AreEqual(Constants.ErrorCode.S_OK, result2.ExitCode); + Assert.True(result2.StdOut.Contains("Successfully installed")); + TestCommon.VerifyPortablePackage(Path.Combine(installDir, packageDirName), commandAlias, fileName, productCode, true); + } + + [Test] + public void UpgradePortableARPMismatch() + { + string packageId = "AppInstallerTest.TestPortableExe"; + string productCode = packageId + "_" + Constants.TestSourceIdentifier; + + var installResult = TestCommon.RunAICLICommand("install", "AppInstallerTest.TestPortableExe -v 1.0.0.0"); + Assert.AreEqual(Constants.ErrorCode.S_OK, installResult.ExitCode); + Assert.True(installResult.StdOut.Contains("Successfully installed")); + + // Modify packageId to cause mismatch. + ModifyPortableARPEntryValue(productCode, WinGetPackageIdentifier, "testPackageId"); + + var upgradeResult = TestCommon.RunAICLICommand("upgrade", $"{packageId} -v 2.0.0.0"); + + // Reset and perform uninstall cleanup + ModifyPortableARPEntryValue(productCode, WinGetPackageIdentifier, packageId); + TestCommon.RunAICLICommand("uninstall", $"--product-code {productCode}"); + + Assert.AreNotEqual(Constants.ErrorCode.S_OK, upgradeResult.ExitCode); + Assert.True(upgradeResult.StdOut.Contains("Portable package from a different source already exists")); + } + + [Test] + public void UpgradePortableForcedOverride() + { + string installDir = Path.Combine(System.Environment.GetEnvironmentVariable("LocalAppData"), "Microsoft", "WinGet", "Packages"); + string packageId, commandAlias, fileName, packageDirName, productCode; + packageId = "AppInstallerTest.TestPortableExe"; + packageDirName = productCode = packageId + "_" + Constants.TestSourceIdentifier; + commandAlias = fileName = "AppInstallerTestExeInstaller.exe"; + + var installResult = TestCommon.RunAICLICommand("install", "AppInstallerTest.TestPortableExe -v 1.0.0.0"); + Assert.AreEqual(Constants.ErrorCode.S_OK, installResult.ExitCode); + Assert.True(installResult.StdOut.Contains("Successfully installed")); + + // Modify packageId and sourceId to cause mismatch. + ModifyPortableARPEntryValue(productCode, WinGetPackageIdentifier, "testPackageId"); + ModifyPortableARPEntryValue(productCode, WinGetSourceIdentifier, "testSourceId"); + + var upgradeResult = TestCommon.RunAICLICommand("upgrade", $"{packageId} -v 2.0.0.0 --force"); + Assert.AreEqual(Constants.ErrorCode.S_OK, upgradeResult.ExitCode); + Assert.True(upgradeResult.StdOut.Contains("Successfully installed")); + TestCommon.VerifyPortablePackage(Path.Combine(installDir, packageDirName), commandAlias, fileName, productCode, true); + } + + [Test] + public void UpgradePortableUninstallPrevious() + { + string installDir = Path.Combine(System.Environment.GetEnvironmentVariable("LocalAppData"), "Microsoft", "WinGet", "Packages"); + string packageId, commandAlias, fileName, packageDirName, productCode; + packageId = "AppInstallerTest.TestPortableExe"; + packageDirName = productCode = packageId + "_" + Constants.TestSourceIdentifier; + commandAlias = fileName = "AppInstallerTestExeInstaller.exe"; + + var result = TestCommon.RunAICLICommand("install", $"{packageId} -v 1.0.0.0"); + Assert.AreEqual(Constants.ErrorCode.S_OK, result.ExitCode); + Assert.True(result.StdOut.Contains("Successfully installed")); + + var result2 = TestCommon.RunAICLICommand("upgrade", $"{packageId} -v 3.0.0.0"); + Assert.AreEqual(Constants.ErrorCode.S_OK, result2.ExitCode); + Assert.True(result2.StdOut.Contains("Successfully installed")); + TestCommon.VerifyPortablePackage(Path.Combine(installDir, packageDirName), commandAlias, fileName, productCode, true); + } + + private void ModifyPortableARPEntryValue(string productCode, string name, string value) + { + using (RegistryKey uninstallRegistryKey = Registry.CurrentUser.OpenSubKey(UninstallSubKey, true)) + { + RegistryKey entry = uninstallRegistryKey.OpenSubKey(productCode, true); + entry.SetValue(name, value); + } + } + } +} diff --git a/src/AppInstallerCLIPackage/Shared/Strings/en-us/winget.resw b/src/AppInstallerCLIPackage/Shared/Strings/en-us/winget.resw index 9db99604a8..be5a075a7c 100644 --- a/src/AppInstallerCLIPackage/Shared/Strings/en-us/winget.resw +++ b/src/AppInstallerCLIPackage/Shared/Strings/en-us/winget.resw @@ -723,7 +723,7 @@ They can be configured through the settings file 'winget settings'. File does not exist: - Both local manifest and search query args are provided + Both local manifest and search query arguments are provided Logs @@ -1314,4 +1314,27 @@ Please specify one of them using the `--source` option to proceed. No package selection argument was provided; see the help for details about finding a package. + + Portable install failed; Cleaning up... + + + Both `purge` and `preserve` arguments are provided + + + Portable exe has been modified; proceeding due to --force + {Locked="--force"} + + + Unable to remove Portable exe as it has been modified; to override this check use --force + {Locked="--force"} + + + Files remain in install directory: + + + Purging install directory... + + + Cannot purge install directory, as it was not created by WinGet + \ No newline at end of file diff --git a/src/AppInstallerCLITests/AppInstallerCLITests.vcxproj b/src/AppInstallerCLITests/AppInstallerCLITests.vcxproj index 80b9f29c4c..407bcc0e32 100644 --- a/src/AppInstallerCLITests/AppInstallerCLITests.vcxproj +++ b/src/AppInstallerCLITests/AppInstallerCLITests.vcxproj @@ -269,10 +269,10 @@ true - + true - + true @@ -552,6 +552,9 @@ true + + true + true diff --git a/src/AppInstallerCLITests/AppInstallerCLITests.vcxproj.filters b/src/AppInstallerCLITests/AppInstallerCLITests.vcxproj.filters index a331bbf5e2..6d2df2ae11 100644 --- a/src/AppInstallerCLITests/AppInstallerCLITests.vcxproj.filters +++ b/src/AppInstallerCLITests/AppInstallerCLITests.vcxproj.filters @@ -440,10 +440,10 @@ TestData - - + + TestData - + TestData @@ -477,6 +477,9 @@ TestData + + TestData + TestData diff --git a/src/AppInstallerCLITests/TestData/InstallFlowTest_Portable.yaml b/src/AppInstallerCLITests/TestData/InstallFlowTest_Portable.yaml index bdf40ee137..005f4e28b6 100644 --- a/src/AppInstallerCLITests/TestData/InstallFlowTest_Portable.yaml +++ b/src/AppInstallerCLITests/TestData/InstallFlowTest_Portable.yaml @@ -1,11 +1,11 @@ -PackageIdentifier: AppInstallerCliTest.TestPortable +PackageIdentifier: AppInstallerCliTest.TestPortableInstaller PackageVersion: 1.0.0.0 PackageLocale: en-US PackageName: AppInstaller Test Portable Exe Publisher: Microsoft Corporation AppMoniker: AICLITestPortable License: Test -ProductCode: AppInstallerCliTest.TestExeInstaller +ProductCode: AppInstallerCliTest.TestPortable__DefaultSource Installers: - Architecture: x64 InstallerUrl: https://ThisIsNotUsed diff --git a/src/AppInstallerCLITests/TestData/InstallFlowTest_Portable_WithCommand.yaml b/src/AppInstallerCLITests/TestData/InstallFlowTest_Portable_WithCommand.yaml index 52501bff94..99f7b62604 100644 --- a/src/AppInstallerCLITests/TestData/InstallFlowTest_Portable_WithCommand.yaml +++ b/src/AppInstallerCLITests/TestData/InstallFlowTest_Portable_WithCommand.yaml @@ -1,4 +1,4 @@ -PackageIdentifier: AppInstallerCliTest.TestPortable +PackageIdentifier: AppInstallerCliTest.TestPortableInstaller PackageVersion: 1.0.0.0 PackageLocale: en-US PackageName: AppInstaller Test Portable Exe diff --git a/src/AppInstallerCLITests/TestData/UpdateFlowTest_Portable.yaml b/src/AppInstallerCLITests/TestData/UpdateFlowTest_Portable.yaml new file mode 100644 index 0000000000..7b26d5da94 --- /dev/null +++ b/src/AppInstallerCLITests/TestData/UpdateFlowTest_Portable.yaml @@ -0,0 +1,15 @@ +# Same content with InstallFlowTest_Portable.yaml but with higher version +PackageIdentifier: AppInstallerCliTest.TestPortableInstaller +PackageVersion: 2.0.0.0 +PackageLocale: en-US +PackageName: AppInstaller Test Portable Exe +Publisher: Microsoft Corporation +AppMoniker: AICLITestPortable +License: Test +Installers: + - Architecture: x64 + InstallerUrl: https://ThisIsNotUsed + InstallerType: portable + InstallerSha256: 65DB2F2AC2686C7F2FD69D4A4C6683B888DC55BFA20A0E32CA9F838B51689A3B +ManifestType: singleton +ManifestVersion: 1.2.0 \ No newline at end of file diff --git a/src/AppInstallerCLITests/WorkFlow.cpp b/src/AppInstallerCLITests/WorkFlow.cpp index ef22944c58..ff54a68543 100644 --- a/src/AppInstallerCLITests/WorkFlow.cpp +++ b/src/AppInstallerCLITests/WorkFlow.cpp @@ -14,7 +14,7 @@ #include #include #include -#include +#include #include #include #include @@ -152,6 +152,21 @@ namespace PackageMatchFilter(PackageMatchField::Id, MatchType::Exact, "AppInstallerCliTest.TestExeInstaller"))); } + if (input.empty() || input == "AppInstallerCliTest.TestPortableInstaller") + { + auto manifest = YamlParser::CreateFromPath(TestDataFile("InstallFlowTest_Portable.yaml")); + auto manifest2 = YamlParser::CreateFromPath(TestDataFile("UpdateFlowTest_Portable.yaml")); + result.Matches.emplace_back( + ResultMatch( + TestPackage::Make( + manifest, + TestPackage::MetadataMap{ { PackageVersionMetadata::InstalledType, "Portable" } }, + std::vector{ manifest2, manifest }, + shared_from_this() + ), + PackageMatchFilter(PackageMatchField::Id, MatchType::Exact, "AppInstallerCliTest.TestPortableInstaller"))); + } + if (input.empty() || input == "AppInstallerCliTest.TestMsixInstaller") { auto manifest = YamlParser::CreateFromPath(TestDataFile("InstallFlowTest_Msix_StreamingFlow.yaml")); @@ -527,6 +542,17 @@ void OverrideForShellExecute(TestContext& context, std::vector& inst } void OverrideForPortableInstall(TestContext& context) +{ + context.Override({ PortableInstall, [](TestContext&) + { + std::filesystem::path temp = std::filesystem::temp_directory_path(); + temp /= "TestPortableInstalled.txt"; + std::ofstream file(temp, std::ofstream::out); + file.close(); + } }); +} + +void OverrideForPortableInstallFlow(TestContext& context) { context.Override({ DownloadInstallerFile, [](TestContext& context) { @@ -539,14 +565,26 @@ void OverrideForPortableInstall(TestContext& context) } }); OverrideForUpdateInstallerMotw(context); + OverrideForPortableInstall(context); +} - context.Override({ PortableInstall, [](TestContext&) +void OverrideForPortableUninstall(TestContext& context) +{ + context.Override({ PortableUninstallImpl, [](TestContext& context) { - // Write out the install command std::filesystem::path temp = std::filesystem::temp_directory_path(); - temp /= "TestPortableInstalled.txt"; + temp /= "TestPortableUninstalled.txt"; std::ofstream file(temp, std::ofstream::out); file.close(); + + context.Add(0); + } }); +} + +void OverrideForEnsureSupportForPortable(TestContext& context) +{ + context.Override({ EnsureSupportForPortableInstall, [](TestContext&) + { } }); } @@ -590,6 +628,8 @@ void OverrideForExeUninstall(TestContext& context) std::ofstream file(temp, std::ofstream::out); file << context.Get(); file.close(); + + context.Add(0); } }); } @@ -908,7 +948,7 @@ TEST_CASE("PortableInstallFlow", "[InstallFlow][workflow]") std::ostringstream installOutput; TestContext context{ installOutput, std::cin }; auto previousThreadGlobals = context.SetForCurrentThread(); - OverrideForPortableInstall(context); + OverrideForPortableInstallFlow(context); context.Args.AddArg(Execution::Args::Type::Manifest, TestDataFile("InstallFlowTest_Portable.yaml").GetPath().u8string()); context.Args.AddArg(Execution::Args::Type::InstallLocation, tempDirectory); @@ -1453,6 +1493,41 @@ TEST_CASE("UpdateFlow_UpdateExe", "[UpdateFlow][workflow]") REQUIRE(updateResultStr.find("/ver3.0.0.0") != std::string::npos); } +TEST_CASE("UpdateFlow_UpdatePortable", "[UpdateFlow][workflow]") +{ + TestCommon::TempFile updateResultPath("TestPortableInstalled.txt"); + + std::ostringstream updateOutput; + TestContext context{ updateOutput, std::cin }; + auto previousThreadGlobals = context.SetForCurrentThread(); + OverrideForCompositeInstalledSource(context); + OverrideForPortableInstallFlow(context); + context.Args.AddArg(Execution::Args::Type::Query, "AppInstallerCliTest.TestPortableInstaller"sv); + + UpgradeCommand update({}); + update.Execute(context); + INFO(updateOutput.str()); + REQUIRE(std::filesystem::exists(updateResultPath.GetPath())); +} + +TEST_CASE("UpdateFlow_UpdatePortableWithManifest", "[UpdateFlow][workflow]") +{ + TestCommon::TempFile updateResultPath("TestPortableInstalled.txt"); + + std::ostringstream updateOutput; + TestContext context{ updateOutput, std::cin }; + auto previousThreadGlobals = context.SetForCurrentThread(); + OverrideForCompositeInstalledSource(context); + OverrideForEnsureSupportForPortable(context); + OverrideForPortableInstallFlow(context); + context.Args.AddArg(Execution::Args::Type::Manifest, TestDataFile("UpdateFlowTest_Portable.yaml").GetPath().u8string()); + + UpgradeCommand update({}); + update.Execute(context); + INFO(updateOutput.str()); + REQUIRE(std::filesystem::exists(updateResultPath.GetPath())); +} + TEST_CASE("UpdateFlow_UpdateMsix", "[UpdateFlow][workflow]") { TestCommon::TempFile updateResultPath("TestMsixInstalled.txt"); @@ -1625,6 +1700,7 @@ TEST_CASE("UpdateFlow_UpdateAllApplicable", "[UpdateFlow][workflow]") TestCommon::TempFile updateExeResultPath("TestExeInstalled.txt"); TestCommon::TempFile updateMsixResultPath("TestMsixInstalled.txt"); TestCommon::TempFile updateMSStoreResultPath("TestMSStoreUpdated.txt"); + TestCommon::TempFile updatePortableResultPath("TestPortableInstalled.txt"); std::ostringstream updateOutput; TestContext context{ updateOutput, std::cin }; @@ -1633,6 +1709,7 @@ TEST_CASE("UpdateFlow_UpdateAllApplicable", "[UpdateFlow][workflow]") OverrideForShellExecute(context); OverrideForMSIX(context); OverrideForMSStore(context, true); + OverrideForPortableInstall(context); context.Args.AddArg(Execution::Args::Type::All); UpgradeCommand update({}); @@ -1643,6 +1720,7 @@ TEST_CASE("UpdateFlow_UpdateAllApplicable", "[UpdateFlow][workflow]") REQUIRE(std::filesystem::exists(updateExeResultPath.GetPath())); REQUIRE(std::filesystem::exists(updateMsixResultPath.GetPath())); REQUIRE(std::filesystem::exists(updateMSStoreResultPath.GetPath())); + REQUIRE(std::filesystem::exists(updatePortableResultPath.GetPath())); } TEST_CASE("UpdateFlow_UpgradeWithDuplicateUpgradeItemsFound", "[UpdateFlow][workflow]") @@ -1748,6 +1826,7 @@ TEST_CASE("UpdateFlow_All_LicenseAgreement", "[UpdateFlow][workflow]") TestCommon::TempFile updateExeResultPath("TestExeInstalled.txt"); TestCommon::TempFile updateMsixResultPath("TestMsixInstalled.txt"); TestCommon::TempFile updateMSStoreResultPath("TestMSStoreUpdated.txt"); + TestCommon::TempFile updatePortableResultPath("TestPortableInstalled.txt"); std::ostringstream updateOutput; TestContext context{ updateOutput, std::cin }; @@ -1756,6 +1835,7 @@ TEST_CASE("UpdateFlow_All_LicenseAgreement", "[UpdateFlow][workflow]") OverrideForShellExecute(context); OverrideForMSIX(context); OverrideForMSStore(context, true); + OverrideForPortableInstall(context); context.Args.AddArg(Execution::Args::Type::All); context.Args.AddArg(Execution::Args::Type::AcceptPackageAgreements); @@ -1773,6 +1853,7 @@ TEST_CASE("UpdateFlow_All_LicenseAgreement", "[UpdateFlow][workflow]") REQUIRE(std::filesystem::exists(updateExeResultPath.GetPath())); REQUIRE(std::filesystem::exists(updateMsixResultPath.GetPath())); REQUIRE(std::filesystem::exists(updateMSStoreResultPath.GetPath())); + REQUIRE(std::filesystem::exists(updatePortableResultPath.GetPath())); } TEST_CASE("UpdateFlow_All_LicenseAgreement_NotAccepted", "[UpdateFlow][workflow]") @@ -1807,6 +1888,23 @@ TEST_CASE("UpdateFlow_All_LicenseAgreement_NotAccepted", "[UpdateFlow][workflow] REQUIRE_FALSE(std::filesystem::exists(updateMSStoreResultPath.GetPath())); } +TEST_CASE("UninstallFlow_UninstallPortable", "[UninstallFlow][workflow]") +{ + TestCommon::TempFile uninstallResultPath("TestPortableUninstalled.txt"); + + std::ostringstream uninstallOutput; + TestContext context{ uninstallOutput, std::cin }; + auto previousThreadGlobals = context.SetForCurrentThread(); + OverrideForCompositeInstalledSource(context); + OverrideForPortableUninstall(context); + context.Args.AddArg(Execution::Args::Type::Query, "AppInstallerCliTest.TestPortableInstaller"sv); + + UninstallCommand uninstall({}); + uninstall.Execute(context); + INFO(uninstallOutput.str()); + REQUIRE(std::filesystem::exists(uninstallResultPath.GetPath())); +} + TEST_CASE("UninstallFlow_UninstallExe", "[UninstallFlow][workflow]") { TestCommon::TempFile uninstallResultPath("TestExeUninstalled.txt"); @@ -1922,7 +2020,7 @@ TEST_CASE("ExportFlow_ExportAll", "[ExportFlow][workflow]") REQUIRE(exportedCollection.Sources[0].Details.Identifier == "*TestSource"); const auto& exportedPackages = exportedCollection.Sources[0].Packages; - REQUIRE(exportedPackages.size() == 3); + REQUIRE(exportedPackages.size() == 4); REQUIRE(exportedPackages.end() != std::find_if(exportedPackages.begin(), exportedPackages.end(), [](const auto& p) { return p.Id == "AppInstallerCliTest.TestExeInstaller" && p.VersionAndChannel.GetVersion().ToString().empty(); @@ -1935,6 +2033,10 @@ TEST_CASE("ExportFlow_ExportAll", "[ExportFlow][workflow]") { return p.Id == "AppInstallerCliTest.TestMSStoreInstaller" && p.VersionAndChannel.GetVersion().ToString().empty(); })); + REQUIRE(exportedPackages.end() != std::find_if(exportedPackages.begin(), exportedPackages.end(), [](const auto& p) + { + return p.Id == "AppInstallerCliTest.TestPortableInstaller" && p.VersionAndChannel.GetVersion().ToString().empty(); + })); } TEST_CASE("ExportFlow_ExportAll_WithVersions", "[ExportFlow][workflow]") @@ -1958,7 +2060,7 @@ TEST_CASE("ExportFlow_ExportAll_WithVersions", "[ExportFlow][workflow]") REQUIRE(exportedCollection.Sources[0].Details.Identifier == "*TestSource"); const auto& exportedPackages = exportedCollection.Sources[0].Packages; - REQUIRE(exportedPackages.size() == 3); + REQUIRE(exportedPackages.size() == 4); REQUIRE(exportedPackages.end() != std::find_if(exportedPackages.begin(), exportedPackages.end(), [](const auto& p) { return p.Id == "AppInstallerCliTest.TestExeInstaller" && p.VersionAndChannel.GetVersion().ToString() == "1.0.0.0"; @@ -1971,6 +2073,10 @@ TEST_CASE("ExportFlow_ExportAll_WithVersions", "[ExportFlow][workflow]") { return p.Id == "AppInstallerCliTest.TestMSStoreInstaller" && p.VersionAndChannel.GetVersion().ToString() == "Latest"; })); + REQUIRE(exportedPackages.end() != std::find_if(exportedPackages.begin(), exportedPackages.end(), [](const auto& p) + { + return p.Id == "AppInstallerCliTest.TestPortableInstaller" && p.VersionAndChannel.GetVersion().ToString() == "1.0.0.0"; + })); } TEST_CASE("ImportFlow_Successful", "[ImportFlow][workflow]") diff --git a/src/AppInstallerCommonCore/Errors.cpp b/src/AppInstallerCommonCore/Errors.cpp index 20400dce28..2de6f01138 100644 --- a/src/AppInstallerCommonCore/Errors.cpp +++ b/src/AppInstallerCommonCore/Errors.cpp @@ -182,6 +182,8 @@ namespace AppInstaller return "Portable package from a different source already exists."; case APPINSTALLER_CLI_ERROR_PORTABLE_SYMLINK_PATH_IS_DIRECTORY: return "Unable to create symlink, path points to a directory."; + case APPINSTALLER_CLI_ERROR_PORTABLE_UNINSTALL_FAILED: + return "Failed to uninstall portable package"; case APPINSTALLER_CLI_ERROR_INSTALL_PACKAGE_IN_USE: return "Application is currently running.Exit the application then try again."; case APPINSTALLER_CLI_ERROR_INSTALL_INSTALL_IN_PROGRESS: diff --git a/src/AppInstallerCommonCore/GroupPolicy.cpp b/src/AppInstallerCommonCore/GroupPolicy.cpp index 777ea675fa..48d95c3471 100644 --- a/src/AppInstallerCommonCore/GroupPolicy.cpp +++ b/src/AppInstallerCommonCore/GroupPolicy.cpp @@ -133,7 +133,7 @@ namespace AppInstaller::Settings return std::nullopt; } - std::vector items; + typename Mapping::value_t items; for (const auto& value : listKey->Values()) { auto item = Mapping::ReadAndValidateItem(value); diff --git a/src/AppInstallerCommonCore/PortableARPEntry.cpp b/src/AppInstallerCommonCore/PortableARPEntry.cpp index 6b47814b4e..80a50f4b71 100644 --- a/src/AppInstallerCommonCore/PortableARPEntry.cpp +++ b/src/AppInstallerCommonCore/PortableARPEntry.cpp @@ -31,31 +31,35 @@ namespace AppInstaller::Registry::Portable constexpr std::wstring_view s_InstallDirectoryCreated = L"InstallDirectoryCreated"; } - PortableARPEntry::PortableARPEntry(Manifest::ScopeEnum scope, Utility::Architecture arch, const std::wstring& productCode) + PortableARPEntry::PortableARPEntry(Manifest::ScopeEnum scope, Utility::Architecture arch, const std::string& productCode) { - HKEY root; - std::wstring subKey; - if (scope == Manifest::ScopeEnum::Machine) + m_scope = scope; + m_arch = arch; + + if (m_scope == Manifest::ScopeEnum::Machine) { - root = HKEY_LOCAL_MACHINE; - if (arch == Utility::Architecture::X64) + m_root = HKEY_LOCAL_MACHINE; + if (m_arch == Utility::Architecture::X64) { - subKey = s_UninstallRegistryX64; + m_subKey = s_UninstallRegistryX64; + m_samDesired = KEY_WOW64_64KEY; } else { - subKey = s_UninstallRegistryX86; + m_subKey = s_UninstallRegistryX86; + m_samDesired = KEY_WOW64_32KEY; } } else { // HKCU uninstall registry share the x64 registry view. - root = HKEY_CURRENT_USER; - subKey = s_UninstallRegistryX64; + m_root = HKEY_CURRENT_USER; + m_subKey = s_UninstallRegistryX64; + m_samDesired = KEY_WOW64_64KEY; } - subKey += L"\\" + productCode; - m_key = Key::OpenIfExists(root, subKey, 0, KEY_ALL_ACCESS); + m_subKey += L"\\" + ConvertToUTF16(productCode); + m_key = Key::OpenIfExists(m_root, m_subKey, 0, KEY_ALL_ACCESS); if (m_key != NULL) { m_exists = true; @@ -63,7 +67,7 @@ namespace AppInstaller::Registry::Portable else { m_exists = false; - m_key = Key::Create(root, subKey); + m_key = Key::Create(m_root, m_subKey); } } @@ -111,6 +115,11 @@ namespace AppInstaller::Registry::Portable return isSamePackageId && isSamePackageSource; } + std::optional PortableARPEntry::operator[](PortableValueName valueName) const + { + return m_key[std::wstring{ ToString(valueName) }]; + } + void PortableARPEntry::SetValue(PortableValueName valueName, const std::wstring& value) { m_key.SetValue(std::wstring{ ToString(valueName) }, value, REG_SZ); @@ -125,4 +134,9 @@ namespace AppInstaller::Registry::Portable { m_key.SetValue(std::wstring{ ToString(valueName) }, value); } + + void PortableARPEntry::Delete() + { + Registry::Key::Delete(m_root, m_subKey, m_samDesired); + } } \ No newline at end of file diff --git a/src/AppInstallerCommonCore/Public/AppInstallerErrors.h b/src/AppInstallerCommonCore/Public/AppInstallerErrors.h index 9e30f16823..ae04e2f954 100644 --- a/src/AppInstallerCommonCore/Public/AppInstallerErrors.h +++ b/src/AppInstallerCommonCore/Public/AppInstallerErrors.h @@ -99,6 +99,7 @@ #define APPINSTALLER_CLI_ERROR_PORTABLE_PACKAGE_ALREADY_EXISTS ((HRESULT)0x8A150054) #define APPINSTALLER_CLI_ERROR_PORTABLE_SYMLINK_PATH_IS_DIRECTORY ((HRESULT)0x8A150055) #define APPINSTALLER_CLI_ERROR_INSTALLER_PROHIBITS_ELEVATION ((HRESULT)0x8A150056) +#define APPINSTALLER_CLI_ERROR_PORTABLE_UNINSTALL_FAILED ((HRESULT)0x8A150057) // Install errors. #define APPINSTALLER_CLI_ERROR_INSTALL_PACKAGE_IN_USE ((HRESULT)0x8A150101) diff --git a/src/AppInstallerCommonCore/Public/winget/PortableARPEntry.h b/src/AppInstallerCommonCore/Public/winget/PortableARPEntry.h index 5c3757502c..88b8df75fd 100644 --- a/src/AppInstallerCommonCore/Public/winget/PortableARPEntry.h +++ b/src/AppInstallerCommonCore/Public/winget/PortableARPEntry.h @@ -29,23 +29,32 @@ namespace AppInstaller::Registry::Portable struct PortableARPEntry : Registry::Key { - PortableARPEntry(Manifest::ScopeEnum scope, Utility::Architecture arch, const std::wstring& productCode); + PortableARPEntry(Manifest::ScopeEnum scope, Utility::Architecture arch, const std::string& productCode); + + std::optional operator[](PortableValueName valueName) const; bool IsSamePortablePackageEntry(const std::string& packageId, const std::string& sourceId); bool Exists() { return m_exists; } void SetValue(PortableValueName valueName, const std::wstring& value); - void SetValue(PortableValueName valueName, const std::string_view& value); - void SetValue(PortableValueName valueName, bool& value); + void Delete(); + Registry::Key GetKey() { return m_key; }; + Manifest::ScopeEnum GetScope() { return m_scope; }; + Utility::Architecture GetArchitecture() { return m_arch; }; private: bool m_exists = false; Key m_key; + HKEY m_root; + std::wstring m_subKey; + DWORD m_samDesired; + Manifest::ScopeEnum m_scope; + Utility::Architecture m_arch; }; } \ No newline at end of file diff --git a/src/AppInstallerCommonCore/Public/winget/UserSettings.h b/src/AppInstallerCommonCore/Public/winget/UserSettings.h index 7272cad579..f215db2baa 100644 --- a/src/AppInstallerCommonCore/Public/winget/UserSettings.h +++ b/src/AppInstallerCommonCore/Public/winget/UserSettings.h @@ -89,7 +89,7 @@ namespace AppInstaller::Settings InstallIgnoreWarnings, PortableAppUserRoot, PortableAppMachineRoot, - UninstallPurgePortableApp, + UninstallPurgePortablePackage, Max }; @@ -141,7 +141,7 @@ namespace AppInstaller::Settings SETTINGMAPPING_SPECIALIZATION(Setting::InstallIgnoreWarnings, bool, bool, false, ".installBehavior.ignoreWarnings"sv); SETTINGMAPPING_SPECIALIZATION(Setting::PortableAppUserRoot, std::string, std::filesystem::path, {}, ".installBehavior.portableAppUserRoot"sv); SETTINGMAPPING_SPECIALIZATION(Setting::PortableAppMachineRoot, std::string, std::filesystem::path, {}, ".installBehavior.portableAppMachineRoot"sv); - SETTINGMAPPING_SPECIALIZATION(Setting::UninstallPurgePortableApp, bool, bool, false, ".uninstallBehavior.purgePortableApp"sv); + SETTINGMAPPING_SPECIALIZATION(Setting::UninstallPurgePortablePackage, bool, bool, false, ".uninstallBehavior.purgePortablePackage"sv); SETTINGMAPPING_SPECIALIZATION(Setting::EFDirectMSI, bool, bool, false, ".experimentalFeatures.directMSI"sv); SETTINGMAPPING_SPECIALIZATION(Setting::EnableSelfInitiatedMinidump, bool, bool, false, ".debugging.enableSelfInitiatedMinidump"sv); SETTINGMAPPING_SPECIALIZATION(Setting::LoggingLevelPreference, std::string, Logging::Level, Logging::Level::Info, ".logging.level"sv); diff --git a/src/AppInstallerCommonCore/UserSettings.cpp b/src/AppInstallerCommonCore/UserSettings.cpp index c8b50d3838..799714d509 100644 --- a/src/AppInstallerCommonCore/UserSettings.cpp +++ b/src/AppInstallerCommonCore/UserSettings.cpp @@ -239,7 +239,7 @@ namespace AppInstaller::Settings WINGET_VALIDATE_PASS_THROUGH(EFDirectMSI) WINGET_VALIDATE_PASS_THROUGH(EnableSelfInitiatedMinidump) WINGET_VALIDATE_PASS_THROUGH(InstallIgnoreWarnings) - WINGET_VALIDATE_PASS_THROUGH(UninstallPurgePortableApp) + WINGET_VALIDATE_PASS_THROUGH(UninstallPurgePortablePackage) WINGET_VALIDATE_SIGNATURE(PortableAppUserRoot) { diff --git a/src/AppInstallerRepositoryCore/Microsoft/ARPHelper.cpp b/src/AppInstallerRepositoryCore/Microsoft/ARPHelper.cpp index 1ade9f72da..41ec717a49 100644 --- a/src/AppInstallerRepositoryCore/Microsoft/ARPHelper.cpp +++ b/src/AppInstallerRepositoryCore/Microsoft/ARPHelper.cpp @@ -2,9 +2,12 @@ // Licensed under the MIT License. #include "pch.h" #include "ARPHelper.h" +#include "winget/PortableARPEntry.h" namespace AppInstaller::Repository::Microsoft { + using namespace AppInstaller::Registry::Portable; + Registry::Key ARPHelper::GetARPKey(Manifest::ScopeEnum scope, Utility::Architecture architecture) const { HKEY rootKey = NULL; @@ -97,6 +100,17 @@ namespace AppInstaller::Repository::Microsoft return (value && value->GetType() == Registry::Value::Type::DWord && value->GetValue()); } + std::string ARPHelper::GetStringValue(const Registry::Key& arpKey, const std::wstring& name) + { + auto value = arpKey[name]; + if (value && value->GetType() == Registry::Value::Type::String) + { + return value->GetValue(); + } + + return {}; + } + std::string ARPHelper::DetermineVersion(const Registry::Key& arpKey) const { // First check DisplayVersion for a complete version string @@ -342,6 +356,13 @@ namespace AppInstaller::Repository::Microsoft installedType = Manifest::InstallerTypeEnum::Msi; } + if (Manifest::ConvertToInstallerTypeEnum(GetStringValue(arpKey, std::wstring{ ToString(PortableValueName::WinGetInstallerType) })) == Manifest::InstallerTypeEnum::Portable) + { + // Portable uninstall requires the installed architecture for locating the entry in the registry. + index.SetMetadataByManifestId(manifestId, PackageVersionMetadata::InstalledArchitecture, architecture); + installedType = Manifest::InstallerTypeEnum::Portable; + } + index.SetMetadataByManifestId(manifestId, PackageVersionMetadata::InstalledType, Manifest::InstallerTypeToString(installedType)); } catch (...) diff --git a/src/AppInstallerRepositoryCore/Microsoft/ARPHelper.h b/src/AppInstallerRepositoryCore/Microsoft/ARPHelper.h index e1d0616402..99f01e7bb9 100644 --- a/src/AppInstallerRepositoryCore/Microsoft/ARPHelper.h +++ b/src/AppInstallerRepositoryCore/Microsoft/ARPHelper.h @@ -59,6 +59,9 @@ namespace AppInstaller::Repository::Microsoft // Returns true IFF the value exists and contains a non-zero DWORD. static bool GetBoolValue(const Registry::Key& arpKey, const std::wstring& name); + // Returns the string value if it exists. + static std::string GetStringValue(const Registry::Key& arpKey, const std::wstring& name); + // Determines the version from an ARP entry. // The priority is: // DisplayVersion diff --git a/src/AppInstallerRepositoryCore/Public/winget/RepositorySearch.h b/src/AppInstallerRepositoryCore/Public/winget/RepositorySearch.h index c85a07476c..6cf7dc6a5e 100644 --- a/src/AppInstallerRepositoryCore/Public/winget/RepositorySearch.h +++ b/src/AppInstallerRepositoryCore/Public/winget/RepositorySearch.h @@ -155,7 +155,7 @@ namespace AppInstaller::Repository Locale, }; - // A metadata item of a package version. + // A metadata item of a package version. These values are persisted and cannot be changed. enum class PackageVersionMetadata : int32_t { // The InstallerType of an installed package @@ -174,6 +174,8 @@ namespace AppInstaller::Repository InstalledLocale, // The write time for the given version TrackingWriteTime, + // The Architecture of an installed package + InstalledArchitecture, }; // Convert a PackageVersionMetadata to a string.