winget-cli

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

commit d54c5a3d438b25289e574d305b9b2e8881b27b59
parent 01ad798c7f0c43db7b41addca6eef560fb1b3ba0
Author: yao-msft <50888816+yao-msft@users.noreply.github.com>
Date:   Sun,  9 Oct 2022 12:55:10 -0700

Make Install flow aware of package installed status (#2539)


Diffstat:
Msrc/AppInstallerCLICore/Argument.cpp | 4+++-
Msrc/AppInstallerCLICore/Commands/InstallCommand.cpp | 32+++++++++++++++++++++++++-------
Msrc/AppInstallerCLICore/Commands/UninstallCommand.cpp | 2+-
Msrc/AppInstallerCLICore/Commands/UpgradeCommand.cpp | 26++------------------------
Msrc/AppInstallerCLICore/ExecutionArgs.h | 1+
Msrc/AppInstallerCLICore/ExecutionReporter.cpp | 1+
Msrc/AppInstallerCLICore/ExecutionReporter.h | 1+
Msrc/AppInstallerCLICore/PortableInstaller.cpp | 3+--
Msrc/AppInstallerCLICore/Resources.h | 4+++-
Msrc/AppInstallerCLICore/Workflows/ArchiveFlow.cpp | 3+--
Msrc/AppInstallerCLICore/Workflows/DependenciesFlow.cpp | 4++--
Msrc/AppInstallerCLICore/Workflows/ImportExportFlow.cpp | 31++++++++++++++++++-------------
Msrc/AppInstallerCLICore/Workflows/InstallFlow.cpp | 4++--
Msrc/AppInstallerCLICore/Workflows/PortableFlow.cpp | 436+++++++++++++++++++++++++++++++++++++++----------------------------------------
Msrc/AppInstallerCLICore/Workflows/ShellExecuteInstallerHandler.cpp | 3+--
Msrc/AppInstallerCLICore/Workflows/UpdateFlow.cpp | 86++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++-------------
Msrc/AppInstallerCLICore/Workflows/UpdateFlow.h | 42++++++++++++++++++++++++++++++++++++------
Msrc/AppInstallerCLICore/Workflows/WorkflowBase.cpp | 8++++----
Msrc/AppInstallerCLICore/Workflows/WorkflowBase.h | 7++++++-
Msrc/AppInstallerCLIE2ETests/GroupPolicy.cs | 2+-
Msrc/AppInstallerCLIE2ETests/InstallCommand.cs | 64+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++-
Msrc/AppInstallerCLIE2ETests/Interop/UpgradeInterop.cs | 2+-
Msrc/AppInstallerCLIE2ETests/ListCommand.cs | 7+++++++
Msrc/AppInstallerCLIE2ETests/TestCommon.cs | 21++++++++++++++++-----
Msrc/AppInstallerCLIPackage/Shared/Strings/en-us/winget.resw | 38+++++++++++++++++++++++---------------
Msrc/AppInstallerCLITests/WorkFlow.cpp | 94+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++----------------
Msrc/AppInstallerTestExeInstaller/main.cpp | 4++++
Msrc/Microsoft.Management.Deployment/InstallOptions.cpp | 8++++++++
Msrc/Microsoft.Management.Deployment/InstallOptions.h | 3+++
Msrc/Microsoft.Management.Deployment/PackageManager.cpp | 8++++++++
Msrc/Microsoft.Management.Deployment/PackageManager.idl | 12++++++++++++
Msrc/Microsoft.Management.Deployment/UninstallOptions.cpp | 8++++++++
Msrc/Microsoft.Management.Deployment/UninstallOptions.h | 3+++
Msrc/PowerShell/Microsoft.WinGet.Client/Commands/UninstallPackageCommand.cs | 7+++++++
Msrc/PowerShell/Microsoft.WinGet.Client/Common/BaseInstallCommand.cs | 9++++++++-
35 files changed, 644 insertions(+), 344 deletions(-)

diff --git a/src/AppInstallerCLICore/Argument.cpp b/src/AppInstallerCLICore/Argument.cpp @@ -58,7 +58,7 @@ namespace AppInstaller::CLI case Args::Type::InstallLocation: return Argument{ "location"_liv, 'l', Args::Type::InstallLocation, Resource::String::LocationArgumentDescription, ArgumentType::Standard }; case Args::Type::HashOverride: - return Argument{ "force"_liv, NoAlias, Args::Type::HashOverride, Resource::String::InstallForceArgumentDescription, ArgumentType::Flag, Settings::TogglePolicy::Policy::HashOverride }; + return Argument{ "ignore-security-hash"_liv, NoAlias, Args::Type::HashOverride, Resource::String::HashOverrideArgumentDescription, ArgumentType::Flag, Settings::TogglePolicy::Policy::HashOverride }; case Args::Type::AcceptPackageAgreements: return Argument{ "accept-package-agreements"_liv, NoAlias, Args::Type::AcceptPackageAgreements, Resource::String::AcceptPackageAgreementsArgumentDescription, ArgumentType::Flag }; case Args::Type::HashFile: @@ -103,6 +103,8 @@ namespace AppInstaller::CLI return Argument{ "product-code"_liv, NoAlias, Args::Type::ProductCode, Resource::String::ProductCodeArgumentDescription, ArgumentType::Standard, false }; case Args::Type::OpenLogs: return Argument{ "open-logs"_liv, NoAlias, "logs"_liv, Args::Type::OpenLogs, Resource::String::OpenLogsArgumentDescription, ArgumentType::Flag, ExperimentalFeature::Feature::OpenLogsArgument}; + case Args::Type::Force: + return Argument{ "force"_liv, NoAlias, Args::Type::Force, Resource::String::ForceArgumentDescription, ArgumentType::Flag, false }; default: THROW_HR(E_UNEXPECTED); } diff --git a/src/AppInstallerCLICore/Commands/InstallCommand.cpp b/src/AppInstallerCLICore/Commands/InstallCommand.cpp @@ -4,6 +4,7 @@ #include "InstallCommand.h" #include "Workflows/CompletionFlow.h" #include "Workflows/InstallFlow.h" +#include "Workflows/UpdateFlow.h" #include "Workflows/WorkflowBase.h" #include "Resources.h" @@ -45,6 +46,7 @@ namespace AppInstaller::CLI Argument::ForType(Args::Type::CustomHeader), Argument::ForType(Args::Type::AcceptSourceAgreements), Argument::ForType(Args::Type::Rename), + Argument::ForType(Args::Type::Force), }; } @@ -122,12 +124,28 @@ namespace AppInstaller::CLI { context.SetFlags(ContextFlag::ShowSearchResultsOnPartialFailure); - context << - Workflow::ReportExecutionStage(ExecutionStage::Discovery) << - Workflow::GetManifest << - Workflow::SelectInstaller << - Workflow::EnsureApplicableInstaller << - Workflow::CheckForUnsupportedArgs << - Workflow::InstallSinglePackage; + if (context.Args.Contains(Execution::Args::Type::Manifest)) + { + context << + Workflow::ReportExecutionStage(ExecutionStage::Discovery) << + Workflow::GetManifestFromArg << + Workflow::SelectInstaller << + Workflow::EnsureApplicableInstaller << + Workflow::InstallSinglePackage; + } + else + { + context << + Workflow::ReportExecutionStage(ExecutionStage::Discovery) << + Workflow::OpenSource(); + + if (!context.Args.Contains(Execution::Args::Type::Force)) + { + context << + Workflow::OpenCompositeSource(Repository::PredefinedSource::Installed, false, Repository::CompositeSearchBehavior::AvailablePackages); + } + + context << Workflow::InstallOrUpgradeSinglePackage(false); + } } } diff --git a/src/AppInstallerCLICore/Commands/UninstallCommand.cpp b/src/AppInstallerCLICore/Commands/UninstallCommand.cpp @@ -28,7 +28,7 @@ namespace AppInstaller::CLI 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::Force), Argument::ForType(Args::Type::Purge), Argument::ForType(Args::Type::Preserve), Argument::ForType(Args::Type::Log), diff --git a/src/AppInstallerCLICore/Commands/UpgradeCommand.cpp b/src/AppInstallerCLICore/Commands/UpgradeCommand.cpp @@ -108,6 +108,7 @@ namespace AppInstaller::CLI Argument::ForType(Execution::Args::Type::CustomHeader), Argument{ "all"_liv, 'r', "recurse"_liv, Args::Type::All, Resource::String::UpdateAllArgumentDescription, ArgumentType::Flag }, Argument{ "include-unknown"_liv, 'u', "unknown"_liv, Args::Type::IncludeUnknown, Resource::String::IncludeUnknownArgumentDescription, ArgumentType::Flag }, + Argument::ForType(Args::Type::Force), }; } @@ -246,30 +247,7 @@ namespace AppInstaller::CLI else { // The remaining case: search for single installed package to update - context << - SearchSourceForSingle << - HandleSearchResultFailures << - EnsureOneMatchFromSearchResult(true) << - GetInstalledPackageVersion; - - if (context.Args.Contains(Execution::Args::Type::Version)) - { - // If version specified, use the version and verify applicability - context << - GetManifestFromPackage << - EnsureUpdateVersionApplicable << - SelectInstaller << - EnsureApplicableInstaller; - } - else - { - // iterate through available versions to find latest applicable update - // This step also populates Manifest and Installer in context data - context << SelectLatestApplicableUpdate(true); - } - - context << - InstallSinglePackage; + context << InstallOrUpgradeSinglePackage(true); } } } diff --git a/src/AppInstallerCLICore/ExecutionArgs.h b/src/AppInstallerCLICore/ExecutionArgs.h @@ -96,6 +96,7 @@ namespace AppInstaller::CLI::Execution DisableInteractivity, // Disable interactive prompts Wait, // Prompts the user to press any key before exiting OpenLogs, // Opens the default logs directory after executing the command + Force, // Forces the execution of the workflow with non security related issues DependencySource, // Index source to be queried against for finding dependencies CustomHeader, // Optional Rest source header diff --git a/src/AppInstallerCLICore/ExecutionReporter.cpp b/src/AppInstallerCLICore/ExecutionReporter.cpp @@ -17,6 +17,7 @@ namespace AppInstaller::CLI::Execution const Sequence& IdEmphasis = TextFormat::Foreground::BrightCyan; const Sequence& UrlEmphasis = TextFormat::Foreground::BrightBlue; const Sequence& PromptEmphasis = TextFormat::Foreground::Bright; + const Sequence& ConvertToUpgradeFlowEmphasis = TextFormat::Foreground::BrightYellow; Reporter::Reporter(std::ostream& outStream, std::istream& inStream) : Reporter(std::make_shared<BaseStream>(outStream, true, ConsoleModeRestore::Instance().IsVTEnabled()), inStream) diff --git a/src/AppInstallerCLICore/ExecutionReporter.h b/src/AppInstallerCLICore/ExecutionReporter.h @@ -171,4 +171,5 @@ namespace AppInstaller::CLI::Execution extern const VirtualTerminal::Sequence& IdEmphasis; extern const VirtualTerminal::Sequence& UrlEmphasis; extern const VirtualTerminal::Sequence& PromptEmphasis; + extern const VirtualTerminal::Sequence& ConvertToUpgradeFlowEmphasis; } diff --git a/src/AppInstallerCLICore/PortableInstaller.cpp b/src/AppInstallerCLICore/PortableInstaller.cpp @@ -60,7 +60,6 @@ namespace AppInstaller::CLI::Portable if (fileType == PortableFileType::File) { - if (std::filesystem::exists(filePath)) { SHA256::HashBuffer fileHash = SHA256::ComputeHashFromFile(filePath); @@ -296,7 +295,7 @@ namespace AppInstaller::CLI::Portable else { AICLI_LOG(CLI, Info, << "Unable to remove install directory as there are remaining files in: " << InstallLocation); - m_stream << Resource::String::FilesRemainInInstallDirectory << ' ' << InstallLocation << std::endl; + m_stream << Resource::String::FilesRemainInInstallDirectory << ' ' << InstallLocation.u8string() << std::endl; } } } diff --git a/src/AppInstallerCLICore/Resources.h b/src/AppInstallerCLICore/Resources.h @@ -46,6 +46,7 @@ namespace AppInstaller::CLI::Resource WINGET_DEFINE_RESOURCE_STRINGID(CommandRequiresAdmin); WINGET_DEFINE_RESOURCE_STRINGID(CompleteCommandLongDescription); WINGET_DEFINE_RESOURCE_STRINGID(CompleteCommandShortDescription); + WINGET_DEFINE_RESOURCE_STRINGID(ConvertInstallFlowToUpgrade); WINGET_DEFINE_RESOURCE_STRINGID(CountArgumentDescription); WINGET_DEFINE_RESOURCE_STRINGID(CountOutOfBoundsError); WINGET_DEFINE_RESOURCE_STRINGID(DependenciesFlowInstall); @@ -92,9 +93,11 @@ namespace AppInstaller::CLI::Resource WINGET_DEFINE_RESOURCE_STRINGID(FileArgumentDescription); WINGET_DEFINE_RESOURCE_STRINGID(FilesRemainInInstallDirectory); WINGET_DEFINE_RESOURCE_STRINGID(FlagContainAdjoinedError); + WINGET_DEFINE_RESOURCE_STRINGID(ForceArgumentDescription); WINGET_DEFINE_RESOURCE_STRINGID(GetManifestResultVersionNotFound); WINGET_DEFINE_RESOURCE_STRINGID(HashCommandLongDescription); WINGET_DEFINE_RESOURCE_STRINGID(HashCommandShortDescription); + WINGET_DEFINE_RESOURCE_STRINGID(HashOverrideArgumentDescription); WINGET_DEFINE_RESOURCE_STRINGID(HeaderArgumentDescription); WINGET_DEFINE_RESOURCE_STRINGID(HeaderArgumentNotApplicableForNonRestSourceWarning); WINGET_DEFINE_RESOURCE_STRINGID(HeaderArgumentNotApplicableWithoutSource); @@ -161,7 +164,6 @@ namespace AppInstaller::CLI::Resource WINGET_DEFINE_RESOURCE_STRINGID(InstallFlowReturnCodeRebootRequiredForInstall); WINGET_DEFINE_RESOURCE_STRINGID(InstallFlowReturnCodeRebootRequiredToFinish); WINGET_DEFINE_RESOURCE_STRINGID(InstallFlowStartingPackageInstall); - WINGET_DEFINE_RESOURCE_STRINGID(InstallForceArgumentDescription); WINGET_DEFINE_RESOURCE_STRINGID(InstallLocationNotProvided); WINGET_DEFINE_RESOURCE_STRINGID(InstallScopeDescription); WINGET_DEFINE_RESOURCE_STRINGID(InteractiveArgumentDescription); diff --git a/src/AppInstallerCLICore/Workflows/ArchiveFlow.cpp b/src/AppInstallerCLICore/Workflows/ArchiveFlow.cpp @@ -27,8 +27,7 @@ namespace AppInstaller::CLI::Workflow } else { - // TODO: replace with proper --force argument when available. - if (context.Args.Contains(Execution::Args::Type::HashOverride)) + if (context.Args.Contains(Execution::Args::Type::Force)) { AICLI_LOG(CLI, Warning, << "Archive malware scan failed; proceeding due to --force override"); context.Reporter.Warn() << Resource::String::ArchiveFailedMalwareScanOverridden << std::endl; diff --git a/src/AppInstallerCLICore/Workflows/DependenciesFlow.cpp b/src/AppInstallerCLICore/Workflows/DependenciesFlow.cpp @@ -122,14 +122,14 @@ namespace AppInstaller::CLI::Workflow const auto& packageVersion = context.Get<Execution::Data::PackageVersion>(); context.Add<Execution::Data::DependencySource>(packageVersion->GetSource()); context << - Workflow::OpenCompositeSource(Repository::PredefinedSource::Installed, true); + Workflow::OpenCompositeSource(Repository::PredefinedSource::Installed, true, Repository::CompositeSearchBehavior::AvailablePackages); } else { // install from manifest requires --dependency-source to be set context << Workflow::OpenSource(true) << - Workflow::OpenCompositeSource(Repository::PredefinedSource::Installed, true); + Workflow::OpenCompositeSource(Repository::PredefinedSource::Installed, true, Repository::CompositeSearchBehavior::AvailablePackages); } } diff --git a/src/AppInstallerCLICore/Workflows/ImportExportFlow.cpp b/src/AppInstallerCLICore/Workflows/ImportExportFlow.cpp @@ -279,23 +279,28 @@ namespace AppInstaller::CLI::Workflow searchContext.Add<Execution::Data::Source>(source); searchContext.Add<Execution::Data::SearchResult>(source.Search(searchRequest)); - // TODO: In the future, it would be better to not have to convert back and forth from a string - searchContext.Args.AddArg(Execution::Args::Type::InstallScope, ScopeToString(packageRequest.Scope)); + if (packageRequest.Scope != Manifest::ScopeEnum::Unknown) + { + // TODO: In the future, it would be better to not have to convert back and forth from a string + searchContext.Args.AddArg(Execution::Args::Type::InstallScope, ScopeToString(packageRequest.Scope)); + } - // Find the single version we want is available - searchContext << - Workflow::HandleSearchResultFailures << - Workflow::EnsureOneMatchFromSearchResult(false) << - Workflow::GetManifestWithVersionFromPackage(packageRequest.VersionAndChannel) << - Workflow::GetInstalledPackageVersion << - Workflow::SelectInstaller << - Workflow::EnsureApplicableInstaller; - - if (searchContext.Contains(Execution::Data::InstalledPackageVersion) && searchContext.Get<Execution::Data::InstalledPackageVersion>()) + auto versionString = packageRequest.VersionAndChannel.GetVersion().ToString(); + if (!versionString.empty()) + { + searchContext.Args.AddArg(Execution::Args::Type::Version, versionString); + } + + auto channelString = packageRequest.VersionAndChannel.GetChannel().ToString(); + if (!channelString.empty()) { - searchContext << Workflow::EnsureUpdateVersionApplicable; + searchContext.Args.AddArg(Execution::Args::Type::Channel, channelString); } + // Find the single version we want is available + searchContext << + Workflow::SelectSinglePackageVersionForInstallOrUpgrade(false); + if (searchContext.IsTerminated()) { if (context.IsTerminated() && context.GetTerminationHR() == E_ABORT) diff --git a/src/AppInstallerCLICore/Workflows/InstallFlow.cpp b/src/AppInstallerCLICore/Workflows/InstallFlow.cpp @@ -167,8 +167,6 @@ namespace AppInstaller::CLI::Workflow AICLI_TERMINATE_CONTEXT(APPINSTALLER_CLI_ERROR_NO_APPLICABLE_INSTALLER); } - context << EnsureSupportForInstall; - // This installer cannot be run elevated, but we are running elevated. // Implementation of de-elevation is complex; simply block for now. if (installer->ElevationRequirement == ElevationRequirementEnum::ElevationProhibited && Runtime::IsRunningAsAdmin()) @@ -176,6 +174,8 @@ namespace AppInstaller::CLI::Workflow context.Reporter.Error() << Resource::String::InstallerProhibitsElevation << std::endl; AICLI_TERMINATE_CONTEXT(APPINSTALLER_CLI_ERROR_INSTALLER_PROHIBITS_ELEVATION); } + + context << EnsureSupportForInstall; } void CheckForUnsupportedArgs(Execution::Context& context) diff --git a/src/AppInstallerCLICore/Workflows/PortableFlow.cpp b/src/AppInstallerCLICore/Workflows/PortableFlow.cpp @@ -26,9 +26,9 @@ namespace AppInstaller::CLI::Workflow 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(); + if (context.Contains(Execution::Data::PackageVersion)) + { + source = context.Get<Execution::Data::PackageVersion>()->GetSource().GetIdentifier(); } else { @@ -62,10 +62,10 @@ namespace AppInstaller::CLI::Workflow 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); + if (!AppInstaller::Filesystem::SupportsReparsePoints(symlinkDirectory)) + { + context.Reporter.Error() << Resource::String::ReparsePointsNotSupportedError << std::endl; + AICLI_TERMINATE_CONTEXT(APPINSTALLER_CLI_ERROR_PORTABLE_REPARSE_POINT_NOT_SUPPORTED); } } @@ -82,36 +82,35 @@ namespace AppInstaller::CLI::Workflow void VerifyPackageAndSourceMatch(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; + 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; } PortableInstaller& portableInstaller = context.Get<Execution::Data::PortableInstaller>(); - if (portableInstaller.ARPEntryExists()) - { - if (packageIdentifier != portableInstaller.WinGetPackageIdentifier || sourceIdentifier != portableInstaller.WinGetSourceIdentifier) - { - // 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"); - context.Reporter.Error() << Resource::String::PortablePackageAlreadyExists << std::endl; - 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; - } - } + if (portableInstaller.ARPEntryExists()) + { + if (packageIdentifier != portableInstaller.WinGetPackageIdentifier || sourceIdentifier != portableInstaller.WinGetSourceIdentifier) + { + if (!context.Args.Contains(Execution::Args::Type::Force)) + { + AICLI_LOG(CLI, Error, << "Registry match failed, skipping write to uninstall registry"); + context.Reporter.Error() << Resource::String::PortablePackageAlreadyExists << std::endl; + 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; + } + } } portableInstaller.WinGetPackageIdentifier = packageIdentifier; @@ -120,20 +119,20 @@ namespace AppInstaller::CLI::Workflow void InitializePortableInstaller(Execution::Context& context) { - Manifest::ScopeEnum scope = Manifest::ScopeEnum::Unknown; - bool isUpdate = WI_IsFlagSet(context.GetFlags(), Execution::ContextFlag::InstallerExecutionUseUpdate); - if (isUpdate) - { - IPackageVersion::Metadata installationMetadata = context.Get<Execution::Data::InstalledPackageVersion>()->GetMetadata(); - auto installerScopeItr = installationMetadata.find(Repository::PackageVersionMetadata::InstalledScope); - if (installerScopeItr != installationMetadata.end()) - { - scope = Manifest::ConvertToScopeEnum(installerScopeItr->second); - } - } - else - { - scope = Manifest::ConvertToScopeEnum(context.Args.GetArg(Execution::Args::Type::InstallScope)); + Manifest::ScopeEnum scope = Manifest::ScopeEnum::Unknown; + bool isUpdate = WI_IsFlagSet(context.GetFlags(), Execution::ContextFlag::InstallerExecutionUseUpdate); + if (isUpdate) + { + IPackageVersion::Metadata installationMetadata = context.Get<Execution::Data::InstalledPackageVersion>()->GetMetadata(); + auto installerScopeItr = installationMetadata.find(Repository::PackageVersionMetadata::InstalledScope); + if (installerScopeItr != installationMetadata.end()) + { + scope = Manifest::ConvertToScopeEnum(installerScopeItr->second); + } + } + else + { + scope = Manifest::ConvertToScopeEnum(context.Args.GetArg(Execution::Args::Type::InstallScope)); } Utility::Architecture arch = context.Get<Execution::Data::Installer>()->Arch; @@ -143,17 +142,17 @@ namespace AppInstaller::CLI::Workflow portableInstaller.IsUpdate = isUpdate; // Set target install directory - 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 - { - targetInstallDirectory = GetPortableInstallRoot(scope, arch); - targetInstallDirectory /= ConvertToUTF16(productCode); + 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 + { + targetInstallDirectory = GetPortableInstallRoot(scope, arch); + targetInstallDirectory /= ConvertToUTF16(productCode); } portableInstaller.TargetInstallLocation = targetInstallDirectory; @@ -165,194 +164,193 @@ namespace AppInstaller::CLI::Workflow { std::filesystem::path& installerPath = context.Get<Execution::Data::InstallerPath>(); PortableInstaller& portableInstaller = context.Get<Execution::Data::PortableInstaller>(); - std::vector<PortableFileEntry> entries; - - const std::filesystem::path& targetInstallDirectory = portableInstaller.TargetInstallLocation; - const std::filesystem::path& symlinkDirectory = GetPortableLinksLocation(portableInstaller.GetScope()); - - // InstallerPath will point to a directory if it is extracted from an archive. - if (std::filesystem::is_directory(installerPath)) - { - for (const auto& entry : std::filesystem::directory_iterator(installerPath)) - { - std::filesystem::path entryPath = entry.path(); - PortableFileEntry portableFile; - std::filesystem::path relativePath = std::filesystem::relative(entryPath, entryPath.parent_path()); - std::filesystem::path targetPath = targetInstallDirectory / relativePath; - - if (std::filesystem::is_directory(entryPath)) - { - entries.emplace_back(std::move(PortableFileEntry::CreateDirectoryEntry(entryPath, targetPath))); - } - else - { - entries.emplace_back(std::move(PortableFileEntry::CreateFileEntry(entryPath, targetPath, {}))); - } - } - - if (entries.size() > 1) - { - portableInstaller.RecordToIndex = true; - } - - const std::vector<Manifest::NestedInstallerFile>& nestedInstallerFiles = context.Get<Execution::Data::Installer>()->NestedInstallerFiles; - - for (const auto& nestedInstallerFile : nestedInstallerFiles) - { - const std::filesystem::path& targetPath = targetInstallDirectory / ConvertToUTF16(nestedInstallerFile.RelativeFilePath); - - std::filesystem::path commandAlias; - if (nestedInstallerFile.PortableCommandAlias.empty()) - { - commandAlias = targetPath.filename(); - } - else - { - commandAlias = ConvertToUTF16(nestedInstallerFile.PortableCommandAlias); - } - - Filesystem::AppendExtension(commandAlias, ".exe"); - entries.emplace_back(std::move(PortableFileEntry::CreateSymlinkEntry(symlinkDirectory / commandAlias, targetPath))); - } - } - else - { - std::string_view renameArg = context.Args.GetArg(Execution::Args::Type::Rename); - const std::vector<string_t>& commands = context.Get<Execution::Data::Installer>()->Commands; - std::filesystem::path fileName; - std::filesystem::path commandAlias; - - if (!renameArg.empty()) - { - fileName = commandAlias = ConvertToUTF16(renameArg); - } - else - { - if (!commands.empty()) - { - commandAlias = ConvertToUTF16(commands[0]); - } - else - { - commandAlias = installerPath.filename(); - } - - fileName = installerPath.filename(); - } - - AppInstaller::Filesystem::AppendExtension(fileName, ".exe"); - AppInstaller::Filesystem::AppendExtension(commandAlias, ".exe"); - - const std::filesystem::path& targetFullPath = targetInstallDirectory / fileName; - entries.emplace_back(std::move(PortableFileEntry::CreateFileEntry(installerPath, targetFullPath, {}))); - entries.emplace_back(std::move(PortableFileEntry::CreateSymlinkEntry(symlinkDirectory / commandAlias, targetFullPath))); + std::vector<PortableFileEntry> entries; + + const std::filesystem::path& targetInstallDirectory = portableInstaller.TargetInstallLocation; + const std::filesystem::path& symlinkDirectory = GetPortableLinksLocation(portableInstaller.GetScope()); + + // InstallerPath will point to a directory if it is extracted from an archive. + if (std::filesystem::is_directory(installerPath)) + { + for (const auto& entry : std::filesystem::directory_iterator(installerPath)) + { + std::filesystem::path entryPath = entry.path(); + PortableFileEntry portableFile; + std::filesystem::path relativePath = std::filesystem::relative(entryPath, entryPath.parent_path()); + std::filesystem::path targetPath = targetInstallDirectory / relativePath; + + if (std::filesystem::is_directory(entryPath)) + { + entries.emplace_back(std::move(PortableFileEntry::CreateDirectoryEntry(entryPath, targetPath))); + } + else + { + entries.emplace_back(std::move(PortableFileEntry::CreateFileEntry(entryPath, targetPath, {}))); + } + } + + if (entries.size() > 1) + { + portableInstaller.RecordToIndex = true; + } + + const std::vector<Manifest::NestedInstallerFile>& nestedInstallerFiles = context.Get<Execution::Data::Installer>()->NestedInstallerFiles; + + for (const auto& nestedInstallerFile : nestedInstallerFiles) + { + const std::filesystem::path& targetPath = targetInstallDirectory / ConvertToUTF16(nestedInstallerFile.RelativeFilePath); + + std::filesystem::path commandAlias; + if (nestedInstallerFile.PortableCommandAlias.empty()) + { + commandAlias = targetPath.filename(); + } + else + { + commandAlias = ConvertToUTF16(nestedInstallerFile.PortableCommandAlias); + } + + Filesystem::AppendExtension(commandAlias, ".exe"); + entries.emplace_back(std::move(PortableFileEntry::CreateSymlinkEntry(symlinkDirectory / commandAlias, targetPath))); + } + } + else + { + std::string_view renameArg = context.Args.GetArg(Execution::Args::Type::Rename); + const std::vector<string_t>& commands = context.Get<Execution::Data::Installer>()->Commands; + std::filesystem::path fileName; + std::filesystem::path commandAlias; + + if (!renameArg.empty()) + { + fileName = commandAlias = ConvertToUTF16(renameArg); + } + else + { + if (!commands.empty()) + { + commandAlias = ConvertToUTF16(commands[0]); + } + else + { + commandAlias = installerPath.filename(); + } + + fileName = installerPath.filename(); + } + + AppInstaller::Filesystem::AppendExtension(fileName, ".exe"); + AppInstaller::Filesystem::AppendExtension(commandAlias, ".exe"); + + const std::filesystem::path& targetFullPath = targetInstallDirectory / fileName; + entries.emplace_back(std::move(PortableFileEntry::CreateFileEntry(installerPath, targetFullPath, {}))); + entries.emplace_back(std::move(PortableFileEntry::CreateSymlinkEntry(symlinkDirectory / commandAlias, targetFullPath))); } return entries; } - void PortableInstallImpl(Execution::Context& context) - { - PortableInstaller& portableInstaller = context.Get<Execution::Data::PortableInstaller>(); - - try - { - context.Reporter.Info() << Resource::String::InstallFlowStartingPackageInstall << std::endl; - - std::vector<AppInstaller::Portable::PortableFileEntry> desiredState = GetDesiredStateForPortableInstall(context); - - portableInstaller.SetDesiredState(desiredState); - - if (!portableInstaller.VerifyExpectedState()) - { - if (context.Args.Contains(Execution::Args::Type::HashOverride)) - { - 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); - } - } - - portableInstaller.Install(); - context.Add<Execution::Data::OperationReturnCode>(ERROR_SUCCESS); - context.Reporter.Warn() << portableInstaller.GetOutputMessage(); - } - catch (...) - { - context.Add<Execution::Data::OperationReturnCode>(Workflow::HandleException(context, std::current_exception())); - - if (!portableInstaller.IsUpdate) - { - context.Reporter.Warn() << Resource::String::PortableInstallFailed << std::endl; - portableInstaller.PrepareForCleanUp(); -; portableInstaller.Uninstall(); + void PortableInstallImpl(Execution::Context& context) + { + PortableInstaller& portableInstaller = context.Get<Execution::Data::PortableInstaller>(); + + try + { + context.Reporter.Info() << Resource::String::InstallFlowStartingPackageInstall << std::endl; + + std::vector<AppInstaller::Portable::PortableFileEntry> desiredState = GetDesiredStateForPortableInstall(context); + + portableInstaller.SetDesiredState(desiredState); + + if (!portableInstaller.VerifyExpectedState()) + { + if (context.Args.Contains(Execution::Args::Type::Force)) + { + 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); + } + } + + portableInstaller.Install(); + context.Add<Execution::Data::OperationReturnCode>(ERROR_SUCCESS); + context.Reporter.Warn() << portableInstaller.GetOutputMessage(); + } + catch (...) + { + context.Add<Execution::Data::OperationReturnCode>(Workflow::HandleException(context, std::current_exception())); + + if (!portableInstaller.IsUpdate) + { + context.Reporter.Warn() << Resource::String::PortableInstallFailed << std::endl; + portableInstaller.PrepareForCleanUp(); +; portableInstaller.Uninstall(); AICLI_TERMINATE_CONTEXT(APPINSTALLER_CLI_ERROR_PORTABLE_UNINSTALL_FAILED); - } - } - } - - void PortableUninstallImpl(Execution::Context& context) - { - PortableInstaller& portableInstaller = context.Get<Execution::Data::PortableInstaller>(); - + } + } + } + + void PortableUninstallImpl(Execution::Context& context) + { + PortableInstaller& portableInstaller = context.Get<Execution::Data::PortableInstaller>(); + try { - context.Reporter.Info() << Resource::String::UninstallFlowStartingPackageUninstall << std::endl; + context.Reporter.Info() << Resource::String::UninstallFlowStartingPackageUninstall << std::endl; if (!portableInstaller.VerifyExpectedState()) { - // TODO: replace with appropriate --force argument when available. - if (context.Args.Contains(Execution::Args::Type::HashOverride)) + if (context.Args.Contains(Execution::Args::Type::Force)) { context.Reporter.Warn() << Resource::String::PortableHashMismatchOverridden << std::endl; } else { - context.Reporter.Warn() << Resource::String::PortableHashMismatchOverrideRequired << std::endl; + context.Reporter.Warn() << Resource::String::PortableHashMismatchOverrideRequired << std::endl; AICLI_TERMINATE_CONTEXT(APPINSTALLER_CLI_ERROR_PORTABLE_UNINSTALL_FAILED); } } - portableInstaller.Purge = context.Args.Contains(Execution::Args::Type::Purge) || + portableInstaller.Purge = context.Args.Contains(Execution::Args::Type::Purge) || (!portableInstaller.IsUpdate && Settings::User().Get<Settings::Setting::UninstallPurgePortablePackage>() && !context.Args.Contains(Execution::Args::Type::Preserve)); portableInstaller.Uninstall(); - context.Add<Execution::Data::OperationReturnCode>(ERROR_SUCCESS); - context.Reporter.Warn() << portableInstaller.GetOutputMessage(); + context.Add<Execution::Data::OperationReturnCode>(ERROR_SUCCESS); + context.Reporter.Warn() << portableInstaller.GetOutputMessage(); + } + catch (...) + { + context.Add<Execution::Data::OperationReturnCode>(Workflow::HandleException(context, std::current_exception())); } - catch (...) - { - context.Add<Execution::Data::OperationReturnCode>(Workflow::HandleException(context, std::current_exception())); - } - } - - void EnsureSupportForPortableInstall(Execution::Context& context) - { - auto installerType = context.Get<Execution::Data::Installer>().value().EffectiveInstallerType(); - - if (installerType == InstallerTypeEnum::Portable) - { - context << - EnsureRunningAsAdminForMachineScopeInstall << - EnsureValidArgsForPortableInstall << + } + + void EnsureSupportForPortableInstall(Execution::Context& context) + { + auto installerType = context.Get<Execution::Data::Installer>().value().EffectiveInstallerType(); + + if (installerType == InstallerTypeEnum::Portable) + { + context << + EnsureRunningAsAdminForMachineScopeInstall << + EnsureValidArgsForPortableInstall << EnsureVolumeSupportsReparsePoints; - } - } - - void EnsureSupportForPortableUninstall(Execution::Context& context) - { + } + } + + void EnsureSupportForPortableUninstall(Execution::Context& context) + { auto installedPackageVersion = context.Get<Execution::Data::InstalledPackageVersion>(); - const std::string installedTypeString = installedPackageVersion->GetMetadata()[PackageVersionMetadata::InstalledType]; - if (ConvertToInstallerTypeEnum(installedTypeString) == InstallerTypeEnum::Portable) - { + const std::string installedTypeString = installedPackageVersion->GetMetadata()[PackageVersionMetadata::InstalledType]; + if (ConvertToInstallerTypeEnum(installedTypeString) == InstallerTypeEnum::Portable) + { const std::string installedScope = installedPackageVersion->GetMetadata()[Repository::PackageVersionMetadata::InstalledScope]; if (ConvertToScopeEnum(installedScope) == Manifest::ScopeEnum::Machine) { context << EnsureRunningAsAdmin; - } - } - } + } + } + } } \ No newline at end of file diff --git a/src/AppInstallerCLICore/Workflows/ShellExecuteInstallerHandler.cpp b/src/AppInstallerCLICore/Workflows/ShellExecuteInstallerHandler.cpp @@ -125,8 +125,7 @@ namespace AppInstaller::CLI::Workflow } // Construct install location arg if necessary. - if (!isUpdate && - context.Args.Contains(Execution::Args::Type::InstallLocation) && + if (context.Args.Contains(Execution::Args::Type::InstallLocation) && installerSwitches.find(InstallerSwitchType::InstallLocation) != installerSwitches.end()) { installerArgs += ' ' + installerSwitches.at(InstallerSwitchType::InstallLocation); diff --git a/src/AppInstallerCLICore/Workflows/UpdateFlow.cpp b/src/AppInstallerCLICore/Workflows/UpdateFlow.cpp @@ -35,19 +35,26 @@ namespace AppInstaller::CLI::Workflow } } - void SelectLatestApplicableUpdate::operator()(Execution::Context& context) const + void SelectLatestApplicableVersion::operator()(Execution::Context& context) const { auto package = context.Get<Execution::Data::Package>(); auto installedPackage = context.Get<Execution::Data::InstalledPackageVersion>(); - Utility::Version installedVersion = Utility::Version(installedPackage->GetProperty(PackageVersionProperty::Version)); - ManifestComparator manifestComparator(context, installedPackage->GetMetadata()); - bool updateFound = false; + + bool isUpgrade = WI_IsFlagSet(context.GetFlags(), Execution::ContextFlag::InstallerExecutionUseUpdate);; + Utility::Version installedVersion; + if (isUpgrade) + { + installedVersion = Utility::Version(installedPackage->GetProperty(PackageVersionProperty::Version)); + } + + ManifestComparator manifestComparator(context, isUpgrade ? installedPackage->GetMetadata() : IPackageVersion::Metadata{}); + bool versionFound = false; bool installedTypeInapplicable = false; - if (installedVersion.IsUnknown() && !context.Args.Contains(Execution::Args::Type::IncludeUnknown)) + if (isUpgrade && installedVersion.IsUnknown() && !context.Args.Contains(Execution::Args::Type::IncludeUnknown)) { // the package has an unknown version and the user did not request to upgrade it anyway. - if (m_reportUpdateNotFound) + if (m_reportVersionNotFound) { context.Reporter.Info() << Resource::String::UpgradeUnknownVersionExplanation << std::endl; } @@ -59,8 +66,8 @@ namespace AppInstaller::CLI::Workflow const auto& versionKeys = package->GetAvailableVersionKeys(); for (const auto& key : versionKeys) { - // Check Update Version - if (IsUpdateVersionApplicable(installedVersion, Utility::Version(key.Version))) + // Check Applicable Version + if (!isUpgrade || IsUpdateVersionApplicable(installedVersion, Utility::Version(key.Version))) { auto packageVersion = package->GetAvailableVersion(key); auto manifest = packageVersion->GetManifest(); @@ -97,7 +104,7 @@ namespace AppInstaller::CLI::Workflow context.Add<Execution::Data::PackageVersion>(std::move(packageVersion)); context.Add<Execution::Data::Installer>(std::move(installer)); - updateFound = true; + versionFound = true; break; } else @@ -107,21 +114,25 @@ namespace AppInstaller::CLI::Workflow } } - if (!updateFound) + if (!versionFound) { - if (m_reportUpdateNotFound) + if (m_reportVersionNotFound) { if (installedTypeInapplicable) { context.Reporter.Info() << Resource::String::UpgradeDifferentInstallTechnologyInNewerVersions << std::endl; } - else + else if (isUpgrade) { context.Reporter.Info() << Resource::String::UpdateNotApplicable << std::endl; } + else + { + context.Reporter.Error() << Resource::String::NoApplicableInstallers << std::endl; + } } - AICLI_TERMINATE_CONTEXT(APPINSTALLER_CLI_ERROR_UPDATE_NOT_APPLICABLE); + AICLI_TERMINATE_CONTEXT(isUpgrade ? APPINSTALLER_CLI_ERROR_UPDATE_NOT_APPLICABLE : APPINSTALLER_CLI_ERROR_NO_APPLICABLE_INSTALLER); } } @@ -172,7 +183,7 @@ namespace AppInstaller::CLI::Workflow updateContext << Workflow::GetInstalledPackageVersion << Workflow::ReportExecutionStage(ExecutionStage::Discovery) << - SelectLatestApplicableUpdate(false); + SelectLatestApplicableVersion(false); if (updateContext.GetTerminationHR() == APPINSTALLER_CLI_ERROR_UPDATE_NOT_APPLICABLE) { @@ -225,4 +236,51 @@ namespace AppInstaller::CLI::Workflow context.Reporter.Info() << packagesThatRequireExplicitSkipped << " " << Resource::String::UpgradeRequireExplicitCount << std::endl; } } + + void SelectSinglePackageVersionForInstallOrUpgrade::operator()(Execution::Context& context) const + { + context << + HandleSearchResultFailures << + EnsureOneMatchFromSearchResult(m_isUpgrade) << + GetInstalledPackageVersion; + + if (!m_isUpgrade && context.Contains(Execution::Data::InstalledPackageVersion) && context.Get<Execution::Data::InstalledPackageVersion>() != nullptr) + { + AICLI_LOG(CLI, Info, << "Found installed package, converting to upgrade flow"); + context.Reporter.Info() << Execution::ConvertToUpgradeFlowEmphasis << Resource::String::ConvertInstallFlowToUpgrade << std::endl; + context.SetFlags(Execution::ContextFlag::InstallerExecutionUseUpdate); + m_isUpgrade = true; + } + + if (context.Args.Contains(Execution::Args::Type::Version)) + { + // If version specified, use the version and verify applicability + context << + GetManifestFromPackage; + + if (m_isUpgrade) + { + context << EnsureUpdateVersionApplicable; + } + + context << + SelectInstaller; + } + else + { + // Iterate through available versions to find latest applicable version. + // This step also populates Manifest and Installer in context data. + context << SelectLatestApplicableVersion(true); + } + + context << EnsureApplicableInstaller; + } + + void InstallOrUpgradeSinglePackage::operator()(Execution::Context& context) const + { + context << + SearchSourceForSingle << + SelectSinglePackageVersionForInstallOrUpgrade(m_isUpgrade) << + InstallSinglePackage; + } } diff --git a/src/AppInstallerCLICore/Workflows/UpdateFlow.h b/src/AppInstallerCLICore/Workflows/UpdateFlow.h @@ -6,19 +6,19 @@ namespace AppInstaller::CLI::Workflow { - // Iterates through all available versions from a package and find latest applicable update + // Iterates through all available versions from a package and find latest applicable version // Required Args: bool indicating whether to report update not found - // Inputs: InstalledPackageVersion, Package + // Inputs: InstalledPackageVersion?, Package // Outputs: Manifest?, Installer? - struct SelectLatestApplicableUpdate : public WorkflowTask + struct SelectLatestApplicableVersion : public WorkflowTask { - SelectLatestApplicableUpdate(bool reportUpdateNotFound) : - WorkflowTask("SelectLatestApplicableUpdate"), m_reportUpdateNotFound(reportUpdateNotFound) {} + SelectLatestApplicableVersion(bool reportVersionNotFound) : + WorkflowTask("SelectLatestApplicableUpdate"), m_reportVersionNotFound(reportVersionNotFound) {} void operator()(Execution::Context& context) const override; private: - bool m_reportUpdateNotFound; + bool m_reportVersionNotFound; }; // Ensures the update package has higher version than installed @@ -32,4 +32,34 @@ namespace AppInstaller::CLI::Workflow // Inputs: SearchResult // Outputs: None void UpdateAllApplicable(Execution::Context& context); + + // Select single package version for install or upgrade + // Required Args: bool indicating whether the flow is for upgrade + // Inputs: Source, SearchResult + // Outputs: None + struct SelectSinglePackageVersionForInstallOrUpgrade : public WorkflowTask + { + SelectSinglePackageVersionForInstallOrUpgrade(bool isUpgrade) : + WorkflowTask("SelectSinglePackageVersionForInstallOrUpgrade"), m_isUpgrade(isUpgrade) {} + + void operator()(Execution::Context& context) const override; + + private: + mutable bool m_isUpgrade; + }; + + // Install or upgrade a single package + // Required Args: bool indicating whether the flow is for upgrade + // Inputs: Source + // Outputs: None + struct InstallOrUpgradeSinglePackage : public WorkflowTask + { + InstallOrUpgradeSinglePackage(bool isUpgrade) : + WorkflowTask("InstallOrUpgradeSinglePackage"), m_isUpgrade(isUpgrade) {} + + void operator()(Execution::Context& context) const override; + + private: + mutable bool m_isUpgrade; + }; } \ No newline at end of file diff --git a/src/AppInstallerCLICore/Workflows/WorkflowBase.cpp b/src/AppInstallerCLICore/Workflows/WorkflowBase.cpp @@ -399,18 +399,17 @@ namespace AppInstaller::CLI::Workflow // Create the composite source from the two. Repository::Source source; - Repository::Source compositeSource; if (m_forDependencies) { source = context.Get<Execution::Data::DependencySource>(); - compositeSource = Repository::Source{ source, availableSource, CompositeSearchBehavior::AvailablePackages }; } else { source = context.Get<Execution::Data::Source>(); - compositeSource = Repository::Source{ source, availableSource }; } + Repository::Source compositeSource{ source, availableSource, m_searchBehavior }; + // Overwrite the source with the composite. if (m_forDependencies) { @@ -991,6 +990,7 @@ namespace AppInstaller::CLI::Workflow installationMetadata = context.Get<Execution::Data::InstalledPackageVersion>()->GetMetadata(); } + ManifestComparator manifestComparator(context, installationMetadata); auto [installer, inapplicabilities] = manifestComparator.GetPreferredInstaller(context.Get<Execution::Data::Manifest>()); @@ -1056,7 +1056,7 @@ namespace AppInstaller::CLI::Workflow } else if (installer.EffectiveInstallerType() == Manifest::InstallerTypeEnum::Portable) { - const auto& productCode = Utility::MakeSuitablePathPart(manifest.Id + "_" + source.GetIdentifier()); + const auto& productCode = Utility::MakeSuitablePathPart(manifest.Id + '_' + source.GetIdentifier()); searchRequest.Inclusions.emplace_back(PackageMatchFilter(PackageMatchField::ProductCode, MatchType::CaseInsensitive, Utility::Normalize(productCode))); } diff --git a/src/AppInstallerCLICore/Workflows/WorkflowBase.h b/src/AppInstallerCLICore/Workflows/WorkflowBase.h @@ -109,13 +109,18 @@ namespace AppInstaller::CLI::Workflow // Outputs: Source struct OpenCompositeSource : public WorkflowTask { - OpenCompositeSource(Repository::PredefinedSource source, bool forDependencies = false) : WorkflowTask("OpenCompositeSource"), m_predefinedSource(source), m_forDependencies(forDependencies) {} + OpenCompositeSource( + Repository::PredefinedSource source, + bool forDependencies = false, + Repository::CompositeSearchBehavior searchBehavior = Repository::CompositeSearchBehavior::Installed) : + WorkflowTask("OpenCompositeSource"), m_predefinedSource(source), m_forDependencies(forDependencies), m_searchBehavior(searchBehavior) {} void operator()(Execution::Context& context) const override; private: Repository::PredefinedSource m_predefinedSource; bool m_forDependencies; + Repository::CompositeSearchBehavior m_searchBehavior; }; // Performs a search on the source. diff --git a/src/AppInstallerCLIE2ETests/GroupPolicy.cs b/src/AppInstallerCLIE2ETests/GroupPolicy.cs @@ -67,7 +67,7 @@ namespace AppInstallerCLIE2ETests public void EnableHashOverride() { GroupPolicyHelper.EnableHashOverride.Disable(); - var result = TestCommon.RunAICLICommand("install", "AnyPackage --force"); + var result = TestCommon.RunAICLICommand("install", "AnyPackage --ignore-security-hash"); Assert.AreEqual(Constants.ErrorCode.ERROR_BLOCKED_BY_POLICY, result.ExitCode); } diff --git a/src/AppInstallerCLIE2ETests/InstallCommand.cs b/src/AppInstallerCLIE2ETests/InstallCommand.cs @@ -14,6 +14,13 @@ namespace AppInstallerCLIE2ETests ConfigureFeature("zipInstall", true); } + [SetUp] + public void Setup() + { + // Try clean up TestExeInstaller for failure cases where cleanup is not successful + TestCommon.RunAICLICommand("uninstall", "AppInstallerTest.TestExeInstaller"); + } + [Test] public void InstallAppDoesNotExist() { @@ -285,7 +292,7 @@ namespace AppInstallerCLIE2ETests Assert.False(result.StdOut.Contains($"Overwriting existing file: {symlinkPath}")); // Perform second install and verify that file overwrite message is displayed. - var result2 = TestCommon.RunAICLICommand("install", $"{packageId}"); + var result2 = TestCommon.RunAICLICommand("install", $"{packageId} --force"); Assert.AreEqual(Constants.ErrorCode.S_OK, result2.ExitCode); Assert.True(result2.StdOut.Contains("Successfully installed")); @@ -380,5 +387,60 @@ namespace AppInstallerCLIE2ETests Assert.True(result.StdOut.Contains("Successfully installed")); Assert.True(TestCommon.VerifyTestMsixInstalledAndCleanup()); } + + [Test] + public void InstallExeFoundExistingConvertToUpgrade() + { + var baseDir = TestCommon.GetRandomTestDir(); + var baseResult = TestCommon.RunAICLICommand("install", $"AppInstallerTest.TestExeInstaller -v 1.0.0.0 --silent -l {baseDir}"); + Assert.AreEqual(Constants.ErrorCode.S_OK, baseResult.ExitCode); + Assert.True(baseResult.StdOut.Contains("Successfully installed")); + + // Install will convert to upgrade + var upgradeDir = TestCommon.GetRandomTestDir(); + var upgradeResult = TestCommon.RunAICLICommand("install", $"AppInstallerTest.TestExeInstaller --silent -l {upgradeDir}"); + Assert.AreEqual(Constants.ErrorCode.S_OK, upgradeResult.ExitCode); + Assert.True(upgradeResult.StdOut.Contains("Trying to upgrade the installed package...")); + Assert.True(upgradeResult.StdOut.Contains("Successfully installed")); + + Assert.True(TestCommon.VerifyTestExeInstalledAndCleanup(baseDir)); + Assert.True(TestCommon.VerifyTestExeInstalledAndCleanup(upgradeDir, "/Version 2.0.0.0")); + } + + [Test] + public void InstallExeFoundExistingConvertToUpgradeNoAvailableUpgrade() + { + var baseDir = TestCommon.GetRandomTestDir(); + var baseResult = TestCommon.RunAICLICommand("install", $"AppInstallerTest.TestExeInstaller -v 2.0.0.0 --silent -l {baseDir}"); + Assert.AreEqual(Constants.ErrorCode.S_OK, baseResult.ExitCode); + Assert.True(baseResult.StdOut.Contains("Successfully installed")); + + // Install will convert to upgrade + var upgradeDir = TestCommon.GetRandomTestDir(); + var upgradeResult = TestCommon.RunAICLICommand("install", $"AppInstallerTest.TestExeInstaller --silent -l {upgradeDir}"); + Assert.AreEqual(Constants.ErrorCode.ERROR_UPDATE_NOT_APPLICABLE, upgradeResult.ExitCode); + Assert.True(upgradeResult.StdOut.Contains("Trying to upgrade the installed package...")); + Assert.True(upgradeResult.StdOut.Contains("No applicable upgrade")); + + Assert.True(TestCommon.VerifyTestExeInstalledAndCleanup(baseDir)); + } + + [Test] + public void InstallExeWithLatestInstalledWithForce() + { + var baseDir = TestCommon.GetRandomTestDir(); + var baseResult = TestCommon.RunAICLICommand("install", $"AppInstallerTest.TestExeInstaller -v 2.0.0.0 --silent -l {baseDir}"); + Assert.AreEqual(Constants.ErrorCode.S_OK, baseResult.ExitCode); + Assert.True(baseResult.StdOut.Contains("Successfully installed")); + + // Install will not convert to upgrade + var installDir = TestCommon.GetRandomTestDir(); + var installResult = TestCommon.RunAICLICommand("install", $"AppInstallerTest.TestExeInstaller -v 1.0.0.0 --silent -l {installDir} --force"); + Assert.AreEqual(Constants.ErrorCode.S_OK, installResult.ExitCode); + Assert.True(installResult.StdOut.Contains("Successfully installed")); + + Assert.True(TestCommon.VerifyTestExeInstalledAndCleanup(baseDir)); + Assert.True(TestCommon.VerifyTestExeInstalledAndCleanup(installDir, "/execustom")); + } } } \ No newline at end of file diff --git a/src/AppInstallerCLIE2ETests/Interop/UpgradeInterop.cs b/src/AppInstallerCLIE2ETests/Interop/UpgradeInterop.cs @@ -148,7 +148,7 @@ namespace AppInstallerCLIE2ETests.Interop // Configure upgrade options var upgradeOptions = TestFactory.CreateInstallOptions(); upgradeOptions.PackageVersionId = First(searchResult.CatalogPackage.AvailableVersions, (i => i.Version == "2.0.0.0")); - upgradeOptions.AllowHashMismatch = true; + upgradeOptions.Force = true; // Upgrade var upgradeResult = await packageManager.UpgradePackageAsync(searchResult.CatalogPackage, upgradeOptions); diff --git a/src/AppInstallerCLIE2ETests/ListCommand.cs b/src/AppInstallerCLIE2ETests/ListCommand.cs @@ -4,6 +4,7 @@ namespace AppInstallerCLIE2ETests { using NUnit.Framework; + using System.IO; public class ListCommand : BaseCommand { @@ -99,6 +100,12 @@ namespace AppInstallerCLIE2ETests { Assert.False(result.StdOut.Contains(notExpectedListVersion)); } + + // Try clean up + if (File.Exists(Path.Combine(installDir, Constants.TestExeInstalledFileName))) + { + TestCommon.RunCommand(Path.Combine(installDir, Constants.TestExeUninstallerFileName)); + } } } } diff --git a/src/AppInstallerCLIE2ETests/TestCommon.cs b/src/AppInstallerCLIE2ETests/TestCommon.cs @@ -350,7 +350,7 @@ namespace AppInstallerCLIE2ETests if (shouldExist) { - RunAICLICommand("uninstall", $"--product-code {productCode}"); + RunAICLICommand("uninstall", $"--product-code {productCode} --force"); } Assert.AreEqual(shouldExist, exeExists, $"Expected portable exe path: {exePath}"); @@ -399,18 +399,29 @@ namespace AppInstallerCLIE2ETests public static bool VerifyTestExeInstalledAndCleanup(string installDir, string expectedContent = null) { + bool verifyInstallSuccess = true; + if (!File.Exists(Path.Combine(installDir, Constants.TestExeInstalledFileName))) { - return false; + TestContext.Out.WriteLine($"TestExeInstalled.exe not found at {installDir}"); + verifyInstallSuccess = false; } - if (!string.IsNullOrEmpty(expectedContent)) + if (verifyInstallSuccess && !string.IsNullOrEmpty(expectedContent)) { string content = File.ReadAllText(Path.Combine(installDir, Constants.TestExeInstalledFileName)); - return content.Contains(expectedContent); + TestContext.Out.WriteLine($"TestExeInstalled.exe content: {content}"); + verifyInstallSuccess = content.Contains(expectedContent); + } + + // Always try clean up and ignore clean up failure + var uninstallerPath = Path.Combine(installDir, Constants.TestExeUninstallerFileName); + if (File.Exists(uninstallerPath)) + { + RunCommand(Path.Combine(installDir, Constants.TestExeUninstallerFileName)); } - return RunCommand(Path.Combine(installDir, Constants.TestExeUninstallerFileName)); + return verifyInstallSuccess; } public static bool VerifyTestMsiInstalledAndCleanup(string installDir) diff --git a/src/AppInstallerCLIPackage/Shared/Strings/en-us/winget.resw b/src/AppInstallerCLIPackage/Shared/Strings/en-us/winget.resw @@ -135,7 +135,7 @@ </data> <data name="AvailableHeader" xml:space="preserve"> <value>Available</value> - <comment>As in "a new version is available to update to".</comment> + <comment>As in "a new version is available to upgrade to".</comment> </data> <data name="AvailableOptions" xml:space="preserve"> <value>The following options are available:</value> @@ -263,8 +263,8 @@ They can be configured through the settings file 'winget settings'.</value> <value>Cannot install package, as it requires a higher version of Windows:</value> </data> <data name="InstallCommandLongDescription" xml:space="preserve"> - <value>Installs the selected package, either found by searching a configured source or directly from a manifest. By default, the query must case-insensitively match the id, name, or moniker of the package. Other fields can be used by passing their appropriate option.</value> - <comment>id, name, and moniker are all named values in our context, and may benefit from not being translated. The match must be for any of them, with comparison ignoring case.</comment> + <value>Installs the selected package, either found by searching a configured source or directly from a manifest. By default, the query must case-insensitively match the id, name, or moniker of the package. Other fields can be used by passing their appropriate option. By default, install command will check package installed status and try to perform an upgrade if applicable. Override with --force to perform a direct install.</value> + <comment>{Locked="--force","id","name","moniker"}; id, name, and moniker are all named values in our context, and may benefit from not being translated. The match must be for any of them, with comparison ignoring case.</comment> </data> <data name="InstallCommandShortDescription" xml:space="preserve"> <value>Installs the given package</value> @@ -273,12 +273,12 @@ They can be configured through the settings file 'winget settings'.</value> <value>Installer hash does not match; this cannot be overridden when running as admin</value> </data> <data name="InstallerHashMismatchOverridden" xml:space="preserve"> - <value>Installer hash does not match; proceeding due to --force</value> - <comment>{Locked="--force"}</comment> + <value>Installer hash does not match; proceeding due to --ignore-security-hash</value> + <comment>{Locked="--ignore-security-hash"}</comment> </data> <data name="InstallerHashMismatchOverrideRequired" xml:space="preserve"> - <value>Installer hash does not match; to override this check use --force</value> - <comment>{Locked="--force"}</comment> + <value>Installer hash does not match; to override this check use --ignore-security-hash</value> + <comment>{Locked="--ignore-security-hash"}</comment> </data> <data name="InstallerHashVerified" xml:space="preserve"> <value>Successfully verified installer hash</value> @@ -289,8 +289,8 @@ They can be configured through the settings file 'winget settings'.</value> <data name="InstallFlowStartingPackageInstall" xml:space="preserve"> <value>Starting package install...</value> </data> - <data name="InstallForceArgumentDescription" xml:space="preserve"> - <value>Override the installer hash check</value> + <data name="HashOverrideArgumentDescription" xml:space="preserve"> + <value>Ignore the installer hash check failure</value> </data> <data name="InteractiveArgumentDescription" xml:space="preserve"> <value>Request interactive installation; user input may be needed</value> @@ -316,7 +316,7 @@ They can be configured through the settings file 'winget settings'.</value> <comment>Links to different webpages</comment> </data> <data name="ListCommandLongDescription" xml:space="preserve"> - <value>The list command displays the packages installed on the system, as well as whether an update is available. Additional options can be provided to filter the output, much like the search command.</value> + <value>The list command displays the packages installed on the system, as well as whether an upgrade is available. Additional options can be provided to filter the output, much like the search command.</value> <comment>{Locked="list","search"}</comment> </data> <data name="ListCommandShortDescription" xml:space="preserve"> @@ -360,10 +360,10 @@ They can be configured through the settings file 'winget settings'.</value> <value>Failed to calculate MSIX signature hash.</value> </data> <data name="MSStoreAppBlocked" xml:space="preserve"> - <value>Failed to install or update Microsoft Store package because the specific app is blocked by policy</value> + <value>Failed to install or upgrade Microsoft Store package because the specific app is blocked by policy</value> </data> <data name="MSStoreInstallOrUpdateFailed" xml:space="preserve"> - <value>Failed to install or update Microsoft Store package. Error code:</value> + <value>Failed to install or upgrade Microsoft Store package. Error code:</value> </data> <data name="MSStoreInstallGetEntitlementNetworkError" xml:space="preserve"> <value>Verifying/Requesting package acquisition failed: network error</value> @@ -378,7 +378,7 @@ They can be configured through the settings file 'winget settings'.</value> <value>Verifying/Requesting package acquisition success</value> </data> <data name="MSStoreStoreClientBlocked" xml:space="preserve"> - <value>Failed to install or update Microsoft Store package because Microsoft Store client is blocked by policy</value> + <value>Failed to install or upgrade Microsoft Store package because Microsoft Store client is blocked by policy</value> </data> <data name="MSStoreInstallTryGetEntitlement" xml:space="preserve"> <value>Verifying/Requesting package acquisition...</value> @@ -666,10 +666,10 @@ They can be configured through the settings file 'winget settings'.</value> <value>Unrecognized command</value> </data> <data name="UpdateAllArgumentDescription" xml:space="preserve"> - <value>Update all installed packages to latest if available</value> + <value>Upgrade all installed packages to latest if available</value> </data> <data name="UpdateNotApplicable" xml:space="preserve"> - <value>No applicable update found.</value> + <value>No applicable upgrade found.</value> </data> <data name="UpgradeCommandLongDescription" xml:space="preserve"> <value>Upgrades the selected package, either found by searching the installed packages list or directly from a manifest. By default, the query must case-insensitively match the id, name, or moniker of the package. Other fields can be used by passing their appropriate option. When no arguments are given, shows the packages with upgrades available</value> @@ -1327,6 +1327,7 @@ Please specify one of them using the `--source` option to proceed.</value> </data> <data name="PortableRegistryCollisionOverridden" xml:space="preserve"> <value>A portable package with the same name but from a different source already exists; proceeding due to --force</value> + <comment>{Locked="--force"}</comment> </data> <data name="ReparsePointsNotSupportedError" xml:space="preserve"> <value>The volume does not support reparse points</value> @@ -1432,6 +1433,13 @@ Please specify one of them using the `--source` option to proceed.</value> <value>Disable interactive prompts</value> <comment>Description for a command line argument, shown next to it in the help</comment> </data> + <data name="ConvertInstallFlowToUpgrade" xml:space="preserve"> + <value>Found an existing package already installed. Trying to upgrade the installed package...</value> + </data> + <data name="ForceArgumentDescription" xml:space="preserve"> + <value>Direct run the command and continue with non security related issues</value> + <comment>Description for a command line argument, shown next to it in the help</comment> + </data> <data name="PortablePackageAlreadyExists" xml:space="preserve"> <value>Portable package from a different source already exists</value> </data> diff --git a/src/AppInstallerCLITests/WorkFlow.cpp b/src/AppInstallerCLITests/WorkFlow.cpp @@ -514,12 +514,19 @@ namespace }; } -void OverrideForOpenSource(TestContext& context) +void OverrideForOpenSource(TestContext& context, bool overrideOpenCompositeSource = false) { context.Override({ "OpenSource", [](TestContext& context) { context.Add<Execution::Data::Source>(Source{ std::make_shared<WorkflowTestSource>() }); } }); + + if (overrideOpenCompositeSource) + { + context.Override({ "OpenCompositeSource", [](TestContext&) + { + } }); + } } void OverrideForCompositeInstalledSource(TestContext& context, TestSourceSearchOptions searchOptions = TestSourceSearchOptions::None) @@ -669,6 +676,10 @@ void OverridePortableInstaller(TestContext& context) void OverrideForPortableUninstall(TestContext& context) { + context.Override({ GetUninstallInfo, [](TestContext&) + { + } }); + context.Override({ PortableUninstallImpl, [](TestContext& context) { std::filesystem::path temp = std::filesystem::temp_directory_path(); @@ -1238,7 +1249,7 @@ TEST_CASE("InstallFlow_Zip_ArchiveScanOverride", "[InstallFlow][workflow]") OverrideForExtractInstallerFromArchive(context); OverrideForVerifyAndSetNestedInstaller(context); context.Args.AddArg(Execution::Args::Type::Manifest, TestDataFile("InstallFlowTest_Zip_Exe.yaml").GetPath().u8string()); - context.Args.AddArg(Execution::Args::Type::HashOverride); + context.Args.AddArg(Execution::Args::Type::Force); bool overrideArchiveScanResult = false; AppInstaller::Archive::TestHook_SetScanArchiveResult_Override(&overrideArchiveScanResult); @@ -1416,6 +1427,17 @@ TEST_CASE("InstallFlow_Portable_SymlinkCreationFail", "[InstallFlow][workflow]") const auto& portableTargetPath = portableTargetDirectory / "AppInstallerTestExeInstaller.exe"; REQUIRE(std::filesystem::exists(portableTargetPath)); REQUIRE(AppInstaller::Registry::Environment::PathVariable(AppInstaller::Manifest::ScopeEnum::User).Contains(portableTargetDirectory)); + + // Perform uninstall + std::ostringstream uninstallOutput; + TestContext uninstallContext{ uninstallOutput, std::cin }; + auto previousThreadGlobals = uninstallContext.SetForCurrentThread(); + uninstallContext.Args.AddArg(Execution::Args::Type::Name, "AppInstaller Test Portable Exe"sv); + uninstallContext.Args.AddArg(Execution::Args::Type::AcceptSourceAgreements); + + UninstallCommand uninstall({}); + uninstall.Execute(uninstallContext); + INFO(uninstallOutput.str()); } TEST_CASE("PortableInstallFlow_UserScope", "[InstallFlow][workflow]") @@ -1598,7 +1620,7 @@ TEST_CASE("InstallFlow_SearchAndInstall", "[InstallFlow][workflow]") std::ostringstream installOutput; TestContext context{ installOutput, std::cin }; auto previousThreadGlobals = context.SetForCurrentThread(); - OverrideForOpenSource(context); + OverrideForOpenSource(context, true); OverrideForShellExecute(context); context.Args.AddArg(Execution::Args::Type::Query, "TestQueryReturnOne"sv); @@ -1621,7 +1643,7 @@ TEST_CASE("InstallFlow_SearchFoundNoApp", "[InstallFlow][workflow]") std::ostringstream installOutput; TestContext context{ installOutput, std::cin }; auto previousThreadGlobals = context.SetForCurrentThread(); - OverrideForOpenSource(context); + OverrideForOpenSource(context, true); context.Args.AddArg(Execution::Args::Type::Query, "TestQueryReturnZero"sv); InstallCommand install({}); @@ -1637,7 +1659,7 @@ TEST_CASE("InstallFlow_SearchFoundMultipleApp", "[InstallFlow][workflow]") std::ostringstream installOutput; TestContext context{ installOutput, std::cin }; auto previousThreadGlobals = context.SetForCurrentThread(); - OverrideForOpenSource(context); + OverrideForOpenSource(context, true); context.Args.AddArg(Execution::Args::Type::Query, "TestQueryReturnTwo"sv); InstallCommand install({}); @@ -1886,8 +1908,6 @@ TEST_CASE("DependencyGraph_validMinVersions", "[InstallFlow][workflow][dependenc TEST_CASE("DependencyGraph_PathNoLoop", "[InstallFlow][workflow][dependencyGraph][dependencies]", ) { - TestCommon::TempFile installResultPath("TestExeInstalled.txt"); - std::ostringstream installOutput; TestContext context{ installOutput, std::cin }; auto previousThreadGlobals = context.SetForCurrentThread(); @@ -2035,6 +2055,8 @@ TEST_CASE("UpdateFlow_UpdateExe", "[UpdateFlow][workflow]") TEST_CASE("UpdateFlow_UpdateZip_Exe", "[UpdateFlow][workflow]") { TestCommon::TempFile updateResultPath("TestExeInstalled.txt"); + TestCommon::TestUserSettings testSettings; + testSettings.Set<Setting::EFZipInstall>(true); std::ostringstream updateOutput; TestContext context{ updateOutput, std::cin }; @@ -2372,8 +2394,6 @@ TEST_CASE("UpdateFlow_UpgradeWithDuplicateUpgradeItemsFound", "[UpdateFlow][work TEST_CASE("UpdateFlow_Dependencies", "[UpdateFlow][workflow][dependencies]") { - TestCommon::TempFile updateResultPath("TestExeInstalled.txt"); - std::ostringstream updateOutput; TestContext context{ updateOutput, std::cin }; auto previousThreadGlobals = context.SetForCurrentThread(); @@ -2979,9 +2999,6 @@ TEST_CASE("ImportFlow_MachineScope", "[ImportFlow][workflow]") TEST_CASE("ImportFlow_Dependencies", "[ImportFlow][workflow][dependencies]") { - TestCommon::TempFile exeInstallResultPath("TestExeInstalled.txt"); - TestCommon::TempFile msixInstallResultPath("TestMsixInstalled.txt"); - std::ostringstream importOutput; TestContext context{ importOutput, std::cin }; auto previousThreadGlobals = context.SetForCurrentThread(); @@ -3306,7 +3323,6 @@ TEST_CASE("ValidateCommand_Dependencies", "[workflow][dependencies]") TEST_CASE("DependencyGraph_StackOrderIsOk", "[InstallFlow][workflow][dependencyGraph][dependencies]") { - TestCommon::TempFile installResultPath("TestExeInstalled.txt"); std::vector<Dependency> installationOrder; std::ostringstream installOutput; @@ -3335,8 +3351,6 @@ TEST_CASE("DependencyGraph_StackOrderIsOk", "[InstallFlow][workflow][dependencyG TEST_CASE("InstallerWithoutDependencies_RootDependenciesAreUsed", "[dependencies]") { - TestCommon::TempFile installResultPath("TestExeInstalled.txt"); - std::ostringstream installOutput; TestContext context{ installOutput, std::cin }; auto previousThreadGlobals = context.SetForCurrentThread(); @@ -3359,8 +3373,6 @@ TEST_CASE("InstallerWithoutDependencies_RootDependenciesAreUsed", "[dependencies TEST_CASE("DependenciesMultideclaration_InstallerDependenciesPreference", "[dependencies]") { - TestCommon::TempFile installResultPath("TestExeInstalled.txt"); - std::ostringstream installOutput; TestContext context{ installOutput, std::cin }; auto previousThreadGlobals = context.SetForCurrentThread(); @@ -3385,8 +3397,6 @@ TEST_CASE("DependenciesMultideclaration_InstallerDependenciesPreference", "[depe TEST_CASE("InstallFlow_Dependencies", "[InstallFlow][workflow][dependencies]") { - TestCommon::TempFile installResultPath("TestExeInstalled.txt"); - std::ostringstream installOutput; TestContext context{ installOutput, std::cin }; auto previousThreadGlobals = context.SetForCurrentThread(); @@ -3647,4 +3657,50 @@ TEST_CASE("PromptFlow_InstallLocationRequired_Missing", "[PromptFlow][workflow]" // Verify installation failed REQUIRE_TERMINATED_WITH(context, APPINSTALLER_CLI_ERROR_INSTALL_LOCATION_REQUIRED); REQUIRE_FALSE(std::filesystem::exists(installResultPath.GetPath())); +} + +TEST_CASE("InstallFlow_FoundInstalledAndUpgradeAvailable", "[UpdateFlow][workflow]") +{ + TestCommon::TempFile installResultPath("TestExeInstalled.txt"); + + std::ostringstream installOutput; + TestContext context{ installOutput, std::cin }; + auto previousThreadGlobals = context.SetForCurrentThread(); + OverrideForCompositeInstalledSource(context); + OverrideForShellExecute(context); + context.Args.AddArg(Execution::Args::Type::Query, "AppInstallerCliTest.TestExeInstaller"sv); + context.Args.AddArg(Execution::Args::Type::Silent); + + InstallCommand install({}); + install.Execute(context); + INFO(installOutput.str()); + + // Verify Installer is called and parameters are passed in. + REQUIRE(std::filesystem::exists(installResultPath.GetPath())); + std::ifstream installResultFile(installResultPath.GetPath()); + REQUIRE(installResultFile.is_open()); + std::string installResultStr; + std::getline(installResultFile, installResultStr); + REQUIRE(installResultStr.find("/update") != std::string::npos); + REQUIRE(installResultStr.find("/ver3.0.0.0") != std::string::npos); +} + +TEST_CASE("InstallFlow_FoundInstalledAndUpgradeNotAvailable", "[UpdateFlow][workflow]") +{ + TestCommon::TempFile installResultPath("TestExeInstalled.txt"); + + std::ostringstream installOutput; + TestContext context{ installOutput, std::cin }; + auto previousThreadGlobals = context.SetForCurrentThread(); + OverrideForCompositeInstalledSource(context); + context.Args.AddArg(Execution::Args::Type::Query, "TestExeInstallerWithLatestInstalled"sv); + + InstallCommand install({}); + install.Execute(context); + INFO(installOutput.str()); + + // Verify Installer is not called. + REQUIRE(!std::filesystem::exists(installResultPath.GetPath())); + REQUIRE(installOutput.str().find(Resource::LocString(Resource::String::UpdateNotApplicable).get()) != std::string::npos); + REQUIRE(context.GetTerminationHR() == APPINSTALLER_CLI_ERROR_UPDATE_NOT_APPLICABLE); } \ No newline at end of file diff --git a/src/AppInstallerTestExeInstaller/main.cpp b/src/AppInstallerTestExeInstaller/main.cpp @@ -180,6 +180,7 @@ int wmain(int argc, const wchar_t** argv) if (++i < argc) { productCode = argv[i]; + outContent << argv[i] << ' '; } } @@ -189,6 +190,7 @@ int wmain(int argc, const wchar_t** argv) if (++i < argc) { displayName = argv[i]; + outContent << argv[i] << ' '; } } @@ -198,6 +200,7 @@ int wmain(int argc, const wchar_t** argv) if (++i < argc) { displayVersion = argv[i]; + outContent << argv[i] << ' '; } } @@ -208,6 +211,7 @@ int wmain(int argc, const wchar_t** argv) { logFile = std::wofstream(argv[i], std::wofstream::out | std::wofstream::trunc); out = &logFile; + outContent << argv[i] << ' '; } } } diff --git a/src/Microsoft.Management.Deployment/InstallOptions.cpp b/src/Microsoft.Management.Deployment/InstallOptions.cpp @@ -112,6 +112,14 @@ namespace winrt::Microsoft::Management::Deployment::implementation { m_allowUpgradeToUnknownVersion = value; } + bool InstallOptions::Force() + { + return m_force; + } + void InstallOptions::Force(bool value) + { + m_force = value; + } CoCreatableMicrosoftManagementDeploymentClass(InstallOptions); } diff --git a/src/Microsoft.Management.Deployment/InstallOptions.h b/src/Microsoft.Management.Deployment/InstallOptions.h @@ -32,6 +32,8 @@ namespace winrt::Microsoft::Management::Deployment::implementation winrt::Windows::Foundation::Collections::IVector<winrt::Windows::System::ProcessorArchitecture> AllowedArchitectures(); bool AllowUpgradeToUnknownVersion(); void AllowUpgradeToUnknownVersion(bool value); + bool Force(); + void Force(bool value); #if !defined(INCLUDE_ONLY_INTERFACE_METHODS) private: @@ -47,6 +49,7 @@ namespace winrt::Microsoft::Management::Deployment::implementation Windows::Foundation::Collections::IVector<Windows::System::ProcessorArchitecture> m_allowedArchitectures{ winrt::single_threaded_vector<winrt::Windows::System::ProcessorArchitecture>() }; bool m_allowUpgradeToUnknownVersion = false; + bool m_force = false; #endif }; } diff --git a/src/Microsoft.Management.Deployment/PackageManager.cpp b/src/Microsoft.Management.Deployment/PackageManager.cpp @@ -349,6 +349,10 @@ namespace winrt::Microsoft::Management::Deployment::implementation { context->Args.AddArg(Execution::Args::Type::HashOverride); } + if (options.Force()) + { + context->Args.AddArg(Execution::Args::Type::Force); + } // If the PackageInstallScope is anything other than ::Any then set it as a requirement. auto manifestScope = GetManifestScope(options.PackageInstallScope()); @@ -406,6 +410,10 @@ namespace winrt::Microsoft::Management::Deployment::implementation context->Args.AddArg(Execution::Args::Type::Log, ::AppInstaller::Utility::ConvertToUTF8(options.LogOutputPath())); context->Args.AddArg(Execution::Args::Type::VerboseLogs); } + if (options.Force()) + { + context->Args.AddArg(Execution::Args::Type::Force); + } if (options.PackageUninstallMode() == PackageUninstallMode::Interactive) { diff --git a/src/Microsoft.Management.Deployment/PackageManager.idl b/src/Microsoft.Management.Deployment/PackageManager.idl @@ -753,6 +753,12 @@ namespace Microsoft.Management.Deployment /// Allow the upgrade to continue for upgrade packages with manifest versions Unknown. Boolean AllowUpgradeToUnknownVersion; } + + [contract(Microsoft.Management.Deployment.WindowsPackageManagerContract, 5)] + { + /// Force the operation to continue upon non security related failures. + Boolean Force; + } } [contract(Microsoft.Management.Deployment.WindowsPackageManagerContract, 4)] @@ -786,6 +792,12 @@ namespace Microsoft.Management.Deployment /// Used by a caller to correlate the install with a caller's data. /// The string must be JSON encoded. String CorrelationData; + + [contract(Microsoft.Management.Deployment.WindowsPackageManagerContract, 5)] + { + /// Force the operation to continue upon non security related failures. + Boolean Force; + } } [contract(Microsoft.Management.Deployment.WindowsPackageManagerContract, 1)] diff --git a/src/Microsoft.Management.Deployment/UninstallOptions.cpp b/src/Microsoft.Management.Deployment/UninstallOptions.cpp @@ -49,6 +49,14 @@ namespace winrt::Microsoft::Management::Deployment::implementation { m_correlationData = value; } + bool UninstallOptions::Force() + { + return m_force; + } + void UninstallOptions::Force(bool value) + { + m_force = value; + } CoCreatableMicrosoftManagementDeploymentClass(UninstallOptions); } diff --git a/src/Microsoft.Management.Deployment/UninstallOptions.h b/src/Microsoft.Management.Deployment/UninstallOptions.h @@ -19,6 +19,8 @@ namespace winrt::Microsoft::Management::Deployment::implementation void LogOutputPath(hstring const& value); hstring CorrelationData(); void CorrelationData(hstring const& value); + bool Force(); + void Force(bool value); #if !defined(INCLUDE_ONLY_INTERFACE_METHODS) private: @@ -26,6 +28,7 @@ namespace winrt::Microsoft::Management::Deployment::implementation winrt::Microsoft::Management::Deployment::PackageUninstallMode m_packageUninstallMode = winrt::Microsoft::Management::Deployment::PackageUninstallMode::Default; std::wstring m_logOutputPath = L""; std::wstring m_correlationData = L""; + bool m_force = false; #endif }; } diff --git a/src/PowerShell/Microsoft.WinGet.Client/Commands/UninstallPackageCommand.cs b/src/PowerShell/Microsoft.WinGet.Client/Commands/UninstallPackageCommand.cs @@ -30,6 +30,12 @@ namespace Microsoft.WinGet.Client.Commands public PackageUninstallMode Mode { get; set; } = PackageUninstallMode.Default; /// <summary> + /// Gets or sets a value indicating whether to continue upon non security related failures. + /// </summary> + [Parameter(ValueFromPipelineByPropertyName = true)] + public SwitchParameter Force { get; set; } + + /// <summary> /// Uninstalls a package from the local system. /// </summary> protected override void ProcessRecord() @@ -45,6 +51,7 @@ namespace Microsoft.WinGet.Client.Commands private UninstallOptions GetUninstallOptions(PackageVersionId version) { var options = ComObjectFactory.Value.CreateUninstallOptions(); + options.Force = this.Force.ToBool(); if (this.Log != null) { options.LogOutputPath = this.Log; diff --git a/src/PowerShell/Microsoft.WinGet.Client/Common/BaseInstallCommand.cs b/src/PowerShell/Microsoft.WinGet.Client/Common/BaseInstallCommand.cs @@ -54,6 +54,12 @@ namespace Microsoft.WinGet.Client.Common /// Gets or sets a value indicating whether to skip the installer hash validation check. /// </summary> [Parameter(ValueFromPipelineByPropertyName = true)] + public SwitchParameter AllowHashMismatch { get; set; } + + /// <summary> + /// Gets or sets a value indicating whether to continue upon non security related failures. + /// </summary> + [Parameter(ValueFromPipelineByPropertyName = true)] public SwitchParameter Force { get; set; } /// <summary> @@ -70,7 +76,8 @@ namespace Microsoft.WinGet.Client.Common protected virtual InstallOptions GetInstallOptions(PackageVersionId version) { InstallOptions options = ComObjectFactory.Value.CreateInstallOptions(); - options.AllowHashMismatch = this.Force.ToBool(); + options.AllowHashMismatch = this.AllowHashMismatch.ToBool(); + options.Force = this.Force.ToBool(); options.PackageInstallMode = this.Mode; if (version != null) {