winget-cli

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

commit 4df817b3e9fec7f62a57eefb0ab41be4ff6a23dd
parent 3b356a82cb0b58dc7a3c737e786eb85bd03b9ee3
Author: Ryan Fu <69221034+ryfu-msft@users.noreply.github.com>
Date:   Fri, 13 May 2022 18:37:11 -0700

Implementation for Portable Uninstall and Upgrade (#2140)


Diffstat:
Mschemas/JSON/settings/settings.schema.0.2.json | 2+-
Msrc/AppInstallerCLICore/AppInstallerCLICore.vcxproj | 4++--
Msrc/AppInstallerCLICore/AppInstallerCLICore.vcxproj.filters | 4++--
Msrc/AppInstallerCLICore/Argument.cpp | 2+-
Msrc/AppInstallerCLICore/Commands/UninstallCommand.cpp | 11+++++++++++
Msrc/AppInstallerCLICore/Commands/UpgradeCommand.cpp | 1+
Msrc/AppInstallerCLICore/ExecutionContextData.h | 8++++++++
Msrc/AppInstallerCLICore/Resources.h | 7+++++++
Msrc/AppInstallerCLICore/Workflows/InstallFlow.cpp | 9++++++++-
Asrc/AppInstallerCLICore/Workflows/PortableFlow.cpp | 608+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Asrc/AppInstallerCLICore/Workflows/PortableFlow.h | 22++++++++++++++++++++++
Dsrc/AppInstallerCLICore/Workflows/PortableInstallFlow.cpp | 382-------------------------------------------------------------------------------
Dsrc/AppInstallerCLICore/Workflows/PortableInstallFlow.h | 16----------------
Msrc/AppInstallerCLICore/Workflows/ShellExecuteInstallerHandler.cpp | 30+++---------------------------
Msrc/AppInstallerCLICore/Workflows/UninstallFlow.cpp | 73+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++----
Msrc/AppInstallerCLICore/Workflows/UninstallFlow.h | 29+++++++++++++++++++++++------
Msrc/AppInstallerCLICore/Workflows/WorkflowBase.cpp | 21++++++++++++++++++++-
Msrc/AppInstallerCLIE2ETests/AppInstallerCLIE2ETests.csproj | 2++
Msrc/AppInstallerCLIE2ETests/BaseCommand.cs | 1-
Msrc/AppInstallerCLIE2ETests/Constants.cs | 1+
Msrc/AppInstallerCLIE2ETests/InstallCommand.cs | 35+++++++++++++++++++++++++++++------
Msrc/AppInstallerCLIE2ETests/TestCommon.cs | 30+++++++++++++++---------------
Asrc/AppInstallerCLIE2ETests/TestData/Manifests/TestPortableInstaller.2.0.0.0.yaml | 15+++++++++++++++
Asrc/AppInstallerCLIE2ETests/TestData/Manifests/TestPortableInstaller.3.0.0.0_UninstallPrevious.yaml | 16++++++++++++++++
Asrc/AppInstallerCLIE2ETests/TestData/Manifests/TestPortableInstallerUninstallPrevious.yaml | 16++++++++++++++++
Msrc/AppInstallerCLIE2ETests/UninstallCommand.cs | 34++++++++++++++++++++++++++++++++++
Dsrc/AppInstallerCLIE2ETests/UpdateCommand.cs | 24------------------------
Asrc/AppInstallerCLIE2ETests/UpgradeCommand.cs | 115+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Msrc/AppInstallerCLIPackage/Shared/Strings/en-us/winget.resw | 25++++++++++++++++++++++++-
Msrc/AppInstallerCLITests/AppInstallerCLITests.vcxproj | 7+++++--
Msrc/AppInstallerCLITests/AppInstallerCLITests.vcxproj.filters | 9++++++---
Msrc/AppInstallerCLITests/TestData/InstallFlowTest_Portable.yaml | 4++--
Msrc/AppInstallerCLITests/TestData/InstallFlowTest_Portable_WithCommand.yaml | 2+-
Asrc/AppInstallerCLITests/TestData/UpdateFlowTest_Portable.yaml | 16++++++++++++++++
Msrc/AppInstallerCLITests/WorkFlow.cpp | 120++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++-----
Msrc/AppInstallerCommonCore/Errors.cpp | 2++
Msrc/AppInstallerCommonCore/GroupPolicy.cpp | 2+-
Msrc/AppInstallerCommonCore/PortableARPEntry.cpp | 40+++++++++++++++++++++++++++-------------
Msrc/AppInstallerCommonCore/Public/AppInstallerErrors.h | 1+
Msrc/AppInstallerCommonCore/Public/winget/PortableARPEntry.h | 15++++++++++++---
Msrc/AppInstallerCommonCore/Public/winget/UserSettings.h | 4++--
Msrc/AppInstallerCommonCore/UserSettings.cpp | 2+-
Msrc/AppInstallerRepositoryCore/Microsoft/ARPHelper.cpp | 21+++++++++++++++++++++
Msrc/AppInstallerRepositoryCore/Microsoft/ARPHelper.h | 3+++
Msrc/AppInstallerRepositoryCore/Public/winget/RepositorySearch.h | 4+++-
45 files changed, 1269 insertions(+), 526 deletions(-)

diff --git 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 @@ -281,7 +281,7 @@ <ClInclude Include="Workflows\ImportExportFlow.h" /> <ClInclude Include="Workflows\MsiInstallFlow.h" /> <ClInclude Include="Workflows\MSStoreInstallerHandler.h" /> - <ClInclude Include="Workflows\PortableInstallFlow.h" /> + <ClInclude Include="Workflows\PortableFlow.h" /> <ClInclude Include="Workflows\SettingsFlow.h" /> <ClInclude Include="Workflows\ShellExecuteInstallerHandler.h" /> <ClInclude Include="Workflows\InstallFlow.h" /> @@ -333,7 +333,7 @@ <ClCompile Include="Workflows\ImportExportFlow.cpp" /> <ClCompile Include="Workflows\MsiInstallFlow.cpp" /> <ClCompile Include="Workflows\MSStoreInstallerHandler.cpp" /> - <ClCompile Include="Workflows\PortableInstallFlow.cpp" /> + <ClCompile Include="Workflows\PortableFlow.cpp" /> <ClCompile Include="Workflows\SettingsFlow.cpp" /> <ClCompile Include="Workflows\ShellExecuteInstallerHandler.cpp" /> <ClCompile Include="Workflows\InstallFlow.cpp" /> diff --git a/src/AppInstallerCLICore/AppInstallerCLICore.vcxproj.filters b/src/AppInstallerCLICore/AppInstallerCLICore.vcxproj.filters @@ -176,7 +176,7 @@ <ClInclude Include="Public\COMContext.h"> <Filter>Public</Filter> </ClInclude> - <ClInclude Include="Workflows\PortableInstallFlow.h"> + <ClInclude Include="Workflows\PortableFlow.h"> <Filter>Workflows</Filter> </ClInclude> </ItemGroup> @@ -322,7 +322,7 @@ <ClCompile Include="Workflows\DownloadFlow.cpp"> <Filter>Workflows</Filter> </ClCompile> - <ClCompile Include="Workflows\PortableInstallFlow.cpp"> + <ClCompile Include="Workflows\PortableFlow.cpp"> <Filter>Workflows</Filter> </ClCompile> </ItemGroup> diff --git 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 @@ -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 @@ -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 @@ -4,6 +4,7 @@ #include <winget/RepositorySource.h> #include <winget/Manifest.h> #include <winget/ARPCorrelation.h> +#include <winget/PortableARPEntry.h> #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<Utility::Architecture>; }; + + template <> + struct DataMapping<Data::PortableARPEntry> + { + using value_t = Registry::Portable::PortableARPEntry; + }; } } diff --git 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 @@ -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 <AppInstallerDeployment.h> @@ -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/PortableFlow.cpp b/src/AppInstallerCLICore/Workflows/PortableFlow.cpp @@ -0,0 +1,607 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. +#include "pch.h" +#include "PortableFlow.h" +#include "winget/Filesystem.h" +#include "winget/PortableARPEntry.h" +#include "AppInstallerStrings.h" + +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 +{ + namespace + { + 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_DefaultSource = "*DefaultSource"sv; + + void AppendExeExtension(std::filesystem::path& value) + { + if (value.extension() != ".exe") + { + value += ".exe"; + } + } + + std::string GetPortableProductCode(Execution::Context& context) + { + const std::string& packageId = context.Get<Execution::Data::Manifest>().Id; + + std::string source; + if (context.Contains(Execution::Data::PackageVersion)) + { + source = context.Get<Execution::Data::PackageVersion>()->GetSource().GetIdentifier(); + } + else + { + source = s_DefaultSource; + } + + return MakeSuitablePathPart(packageId + "_" + source); + } + + std::filesystem::path GetPortableInstallRoot(Manifest::ScopeEnum scope, Utility::Architecture arch) + { + if (scope == Manifest::ScopeEnum::Machine) + { + if (arch == Utility::Architecture::X86) + { + return Runtime::GetPathTo(Runtime::PathName::PortablePackageMachineRootX86); + } + else + { + return Runtime::GetPathTo(Runtime::PathName::PortablePackageMachineRootX64); + } + } + else + { + return Runtime::GetPathTo(Runtime::PathName::PortablePackageUserRoot); + } + } + + std::filesystem::path GetPortableLinksLocation(Manifest::ScopeEnum scope) + { + if (scope == Manifest::ScopeEnum::Machine) + { + return Runtime::GetPathTo(Runtime::PathName::PortableLinksMachineLocation); + } + else + { + return Runtime::GetPathTo(Runtime::PathName::PortableLinksUserLocation); + } + } + + std::filesystem::path GetPortableTargetDirectory(Execution::Context& context) + { + Manifest::ScopeEnum scope = ConvertToScopeEnum(context.Args.GetArg(Execution::Args::Type::InstallScope)); + Utility::Architecture arch = context.Get<Execution::Data::Installer>()->Arch; + std::string_view locationArg = context.Args.GetArg(Execution::Args::Type::InstallLocation); + std::filesystem::path targetInstallDirectory; + + if (!locationArg.empty()) + { + targetInstallDirectory = std::filesystem::path{ ConvertToUTF16(locationArg) }; + } + else + { + const std::string& productCode = GetPortableProductCode(context); + targetInstallDirectory = GetPortableInstallRoot(scope, arch); + targetInstallDirectory /= ConvertToUTF16(productCode); + } + + return targetInstallDirectory; + } + + std::filesystem::path GetPortableTargetFullPath(Execution::Context& context) + { + const std::filesystem::path& installerPath = context.Get<Execution::Data::InstallerPath>(); + const std::filesystem::path& targetInstallDirectory = GetPortableTargetDirectory(context); + std::string_view renameArg = context.Args.GetArg(Execution::Args::Type::Rename); + + std::filesystem::path fileName; + if (!renameArg.empty()) + { + fileName = ConvertToUTF16(renameArg); + } + else + { + fileName = installerPath.filename(); + } + + AppendExeExtension(fileName); + return targetInstallDirectory / fileName; + } + + std::filesystem::path GetPortableSymlinkFullPath(Execution::Context& context) + { + const std::filesystem::path& installerPath = context.Get<Execution::Data::InstallerPath>(); + const std::vector<string_t>& commands = context.Get<Execution::Data::Installer>()->Commands; + Manifest::ScopeEnum scope = ConvertToScopeEnum(context.Args.GetArg(Execution::Args::Type::InstallScope)); + std::string_view renameArg = context.Args.GetArg(Execution::Args::Type::Rename); + + std::filesystem::path commandAlias; + if (!renameArg.empty()) + { + commandAlias = ConvertToUTF16(renameArg); + } + else + { + if (!commands.empty()) + { + commandAlias = ConvertToUTF16(commands[0]); + } + else + { + commandAlias = installerPath.filename(); + } + } + + AppendExeExtension(commandAlias); + return GetPortableLinksLocation(scope) / commandAlias; + } + + Manifest::AppsAndFeaturesEntry GetAppsAndFeaturesEntryForPortableInstall( + const std::vector<AppInstaller::Manifest::AppsAndFeaturesEntry>& appsAndFeaturesEntries, + const AppInstaller::Manifest::Manifest& manifest) + { + AppInstaller::Manifest::AppsAndFeaturesEntry appsAndFeaturesEntry; + if (!appsAndFeaturesEntries.empty()) + { + appsAndFeaturesEntry = appsAndFeaturesEntries[0]; + } + + if (appsAndFeaturesEntry.DisplayName.empty()) + { + appsAndFeaturesEntry.DisplayName = manifest.CurrentLocalization.Get<Manifest::Localization::PackageName>(); + } + if (appsAndFeaturesEntry.DisplayVersion.empty()) + { + appsAndFeaturesEntry.DisplayVersion = manifest.Version; + } + if (appsAndFeaturesEntry.Publisher.empty()) + { + appsAndFeaturesEntry.Publisher = manifest.CurrentLocalization.Get<Manifest::Localization::Publisher>(); + } + + return appsAndFeaturesEntry; + } + + bool AddToPathRegistry(Manifest::ScopeEnum scope) + { + 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<Value::Type::String>()); + + if (pathValue.find(portableLinksDir) == std::string::npos) + { + if (pathValue.back() != ';') + { + pathValue += ";"; + } + + pathValue += portableLinksDir + ";"; + AICLI_LOG(CLI, Info, << "Adding to Path environment variable: " << portableLinksDir); + key.SetValue(pathName, ConvertToUTF16(pathValue), REG_EXPAND_SZ); + return true; + } + else + { + AICLI_LOG(CLI, Verbose, << "Path already existed in environment variable. Skipping..."); + return false; + } + } + + bool RemoveFromPathRegistry(Manifest::ScopeEnum scope) + { + 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<Value::Type::String>()); + + 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<Execution::Data::Manifest>().Id; + + std::string sourceIdentifier; + if (context.Contains(Execution::Data::PackageVersion)) + { + sourceIdentifier = context.Get<Execution::Data::PackageVersion>()->GetSource().GetIdentifier(); + } + else + { + sourceIdentifier = s_DefaultSource; + } + + PortableARPEntry& uninstallEntry = context.Get<Execution::Data::PortableARPEntry>(); + + if (uninstallEntry.Exists()) + { + if (!uninstallEntry.IsSamePortablePackageEntry(packageIdentifier, sourceIdentifier)) + { + // TODO: Replace HashOverride with --Force when argument behavior gets updated. + if (!context.Args.Contains(Execution::Args::Type::HashOverride)) + { + AICLI_LOG(CLI, Error, << "Registry match failed, skipping write to uninstall registry"); + AICLI_TERMINATE_CONTEXT(APPINSTALLER_CLI_ERROR_PORTABLE_PACKAGE_ALREADY_EXISTS); + } + else + { + AICLI_LOG(CLI, Info, << "Overriding registry match check..."); + context.Reporter.Warn() << Resource::String::PortableRegistryCollisionOverridden << std::endl; + } + } + } + + 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))); + } + + void MovePortableExe(Execution::Context& context) + { + PortableARPEntry& uninstallEntry = context.Get<Execution::Data::PortableARPEntry>(); + const std::filesystem::path& installerPath = context.Get<Execution::Data::InstallerPath>(); + const std::filesystem::path& targetFullPath = GetPortableTargetFullPath(context); + const std::filesystem::path& targetDirectory = GetPortableTargetDirectory(context); + + bool isDirectoryCreated = false; + if (std::filesystem::create_directories(targetDirectory)) + { + AICLI_LOG(CLI, Info, << "Created target install directory: " << targetDirectory); + isDirectoryCreated = true; + } + + 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<Execution::Data::HashPair>().second)); + } + + void RemovePortableExe(Execution::Context& context) + { + PortableARPEntry& uninstallEntry = context.Get<Execution::Data::PortableARPEntry>(); + const auto& targetPath = uninstallEntry[PortableValueName::PortableTargetFullPath]; + + if (targetPath.has_value()) + { + const std::filesystem::path& targetPathValue = targetPath.value().GetValue<Value::Type::UTF16String>(); + const auto& expectedHash = uninstallEntry[PortableValueName::SHA256]; + std::string expectedHashValue = expectedHash.has_value() ? uninstallEntry[PortableValueName::SHA256].value().GetValue<Value::Type::String>() : ""; + + 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<Execution::Data::PortableARPEntry>(); + const auto& installDirectory = uninstallEntry[PortableValueName::InstallLocation]; + + if (installDirectory.has_value()) + { + const std::filesystem::path& installDirectoryValue = installDirectory.value().GetValue<Value::Type::UTF16String>(); + if (std::filesystem::exists(installDirectoryValue)) + { + const auto& isDirectoryCreated = uninstallEntry[PortableValueName::InstallDirectoryCreated]; + const auto& isDirectoryCreatedValue = isDirectoryCreated.has_value() ? isDirectoryCreated.value().GetValue<Value::Type::DWord>() : FALSE; + + bool isUpdate = WI_IsFlagSet(context.GetFlags(), Execution::ContextFlag::InstallerExecutionUseUpdate); + + if (context.Args.Contains(Execution::Args::Type::Purge) || + (!isUpdate && Settings::User().Get<Settings::Setting::UninstallPurgePortablePackage>() && !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<Execution::Data::PortableARPEntry>(); + 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)) + { + AICLI_LOG(CLI, Info, << "Unable to create symlink. '" << symlinkFullPath << "points to an existing directory."); + AICLI_TERMINATE_CONTEXT(APPINSTALLER_CLI_ERROR_PORTABLE_SYMLINK_PATH_IS_DIRECTORY); + } + else + { + context.Reporter.Warn() << Resource::String::OverwritingExistingFileAtMessage << symlinkFullPath.u8string() << std::endl; + std::filesystem::remove(symlinkFullPath); + } + + std::filesystem::create_symlink(targetFullPath, symlinkFullPath); + 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<Execution::Data::PortableARPEntry>(); + const auto& symlinkPath = uninstallEntry[PortableValueName::PortableSymlinkFullPath]; + if (symlinkPath.has_value()) + { + const std::filesystem::path& symlinkPathValue = symlinkPath.value().GetValue<Value::Type::UTF16String>(); + + 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<Execution::Data::PortableARPEntry>(); + uninstallEntry.Delete(); + AICLI_LOG(CLI, Info, << "PortableARPEntry deleted."); + } + + void CommitPortableMetadataToRegistry(Execution::Context& context) + { + PortableARPEntry& uninstallEntry = context.Get<Execution::Data::PortableARPEntry>(); + const AppInstaller::Manifest::Manifest& manifest = context.Get<Execution::Data::Manifest>(); + const Manifest::AppsAndFeaturesEntry& entry = GetAppsAndFeaturesEntryForPortableInstall(context.Get<Execution::Data::Installer>()->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<Manifest::Localization::PackageUrl>()); + uninstallEntry.SetValue(PortableValueName::HelpLink, manifest.CurrentLocalization.Get<Manifest::Localization::PublisherSupportUrl>()); + } + + void EnsureValidArgsForPortableInstall(Execution::Context& context) + { + std::string_view renameArg = context.Args.GetArg(Execution::Args::Type::Rename); + + try + { + if (MakeSuitablePathPart(renameArg) != renameArg) + { + context.Reporter.Error() << Resource::String::ReservedFilenameError << std::endl; + AICLI_TERMINATE_CONTEXT(APPINSTALLER_CLI_ERROR_INVALID_CL_ARGUMENTS); + } + } + catch (...) + { + context.Reporter.Error() << Resource::String::ReservedFilenameError << std::endl; + AICLI_TERMINATE_CONTEXT(APPINSTALLER_CLI_ERROR_INVALID_CL_ARGUMENTS); + } + } + + void EnsureVolumeSupportsReparsePoints(Execution::Context& context) + { + Manifest::ScopeEnum scope = ConvertToScopeEnum(context.Args.GetArg(Execution::Args::Type::InstallScope)); + const std::filesystem::path& symlinkDirectory = GetPortableLinksLocation(scope); + + if (!AppInstaller::Filesystem::SupportsReparsePoints(symlinkDirectory)) + { + context.Reporter.Error() << Resource::String::ReparsePointsNotSupportedError << std::endl; + 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<Execution::Data::Installer>()->Arch, + GetPortableProductCode(context)); + + context.Add<Execution::Data::PortableARPEntry>(std::move(uninstallEntry)); + + try + { + context.Reporter.Info() << Resource::String::InstallFlowStartingPackageInstall << std::endl; + + context << + InitializePortableARPEntry << + MovePortableExe << + CreatePortableSymlink << + CommitPortableMetadataToRegistry; + + context.Add<Execution::Data::OperationReturnCode>(context.GetTerminationHR()); + } + catch (...) + { + context.Add<Execution::Data::OperationReturnCode>(Workflow::HandleException(context, std::current_exception())); + } + + // Reset termination to allow for ReportInstallResult to process return code. + context.ResetTermination(); + + // Perform cleanup only if the install fails and is not an update. + const auto& installReturnCode = context.Get<Execution::Data::OperationReturnCode>(); + 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<Execution::Data::PortableARPEntry>(context.Get<Execution::Data::PortableARPEntry>()); + uninstallPortableContext << PortableUninstallImpl; + } + } + + void PortableUninstallImpl(Execution::Context& context) + { + try + { + context.Reporter.Info() << Resource::String::UninstallFlowStartingPackageUninstall << std::endl; + + context << + RemovePortableExe << + RemoveInstallDirectory << + RemovePortableSymlink << + RemovePortableARPEntry; + + context.Add<Execution::Data::OperationReturnCode>(context.GetTerminationHR()); + } + catch (...) + { + context.Add<Execution::Data::OperationReturnCode>(Workflow::HandleException(context, std::current_exception())); + } + + // Reset termination to allow for ReportUninstallResult to process return code. + context.ResetTermination(); + } + + void EnsureSupportForPortableInstall(Execution::Context& context) + { + auto installerType = context.Get<Execution::Data::Installer>().value().InstallerType; + + if (installerType == InstallerTypeEnum::Portable) + { + context << + Workflow::EnsureFeatureEnabled(Settings::ExperimentalFeature::Feature::PortableInstall) << + EnsureValidArgsForPortableInstall << + EnsureVolumeSupportsReparsePoints; + } + } +}+ \ No newline at end of file diff --git a/src/AppInstallerCLICore/Workflows/PortableFlow.h b/src/AppInstallerCLICore/Workflows/PortableFlow.h @@ -0,0 +1,21 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. +#pragma once +#include "ExecutionContext.h" + +namespace AppInstaller::CLI::Workflow +{ + // Installs the portable package. + // Required Args: None + // Inputs: Manifest, Scope, Rename, Location + // 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/PortableInstallFlow.cpp b/src/AppInstallerCLICore/Workflows/PortableInstallFlow.cpp @@ -1,381 +0,0 @@ -// Copyright (c) Microsoft Corporation. -// Licensed under the MIT License. -#include "pch.h" -#include "PortableInstallFlow.h" -#include "winget/Filesystem.h" -#include "winget/PortableARPEntry.h" -#include "AppInstallerStrings.h" - -using namespace AppInstaller::Manifest; -using namespace AppInstaller::Utility; -using namespace AppInstaller::Registry; -using namespace AppInstaller::Registry::Portable; -using namespace std::filesystem; - -namespace AppInstaller::CLI::Workflow -{ - namespace - { - 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; - - void AppendExeExtension(std::filesystem::path& value) - { - if (value.extension() != ".exe") - { - value += ".exe"; - } - } - - std::string GetPortableProductCode(Execution::Context& context) - { - const std::string& packageId = context.Get<Execution::Data::Manifest>().Id; - - std::string source; - if (context.Contains(Execution::Data::Source)) - { - source = context.Get<Execution::Data::Source>().GetIdentifier(); - } - else - { - source = s_LocalSource; - } - - return MakeSuitablePathPart(packageId + "_" + source); - } - - std::filesystem::path GetPortableInstallRoot(Manifest::ScopeEnum scope, Utility::Architecture arch) - { - if (scope == Manifest::ScopeEnum::Machine) - { - if (arch == Utility::Architecture::X86) - { - return Runtime::GetPathTo(Runtime::PathName::PortablePackageMachineRootX86); - } - else - { - return Runtime::GetPathTo(Runtime::PathName::PortablePackageMachineRootX64); - } - } - else - { - return Runtime::GetPathTo(Runtime::PathName::PortablePackageUserRoot); - } - } - - std::filesystem::path GetPortableLinksLocation(Manifest::ScopeEnum scope) - { - if (scope == Manifest::ScopeEnum::Machine) - { - return Runtime::GetPathTo(Runtime::PathName::PortableLinksMachineLocation); - } - else - { - return Runtime::GetPathTo(Runtime::PathName::PortableLinksUserLocation); - } - } - - std::filesystem::path GetPortableTargetDirectory(Execution::Context& context) - { - Manifest::ScopeEnum scope = ConvertToScopeEnum(context.Args.GetArg(Execution::Args::Type::InstallScope)); - Utility::Architecture arch = context.Get<Execution::Data::Installer>()->Arch; - std::string_view locationArg = context.Args.GetArg(Execution::Args::Type::InstallLocation); - std::filesystem::path targetInstallDirectory; - - if (!locationArg.empty()) - { - targetInstallDirectory = std::filesystem::path{ ConvertToUTF16(locationArg) }; - } - else - { - const std::string& productCode = GetPortableProductCode(context); - targetInstallDirectory = GetPortableInstallRoot(scope, arch); - targetInstallDirectory /= ConvertToUTF16(productCode); - } - - return targetInstallDirectory; - } - - std::filesystem::path GetPortableTargetFullPath(Execution::Context& context) - { - const std::filesystem::path& installerPath = context.Get<Execution::Data::InstallerPath>(); - const std::filesystem::path& targetInstallDirectory = GetPortableTargetDirectory(context); - std::string_view renameArg = context.Args.GetArg(Execution::Args::Type::Rename); - - std::filesystem::path fileName; - if (!renameArg.empty()) - { - fileName = ConvertToUTF16(renameArg); - } - else - { - fileName = installerPath.filename(); - } - - AppendExeExtension(fileName); - return targetInstallDirectory / fileName; - } - - std::filesystem::path GetPortableSymlinkFullPath(Execution::Context& context) - { - const std::filesystem::path& installerPath = context.Get<Execution::Data::InstallerPath>(); - const std::vector<string_t>& commands = context.Get<Execution::Data::Installer>()->Commands; - Manifest::ScopeEnum scope = ConvertToScopeEnum(context.Args.GetArg(Execution::Args::Type::InstallScope)); - std::string_view renameArg = context.Args.GetArg(Execution::Args::Type::Rename); - - std::filesystem::path commandAlias; - if (!renameArg.empty()) - { - commandAlias = ConvertToUTF16(renameArg); - } - else - { - if (!commands.empty()) - { - commandAlias = ConvertToUTF16(commands[0]); - } - else - { - commandAlias = installerPath.filename(); - } - } - - AppendExeExtension(commandAlias); - return GetPortableLinksLocation(scope) / commandAlias; - } - - Manifest::AppsAndFeaturesEntry GetAppsAndFeaturesEntryForPortableInstall(const std::vector<AppInstaller::Manifest::AppsAndFeaturesEntry>& appsAndFeaturesEntries, const AppInstaller::Manifest::Manifest& manifest) - { - AppInstaller::Manifest::AppsAndFeaturesEntry appsAndFeaturesEntry; - if (!appsAndFeaturesEntries.empty()) - { - appsAndFeaturesEntry = appsAndFeaturesEntries[0]; - } - - if (appsAndFeaturesEntry.DisplayName.empty()) - { - appsAndFeaturesEntry.DisplayName = manifest.DefaultLocalization.Get<Manifest::Localization::PackageName>(); - } - if (appsAndFeaturesEntry.DisplayVersion.empty()) - { - appsAndFeaturesEntry.DisplayVersion = manifest.Version; - } - if (appsAndFeaturesEntry.Publisher.empty()) - { - appsAndFeaturesEntry.Publisher = manifest.DefaultLocalization.Get<Manifest::Localization::Publisher>(); - } - - return appsAndFeaturesEntry; - } - - bool AddToPathRegistry(Execution::Context& context) - { - Manifest::ScopeEnum scope = ConvertToScopeEnum(context.Args.GetArg(Execution::Args::Type::InstallScope)); - 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<Value::Type::String>()); - - if (pathValue.find(portableLinksDir) == std::string::npos) - { - if (pathValue.back() != ';') - { - pathValue += ";"; - } - - pathValue += portableLinksDir + ";"; - AICLI_LOG(CLI, Info, << "Adding to Path environment variable: " << portableLinksDir); - key.SetValue(pathName, ConvertToUTF16(pathValue), REG_EXPAND_SZ); - return true; - } - else - { - AICLI_LOG(CLI, Verbose, << "Path already existed in environment variable. Skipping..."); - return false; - } - } - - void WritePortableEntryToUninstallRegistry(Execution::Context& context) - { - const AppInstaller::Manifest::Manifest& manifest = context.Get<Execution::Data::Manifest>(); - const Manifest::AppsAndFeaturesEntry& entry = GetAppsAndFeaturesEntryForPortableInstall(context.Get<Execution::Data::Installer>()->AppsAndFeaturesEntries, manifest); - const std::string& packageIdentifier = manifest.Id; - - std::string sourceIdentifier; - if (context.Contains(Execution::Data::Source)) - { - sourceIdentifier = context.Get<Execution::Data::Source>().GetIdentifier(); - } - else - { - sourceIdentifier = s_LocalSource; - } - - const std::wstring& productCode = ConvertToUTF16(GetPortableProductCode(context)); - - Portable::PortableARPEntry uninstallEntry = Portable::PortableARPEntry( - ConvertToScopeEnum(context.Args.GetArg(Execution::Args::Type::InstallScope)), - context.Get<Execution::Data::Installer>()->Arch, - productCode); - - if(uninstallEntry.Exists()) - { - if (!uninstallEntry.IsSamePortablePackageEntry(packageIdentifier, sourceIdentifier)) - { - // TODO: Replace HashOverride with --Force when argument behavior gets updated. - if (!context.Args.Contains(Execution::Args::Type::HashOverride)) - { - AICLI_LOG(CLI, Error, << "Registry match failed, skipping write to uninstall registry"); - AICLI_TERMINATE_CONTEXT(APPINSTALLER_CLI_ERROR_PORTABLE_PACKAGE_ALREADY_EXISTS); - } - else - { - AICLI_LOG(CLI, Info, << "Overriding registry match check..."); - context.Reporter.Warn() << Resource::String::PortableRegistryCollisionOverridden << std::endl; - } - } - } - - 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<Manifest::Localization::PackageUrl>()); - uninstallEntry.SetValue(PortableValueName::HelpLink, manifest.DefaultLocalization.Get<Manifest::Localization::PublisherSupportUrl>()); - uninstallEntry.SetValue(PortableValueName::UninstallString, L"winget uninstall --product-code " + productCode); - 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<Execution::Data::HashPair>().second)); - uninstallEntry.SetValue(PortableValueName::InstallLocation, GetPortableTargetDirectory(context).wstring()); - AICLI_LOG(CLI, Info, << "Writing to Uninstall registry complete."); - } - - void MovePortableExeAndCreateSymlink(Execution::Context& context) - { - const std::filesystem::path& installerPath = context.Get<Execution::Data::InstallerPath>(); - const std::filesystem::path& targetFullPath = GetPortableTargetFullPath(context); - const std::filesystem::path& symlinkFullPath = GetPortableSymlinkFullPath(context); - const std::filesystem::path& targetDirectory = GetPortableTargetDirectory(context); - - bool isDirectoryCreated = false; - if (std::filesystem::create_directories(targetDirectory)) - { - AICLI_LOG(CLI, Info, << "Created target install directory: " << targetDirectory); - isDirectoryCreated = true; - } - - Portable::PortableARPEntry uninstallEntry = Portable::PortableARPEntry( - ConvertToScopeEnum(context.Args.GetArg(Execution::Args::Type::InstallScope)), - context.Get<Execution::Data::Installer>()->Arch, - ConvertToUTF16(GetPortableProductCode(context))); - uninstallEntry.SetValue(PortableValueName::InstallDirectoryCreated, isDirectoryCreated); - - Filesystem::RenameFile(installerPath, targetFullPath); - AICLI_LOG(CLI, Info, << "Portable exe moved to: " << targetFullPath); - - std::filesystem::file_status status = std::filesystem::status(symlinkFullPath); - if (std::filesystem::is_directory(status)) - { - AICLI_LOG(CLI, Info, << "Unable to create symlink. '" << symlinkFullPath << "points to an existing directory."); - AICLI_TERMINATE_CONTEXT(APPINSTALLER_CLI_ERROR_PORTABLE_SYMLINK_PATH_IS_DIRECTORY); - } - else - { - context.Reporter.Warn() << Resource::String::OverwritingExistingFileAtMessage << symlinkFullPath.u8string() << std::endl; - std::filesystem::remove(symlinkFullPath); - } - - std::filesystem::create_symlink(targetFullPath, symlinkFullPath); - AICLI_LOG(CLI, Info, << "Symlink created at: " << symlinkFullPath); - - if (AddToPathRegistry(context)) - { - context.Reporter.Warn() << Resource::String::ModifiedPathRequiresShellRestart << std::endl; - } - } - - void EnsureValidArgsForPortableInstall(Execution::Context& context) - { - std::string_view renameArg = context.Args.GetArg(Execution::Args::Type::Rename); - - try - { - if (MakeSuitablePathPart(renameArg) != renameArg) - { - context.Reporter.Error() << Resource::String::ReservedFilenameError << std::endl; - AICLI_TERMINATE_CONTEXT(APPINSTALLER_CLI_ERROR_INVALID_CL_ARGUMENTS); - } - } - catch (...) - { - context.Reporter.Error() << Resource::String::ReservedFilenameError << std::endl; - AICLI_TERMINATE_CONTEXT(APPINSTALLER_CLI_ERROR_INVALID_CL_ARGUMENTS); - } - } - - void EnsureVolumeSupportsReparsePoints(Execution::Context& context) - { - Manifest::ScopeEnum scope = ConvertToScopeEnum(context.Args.GetArg(Execution::Args::Type::InstallScope)); - const std::filesystem::path& symlinkDirectory = GetPortableLinksLocation(scope); - - if (!AppInstaller::Filesystem::SupportsReparsePoints(symlinkDirectory)) - { - context.Reporter.Error() << Resource::String::ReparsePointsNotSupportedError << std::endl; - AICLI_TERMINATE_CONTEXT(APPINSTALLER_CLI_ERROR_PORTABLE_REPARSE_POINT_NOT_SUPPORTED); - } - } - } - - void PortableInstallImpl(Execution::Context& context) - { - try - { - context.Reporter.Info() << Resource::String::InstallFlowStartingPackageInstall << std::endl; - - context << - WritePortableEntryToUninstallRegistry << - MovePortableExeAndCreateSymlink; - - context.Add<Execution::Data::OperationReturnCode>(context.GetTerminationHR()); - } - catch (...) - { - context.Add<Execution::Data::OperationReturnCode>(Workflow::HandleException(context, std::current_exception())); - } - - // Reset termination to allow for ReportInstallResult to process return code. - context.ResetTermination(); - - // TODO: create subcontext for uninstall - } - - void EnsureSupportForPortableInstall(Execution::Context& context) - { - auto installerType = context.Get<Execution::Data::Installer>().value().InstallerType; - - if (installerType == InstallerTypeEnum::Portable) - { - context << - Workflow::EnsureFeatureEnabled(Settings::ExperimentalFeature::Feature::PortableInstall) << - EnsureValidArgsForPortableInstall << - EnsureVolumeSupportsReparsePoints; - } - } -}- \ No newline at end of file diff --git a/src/AppInstallerCLICore/Workflows/PortableInstallFlow.h b/src/AppInstallerCLICore/Workflows/PortableInstallFlow.h @@ -1,15 +0,0 @@ -// Copyright (c) Microsoft Corporation. -// Licensed under the MIT License. -#pragma once -#include "ExecutionContext.h" - -namespace AppInstaller::CLI::Workflow -{ - // Installs the portable package. - // Required Args: None - // Inputs: Manifest, Scope, Rename, Location - // Outputs: None - void PortableInstallImpl(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 @@ -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<Execution::Data::InstalledPackageVersion>(); - Logging::Telemetry().LogUninstallerFailure( - installedPackageVersion->GetProperty(PackageVersionProperty::Id), - installedPackageVersion->GetProperty(PackageVersionProperty::Version), - "UninstallString", - uninstallResult.value()); - - context.Add<Execution::Data::OperationReturnCode>(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<Execution::Data::OperationReturnCode>(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<Execution::Data::InstalledPackageVersion>(); - Logging::Telemetry().LogUninstallerFailure( - installedPackageVersion->GetProperty(PackageVersionProperty::Id), - installedPackageVersion->GetProperty(PackageVersionProperty::Version), - "MsiExec", - uninstallResult.value()); - context.Add<Execution::Data::OperationReturnCode>(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 @@ -6,6 +6,8 @@ #include "DependenciesFlow.h" #include "ShellExecuteInstallerHandler.h" #include "AppInstallerMsixInfo.h" +#include "PortableFlow.h" +#include "winget/PortableARPEntry.h" #include <AppInstallerDeployment.h> @@ -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<Execution::Data::PackageFamilyNames>(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<Execution::Data::InstalledPackageVersion>()->GetMetadata()[Repository::PackageVersionMetadata::InstalledScope]; + const std::string installedArch = context.Get<Execution::Data::InstalledPackageVersion>()->GetMetadata()[Repository::PackageVersionMetadata::InstalledArchitecture]; + Portable::PortableARPEntry uninstallEntry = Portable::PortableARPEntry( + ConvertToScopeEnum(installedScope), + Utility::ConvertToArchitectureEnum(installedArch), + productCodes[0]); + context.Add<Execution::Data::PortableARPEntry>(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<Execution::Data::OperationReturnCode>(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<Execution::Data::OperationReturnCode>(); + if (uninstallResult != 0) + { + const auto installedPackageVersion = context.Get<Execution::Data::InstalledPackageVersion>(); + 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<Execution::Data::LogPath>())) + { + context.Reporter.Info() << Resource::String::InstallerLogAvailable << ' ' << context.Get<Execution::Data::LogPath>().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 @@ -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 @@ -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<Manifest::Localization::PackageName>(); + if (!localizedPackageName.empty()) + { + searchRequest.Filters.emplace_back(PackageMatchField::Name, MatchType::CaseInsensitive, localizedPackageName); + } + } + searchRequest.Filters.emplace_back(PackageMatchFilter(PackageMatchField::Name, MatchType::CaseInsensitive, manifest.DefaultLocalization.Get<Manifest::Localization::PackageName>())); context.Add<Execution::Data::SearchResult>(source.Search(searchRequest)); diff --git a/src/AppInstallerCLIE2ETests/AppInstallerCLIE2ETests.csproj b/src/AppInstallerCLIE2ETests/AppInstallerCLIE2ETests.csproj @@ -29,6 +29,8 @@ <None Remove="TestData\ImportFiles\ImportFile-Bad-UnknownSource.json" /> <None Remove="TestData\ImportFiles\ImportFile-Good.1.0.json" /> <None Remove="TestData\ImportFiles\ImportFile-Good.2.0.json" /> + <None Remove="TestData\Manifests\TestPortableInstaller.2.0.0.0.yaml" /> + <None Remove="TestData\Manifests\TestPortableInstaller.3.0.0.0_UninstallPrevious.yaml" /> </ItemGroup> <ItemGroup> diff --git 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 @@ -27,6 +27,7 @@ namespace AppInstallerCLIE2ETests 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 @@ -3,7 +3,6 @@ namespace AppInstallerCLIE2ETests { - using Microsoft.Win32; using NUnit.Framework; using System.IO; @@ -12,7 +11,6 @@ namespace AppInstallerCLIE2ETests 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 @@ namespace AppInstallerCLIE2ETests 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 @@ namespace AppInstallerCLIE2ETests 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 @@ namespace AppInstallerCLIE2ETests 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 @@ namespace AppInstallerCLIE2ETests 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 @@ namespace AppInstallerCLIE2ETests 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 @@ -286,38 +286,38 @@ namespace AppInstallerCLIE2ETests 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}"); } /// <summary> diff --git a/src/AppInstallerCLIE2ETests/TestData/Manifests/TestPortableInstaller.2.0.0.0.yaml 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: <EXEHASH> +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 @@ -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: <EXEHASH> + 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 @@ -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: <EXEHASH> + 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 @@ -61,6 +61,40 @@ namespace AppInstallerCLIE2ETests } [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() { // Uninstalls a package found with ARP not matching any known manifest. diff --git a/src/AppInstallerCLIE2ETests/UpdateCommand.cs b/src/AppInstallerCLIE2ETests/UpdateCommand.cs @@ -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 @@ -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 @@ -723,7 +723,7 @@ They can be configured through the settings file 'winget settings'.</value> <value>File does not exist:</value> </data> <data name="BothManifestAndSearchQueryProvided" xml:space="preserve"> - <value>Both local manifest and search query args are provided</value> + <value>Both local manifest and search query arguments are provided</value> </data> <data name="Logs" xml:space="preserve"> <value>Logs</value> @@ -1314,4 +1314,27 @@ Please specify one of them using the `--source` option to proceed.</value> <data name="NoPackageSelectionArgumentProvided" xml:space="preserve"> <value>No package selection argument was provided; see the help for details about finding a package.</value> </data> + <data name="PortableInstallFailed" xml:space="preserve"> + <value>Portable install failed; Cleaning up...</value> + </data> + <data name="BothPurgeAndPreserveFlagsProvided" xml:space="preserve"> + <value>Both `purge` and `preserve` arguments are provided</value> + </data> + <data name="PortableHashMismatchOverridden" xml:space="preserve"> + <value>Portable exe has been modified; proceeding due to --force</value> + <comment>{Locked="--force"}</comment> + </data> + <data name="PortableHashMismatchOverrideRequired" xml:space="preserve"> + <value>Unable to remove Portable exe as it has been modified; to override this check use --force</value> + <comment>{Locked="--force"}</comment> + </data> + <data name="FilesRemainInInstallDirectory" xml:space="preserve"> + <value>Files remain in install directory:</value> + </data> + <data name="PurgeInstallDirectory" xml:space="preserve"> + <value>Purging install directory...</value> + </data> + <data name="UnableToPurgeInstallDirectory" xml:space="preserve"> + <value>Cannot purge install directory, as it was not created by WinGet</value> + </data> </root> \ No newline at end of file diff --git a/src/AppInstallerCLITests/AppInstallerCLITests.vcxproj b/src/AppInstallerCLITests/AppInstallerCLITests.vcxproj @@ -269,10 +269,10 @@ </CopyFileToFolders> <CopyFileToFolders Include="TestData\InstallFlowTest_Portable.yaml"> <DeploymentContent>true</DeploymentContent> - </CopyFileToFolders> + </CopyFileToFolders> <CopyFileToFolders Include="TestData\InstallFlowTest_Portable_WithCommand.yaml"> <DeploymentContent>true</DeploymentContent> - </CopyFileToFolders> + </CopyFileToFolders> <CopyFileToFolders Include="TestData\ImportFile-Bad-Invalid.json"> <DeploymentContent>true</DeploymentContent> </CopyFileToFolders> @@ -552,6 +552,9 @@ <CopyFileToFolders Include="TestData\UpdateFlowTest_Msix_LicenseAgreement.yaml"> <DeploymentContent>true</DeploymentContent> </CopyFileToFolders> + <CopyFileToFolders Include="TestData\UpdateFlowTest_Portable.yaml"> + <DeploymentContent>true</DeploymentContent> + </CopyFileToFolders> <CopyFileToFolders Include="TestData\InputNames.txt"> <DeploymentContent>true</DeploymentContent> </CopyFileToFolders> diff --git a/src/AppInstallerCLITests/AppInstallerCLITests.vcxproj.filters b/src/AppInstallerCLITests/AppInstallerCLITests.vcxproj.filters @@ -440,10 +440,10 @@ </CopyFileToFolders> <CopyFileToFolders Include="TestData\InstallFlowTest_Portable.yaml"> <Filter>TestData</Filter> - </CopyFileToFolders> - <CopyFileToFolders Include="TestData\InstallFlowTest_Portable_WithCommand.yaml"> + </CopyFileToFolders> + <CopyFileToFolders Include="TestData\InstallFlowTest_Portable_WithCommand.yaml"> <Filter>TestData</Filter> - </CopyFileToFolders> + </CopyFileToFolders> <CopyFileToFolders Include="TestData\InstallerArgTest_Msi_WithSwitches.yaml"> <Filter>TestData</Filter> </CopyFileToFolders> @@ -477,6 +477,9 @@ <CopyFileToFolders Include="TestData\UpdateFlowTest_Msix_LicenseAgreement.yaml"> <Filter>TestData</Filter> </CopyFileToFolders> + <CopyFileToFolders Include="TestData\UpdateFlowTest_Portable.yaml"> + <Filter>TestData</Filter> + </CopyFileToFolders> <CopyFileToFolders Include="TestData\ImportFile-Good.json"> <Filter>TestData</Filter> </CopyFileToFolders> diff --git 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 @@ -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 @@ -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 @@ -14,7 +14,7 @@ #include <Workflows/DownloadFlow.h> #include <Workflows/InstallFlow.h> #include <Workflows/MsiInstallFlow.h> -#include <Workflows/PortableInstallFlow.h> +#include <Workflows/PortableFlow.h> #include <Workflows/UninstallFlow.h> #include <Workflows/UpdateFlow.h> #include <Workflows/DependenciesFlow.h> @@ -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<Manifest>{ 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")); @@ -528,6 +543,17 @@ void OverrideForShellExecute(TestContext& context, std::vector<Dependency>& 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) { context.Add<Data::HashPair>({ {}, {} }); @@ -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<Execution::Data::OperationReturnCode>(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<Execution::Data::UninstallString>(); file.close(); + + context.Add<Execution::Data::OperationReturnCode>(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 @@ -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 @@ -133,7 +133,7 @@ namespace AppInstaller::Settings return std::nullopt; } - std::vector<typename Mapping::item_t> 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 @@ -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<Value> 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 @@ -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 @@ -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<Value> 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 @@ -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 @@ -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 @@ -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<Registry::Value::Type::DWord>()); } + 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<Registry::Value::Type::String>(); + } + + 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 @@ -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 @@ -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.