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