winget-cli

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

commit 21bb89e135a9a61c4256f3b6ad6e97ce1b44c9ee
parent 8a01471a36aeb75dd9e79b57958ee67229aee542
Author: yao-msft <50888816+yao-msft@users.noreply.github.com>
Date:   Tue, 13 Oct 2020 17:02:08 -0700

Implement update flow (#588)


Diffstat:
Msrc/AppInstallerCLICore/AppInstallerCLICore.vcxproj | 6++++++
Msrc/AppInstallerCLICore/AppInstallerCLICore.vcxproj.filters | 18++++++++++++++++++
Msrc/AppInstallerCLICore/Command.cpp | 7+++++++
Msrc/AppInstallerCLICore/Commands/CompleteCommand.cpp | 3++-
Msrc/AppInstallerCLICore/Commands/InstallCommand.cpp | 28++++++++++++++++------------
Asrc/AppInstallerCLICore/Commands/UpgradeCommand.cpp | 151++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Asrc/AppInstallerCLICore/Commands/UpgradeCommand.h | 25+++++++++++++++++++++++++
Msrc/AppInstallerCLICore/ExecutionArgs.h | 6++++++
Msrc/AppInstallerCLICore/ExecutionContext.cpp | 6++++--
Msrc/AppInstallerCLICore/ExecutionContext.h | 31+++++++++++++++++++++++++++----
Msrc/AppInstallerCLICore/Resources.h | 21++++++++++++++++++---
Msrc/AppInstallerCLICore/Workflows/InstallFlow.cpp | 141+++++++------------------------------------------------------------------------
Msrc/AppInstallerCLICore/Workflows/InstallFlow.h | 6------
Asrc/AppInstallerCLICore/Workflows/MSStoreInstallerHandler.cpp | 197+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Asrc/AppInstallerCLICore/Workflows/MSStoreInstallerHandler.h | 29+++++++++++++++++++++++++++++
Msrc/AppInstallerCLICore/Workflows/ManifestComparator.cpp | 43+++++++++++++++++++++++++++++++++++++++----
Msrc/AppInstallerCLICore/Workflows/ManifestComparator.h | 15++++++++++++---
Msrc/AppInstallerCLICore/Workflows/ShellExecuteInstallerHandler.cpp | 39+++++++++++++++++++++++++--------------
Asrc/AppInstallerCLICore/Workflows/UpdateFlow.cpp | 135+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Asrc/AppInstallerCLICore/Workflows/UpdateFlow.h | 36++++++++++++++++++++++++++++++++++++
Msrc/AppInstallerCLICore/Workflows/WorkflowBase.cpp | 107++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++---------
Msrc/AppInstallerCLICore/Workflows/WorkflowBase.h | 20++++++++++++++++++++
Msrc/AppInstallerCLICore/pch.h | 1+
Msrc/AppInstallerCLIPackage/Shared/Strings/en-us/winget.resw | 61++++++++++++++++++++++++++++++++++++++++++++++++++++++-------
Msrc/AppInstallerCLITests/AppInstallerCLITests.vcxproj | 12+++++++++---
Msrc/AppInstallerCLITests/AppInstallerCLITests.vcxproj.filters | 9+++++++++
Msrc/AppInstallerCLITests/TestData/InstallFlowTest_Exe.yaml | 3++-
Msrc/AppInstallerCLITests/TestData/InstallFlowTest_MSStore.yaml | 2+-
Asrc/AppInstallerCLITests/TestData/Manifest-Bad-InvalidUpdateBehavior.yaml | 13+++++++++++++
Msrc/AppInstallerCLITests/TestData/Manifest-Good.yaml | 4++++
Asrc/AppInstallerCLITests/TestData/UpdateFlowTest_Exe.yaml | 18++++++++++++++++++
Asrc/AppInstallerCLITests/TestData/UpdateFlowTest_Msix.yaml | 14++++++++++++++
Msrc/AppInstallerCLITests/WorkFlow.cpp | 553++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++-----------
Msrc/AppInstallerCLITests/YamlManifest.cpp | 7+++++++
Msrc/AppInstallerCommonCore/Architecture.cpp | 2+-
Msrc/AppInstallerCommonCore/ExperimentalFeature.cpp | 4++++
Msrc/AppInstallerCommonCore/Manifest/ManifestInstaller.cpp | 42++++++++++++++++++++++++++++++++++++++++++
Msrc/AppInstallerCommonCore/Manifest/ManifestValidation.cpp | 5+++++
Msrc/AppInstallerCommonCore/Manifest/YamlParser.cpp | 7++++++-
Msrc/AppInstallerCommonCore/Public/AppInstallerArchitecture.h | 2++
Msrc/AppInstallerCommonCore/Public/AppInstallerErrors.h | 2++
Msrc/AppInstallerCommonCore/Public/winget/ExperimentalFeature.h | 3++-
Msrc/AppInstallerCommonCore/Public/winget/Manifest.h | 3+++
Msrc/AppInstallerCommonCore/Public/winget/ManifestInstaller.h | 18+++++++++++++++++-
Msrc/AppInstallerCommonCore/Public/winget/UserSettings.h | 2++
Msrc/AppInstallerCommonCore/UserSettings.cpp | 6++++++
Msrc/AppInstallerRepositoryCore/Public/AppInstallerRepositorySource.h | 4++++
Msrc/AppInstallerRepositoryCore/RepositorySource.cpp | 6++++++
48 files changed, 1594 insertions(+), 279 deletions(-)

diff --git a/src/AppInstallerCLICore/AppInstallerCLICore.vcxproj b/src/AppInstallerCLICore/AppInstallerCLICore.vcxproj @@ -184,6 +184,7 @@ <ClInclude Include="Commands\InstallCommand.h" /> <ClInclude Include="Commands\RootCommand.h" /> <ClInclude Include="Commands\SourceCommand.h" /> + <ClInclude Include="Commands\UpgradeCommand.h" /> <ClInclude Include="Commands\ValidateCommand.h" /> <ClInclude Include="Commands\SettingsCommand.h" /> <ClInclude Include="CompletionData.h" /> @@ -199,11 +200,13 @@ <ClInclude Include="TableOutput.h" /> <ClInclude Include="VTSupport.h" /> <ClInclude Include="Workflows\CompletionFlow.h" /> + <ClInclude Include="Workflows\MSStoreInstallerHandler.h" /> <ClInclude Include="Workflows\ShellExecuteInstallerHandler.h" /> <ClInclude Include="Workflows\InstallFlow.h" /> <ClInclude Include="Workflows\ManifestComparator.h" /> <ClInclude Include="Workflows\ShowFlow.h" /> <ClInclude Include="Workflows\SourceFlow.h" /> + <ClInclude Include="Workflows\UpdateFlow.h" /> <ClInclude Include="Workflows\WorkflowBase.h" /> </ItemGroup> <ItemGroup> @@ -219,6 +222,7 @@ <ClCompile Include="Commands\InstallCommand.cpp" /> <ClCompile Include="Commands\RootCommand.cpp" /> <ClCompile Include="Commands\SourceCommand.cpp" /> + <ClCompile Include="Commands\UpgradeCommand.cpp" /> <ClCompile Include="Commands\ValidateCommand.cpp" /> <ClCompile Include="Commands\SettingsCommand.cpp" /> <ClCompile Include="CompletionData.cpp" /> @@ -232,11 +236,13 @@ <ClCompile Include="Resources.cpp" /> <ClCompile Include="VTSupport.cpp" /> <ClCompile Include="Workflows\CompletionFlow.cpp" /> + <ClCompile Include="Workflows\MSStoreInstallerHandler.cpp" /> <ClCompile Include="Workflows\ShellExecuteInstallerHandler.cpp" /> <ClCompile Include="Workflows\InstallFlow.cpp" /> <ClCompile Include="Workflows\ManifestComparator.cpp" /> <ClCompile Include="Workflows\ShowFlow.cpp" /> <ClCompile Include="Workflows\SourceFlow.cpp" /> + <ClCompile Include="Workflows\UpdateFlow.cpp" /> <ClCompile Include="Workflows\WorkflowBase.cpp" /> </ItemGroup> <ItemGroup> diff --git a/src/AppInstallerCLICore/AppInstallerCLICore.vcxproj.filters b/src/AppInstallerCLICore/AppInstallerCLICore.vcxproj.filters @@ -123,6 +123,15 @@ <ClInclude Include="ChannelStreams.h"> <Filter>Header Files</Filter> </ClInclude> + <ClInclude Include="Workflows\UpdateFlow.h"> + <Filter>Workflows</Filter> + </ClInclude> + <ClInclude Include="Workflows\MSStoreInstallerHandler.h"> + <Filter>Workflows</Filter> + </ClInclude> + <ClInclude Include="Commands\UpgradeCommand.h"> + <Filter>Commands</Filter> + </ClInclude> </ItemGroup> <ItemGroup> <ClCompile Include="pch.cpp"> @@ -212,6 +221,15 @@ <ClCompile Include="ChannelStreams.cpp"> <Filter>Source Files</Filter> </ClCompile> + <ClCompile Include="Workflows\UpdateFlow.cpp"> + <Filter>Workflows</Filter> + </ClCompile> + <ClCompile Include="Workflows\MSStoreInstallerHandler.cpp"> + <Filter>Workflows</Filter> + </ClCompile> + <ClCompile Include="Commands\UpgradeCommand.cpp"> + <Filter>Commands</Filter> + </ClCompile> </ItemGroup> <ItemGroup> <None Include="PropertySheet.props" /> diff --git a/src/AppInstallerCLICore/Command.cpp b/src/AppInstallerCLICore/Command.cpp @@ -11,6 +11,8 @@ namespace AppInstaller::CLI using namespace Utility::literals; using namespace Settings; + constexpr std::string_view s_Command_ArgName_SilentAndInteractive = "silent|interactive"sv; + Command::Command(std::string_view name, std::string_view parent, Command::Visibility visibility, ExperimentalFeature::Feature feature) : m_name(name), m_visibility(visibility), m_feature(feature) { @@ -720,6 +722,11 @@ namespace AppInstaller::CLI throw CommandException(Resource::String::TooManyArgError, arg.Name()); } } + + if (execArgs.Contains(Execution::Args::Type::Silent) && execArgs.Contains(Execution::Args::Type::Interactive)) + { + throw CommandException(Resource::String::TooManyBehaviorsError, s_Command_ArgName_SilentAndInteractive); + } } void Command::ExecuteInternal(Execution::Context& context) const diff --git a/src/AppInstallerCLICore/Commands/CompleteCommand.cpp b/src/AppInstallerCLICore/Commands/CompleteCommand.cpp @@ -54,7 +54,8 @@ namespace AppInstaller::CLI } // Create a new Context to execute the Complete from - Context subContext = context.Clone(); + auto subContextPtr = context.Clone(); + Context& subContext = *subContextPtr; subContext.Reporter.SetChannel(Execution::Reporter::Channel::Completion); subContext.Add<Data::CompletionData>(std::move(data)); diff --git a/src/AppInstallerCLICore/Commands/InstallCommand.cpp b/src/AppInstallerCLICore/Commands/InstallCommand.cpp @@ -13,10 +13,6 @@ using namespace AppInstaller::CLI::Workflow; namespace AppInstaller::CLI { - using namespace std::string_view_literals; - - constexpr std::string_view s_InstallCommand_ArgName_SilentAndInteractive = "silent|interactive"sv; - std::vector<Argument> InstallCommand::GetArguments() const { return { @@ -82,6 +78,22 @@ namespace AppInstaller::CLI return "https://aka.ms/winget-command-install"; } + void InstallCommand::ValidateArgumentsInternal(Execution::Args& execArgs) const + { + if (execArgs.Contains(Execution::Args::Type::Manifest) && + (execArgs.Contains(Execution::Args::Type::Query) || + execArgs.Contains(Execution::Args::Type::Id) || + execArgs.Contains(Execution::Args::Type::Name) || + execArgs.Contains(Execution::Args::Type::Moniker) || + execArgs.Contains(Execution::Args::Type::Version) || + execArgs.Contains(Execution::Args::Type::Channel) || + execArgs.Contains(Execution::Args::Type::Source) || + execArgs.Contains(Execution::Args::Type::Exact))) + { + throw CommandException(Resource::String::BothManifestAndSearchQueryProvided, ""); + } + } + void InstallCommand::ExecuteInternal(Execution::Context& context) const { context << @@ -94,12 +106,4 @@ namespace AppInstaller::CLI Workflow::ExecuteInstaller << Workflow::RemoveInstaller; } - - void InstallCommand::ValidateArgumentsInternal(Execution::Args& execArgs) const - { - if (execArgs.Contains(Execution::Args::Type::Silent) && execArgs.Contains(Execution::Args::Type::Interactive)) - { - throw CommandException(Resource::String::TooManyBehaviorsError, s_InstallCommand_ArgName_SilentAndInteractive); - } - } } diff --git a/src/AppInstallerCLICore/Commands/UpgradeCommand.cpp b/src/AppInstallerCLICore/Commands/UpgradeCommand.cpp @@ -0,0 +1,151 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. +#include "pch.h" +#include "UpgradeCommand.h" +#include "Workflows/CompletionFlow.h" +#include "Workflows/InstallFlow.h" +#include "Workflows/UpdateFlow.h" +#include "Workflows/WorkflowBase.h" +#include "Resources.h" + +using namespace AppInstaller::CLI::Execution; +using namespace AppInstaller::Manifest; +using namespace AppInstaller::CLI::Workflow; + +namespace AppInstaller::CLI +{ + std::vector<Argument> UpgradeCommand::GetArguments() const + { + return { + Argument::ForType(Args::Type::Query), + Argument::ForType(Args::Type::Manifest), + Argument::ForType(Args::Type::Id), + Argument::ForType(Args::Type::Name), + Argument::ForType(Args::Type::Moniker), + 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::Language), + Argument::ForType(Args::Type::Log), + Argument::ForType(Args::Type::Override), + Argument::ForType(Args::Type::InstallLocation), + Argument{ "force", Argument::NoAlias, Args::Type::Force, Resource::String::InstallForceArgumentDescription, ArgumentType::Flag }, + Argument{ "all", Argument::NoAlias, Args::Type::All, Resource::String::UpdateAllArgumentDescription, ArgumentType::Flag }, + }; + } + + Resource::LocString UpgradeCommand::ShortDescription() const + { + return { Resource::String::UpgradeCommandShortDescription }; + } + + Resource::LocString UpgradeCommand::LongDescription() const + { + return { Resource::String::UpgradeCommandLongDescription }; + } + + void UpgradeCommand::Complete(Execution::Context&, Execution::Args::Type) const + { + // TODO: Should be done similar to list completion + } + + std::string UpgradeCommand::HelpLink() const + { + // TODO: point to correct location + return "https://aka.ms/winget-command-upgrade"; + } + + void UpgradeCommand::ValidateArgumentsInternal(Execution::Args& execArgs) const + { + if (execArgs.Contains(Execution::Args::Type::Manifest) && + (execArgs.Contains(Execution::Args::Type::Query) || + execArgs.Contains(Execution::Args::Type::Id) || + execArgs.Contains(Execution::Args::Type::Name) || + execArgs.Contains(Execution::Args::Type::Moniker) || + execArgs.Contains(Execution::Args::Type::Version) || + execArgs.Contains(Execution::Args::Type::Channel) || + execArgs.Contains(Execution::Args::Type::Source) || + execArgs.Contains(Execution::Args::Type::Exact) || + execArgs.Contains(Execution::Args::Type::All))) + { + throw CommandException(Resource::String::BothManifestAndSearchQueryProvided, ""); + } + } + + void UpgradeCommand::ExecuteInternal(Execution::Context& context) const + { + WI_SetFlag(context.GetFlags(), Execution::ContextFlag::InstallerExecutionUseUpdate); + + context << + OpenSource << + GetCompositeSourceFromInstalledAndAvailable; + + if (context.Args.Empty()) + { + // Upgrade with no args list packages with updates available + // TODO: go to list filtered to packages with update available + } + else if (context.Args.Contains(Execution::Args::Type::All)) + { + // --all switch updates all packages found + context << + SearchSourceForMany << + EnsureMatchesFromSearchResult << + UpdateAllApplicable; + } + else if (context.Args.Contains(Execution::Args::Type::Manifest)) + { + // --manifest case where new manifest is provided + context << + GetManifestFromArg << + ReportManifestIdentity << + SearchSourceUsingManifest << + EnsureOneMatchFromSearchResult << + GetInstalledPackageVersion << + EnsureUpdateVersionApplicable << + EnsureMinOSVersion << + SelectInstaller << + EnsureApplicableInstaller << + ShowInstallationDisclaimer << + DownloadInstaller << + ExecuteInstaller << + RemoveInstaller; + } + else + { + // The remaining case: search for single installed package to update + context << + SearchSourceForSingle << + EnsureOneMatchFromSearchResult << + ReportSearchResultIdentity << + GetInstalledPackageVersion; + + if (context.Args.Contains(Execution::Args::Type::Version)) + { + // If version specified, use the version and verify applicability + context << + GetManifestFromSearchResult << + EnsureUpdateVersionApplicable << + EnsureMinOSVersion << + SelectInstaller << + EnsureApplicableInstaller; + } + else + { + // iterate through available versions to find latest applicable update + // This step also populates Manifest and Installer in context data + context << + SelectLatestApplicableUpdate(*(context.Get<Execution::Data::SearchResult>().Matches.at(0).Package)); + } + + context << + ShowInstallationDisclaimer << + DownloadInstaller << + ExecuteInstaller << + RemoveInstaller; + } + } +} diff --git a/src/AppInstallerCLICore/Commands/UpgradeCommand.h b/src/AppInstallerCLICore/Commands/UpgradeCommand.h @@ -0,0 +1,25 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. +#pragma once +#include "Command.h" + +namespace AppInstaller::CLI +{ + struct UpgradeCommand final : public Command + { + UpgradeCommand(std::string_view parent) : Command("upgrade", parent, Settings::ExperimentalFeature::Feature::ExperimentalUpgrade) {} + + std::vector<Argument> GetArguments() const override; + + Resource::LocString ShortDescription() const override; + Resource::LocString LongDescription() const override; + + void Complete(Execution::Context& context, Execution::Args::Type valueType) const override; + + std::string HelpLink() const override; + + protected: + void ValidateArgumentsInternal(Execution::Args& execArgs) const override; + void ExecuteInternal(Execution::Context& context) const override; + }; +} diff --git a/src/AppInstallerCLICore/ExecutionArgs.h b/src/AppInstallerCLICore/ExecutionArgs.h @@ -56,6 +56,7 @@ namespace AppInstaller::CLI::Execution Position, // Other + All, // Used in Update command to update all installed packages to latest Force, // Generic flag to enable a command to skip some check ListVersions, // Used in Show command to list all available versions of an app NoVT, // Disable VirtualTerminal outputs @@ -110,6 +111,11 @@ namespace AppInstaller::CLI::Execution m_parsedArgs[arg].emplace_back(value); } + bool Empty() + { + return m_parsedArgs.empty(); + } + private: std::map<Type, std::vector<std::string>> m_parsedArgs; }; diff --git a/src/AppInstallerCLICore/ExecutionContext.cpp b/src/AppInstallerCLICore/ExecutionContext.cpp @@ -70,9 +70,11 @@ namespace AppInstaller::CLI::Execution } } - Context Context::Clone() + std::unique_ptr<Context> Context::Clone() { - return { Reporter }; + auto clone = std::make_unique<Context>(Reporter); + clone->GetFlags() = m_flags; + return clone; } void Context::EnableCtrlHandler(bool enabled) diff --git a/src/AppInstallerCLICore/ExecutionContext.h b/src/AppInstallerCLICore/ExecutionContext.h @@ -52,9 +52,19 @@ namespace AppInstaller::CLI::Execution LogPath, InstallerArgs, CompletionData, + InstalledPackageVersion, Max }; + // bit masks used as Context flags + enum class ContextFlag : int + { + None = 0x0, + InstallerExecutionUseUpdate = 0x1, + }; + + DEFINE_ENUM_FLAG_OPERATORS(ContextFlag); + namespace details { template <Data D> @@ -123,6 +133,12 @@ namespace AppInstaller::CLI::Execution using value_t = CLI::CompletionData; }; + template <> + struct DataMapping<Data::InstalledPackageVersion> + { + using value_t = std::shared_ptr<Repository::IPackageVersion>; + }; + // Used to deduce the DataVariant type; making a variant that includes std::monostate and all DataMapping types. template <size_t... I> inline auto Deduce(std::index_sequence<I...>) { return std::variant<std::monostate, DataMapping<static_cast<Data>(I)>::value_t...>{}; } @@ -141,6 +157,9 @@ namespace AppInstaller::CLI::Execution { Context(std::ostream& out, std::istream& in) : Reporter(out, in) {} + // Clone the reporter for this constructor. + Context(Execution::Reporter& reporter) : Reporter(reporter, Execution::Reporter::clone_t{}) {} + virtual ~Context(); // The path for console input/output for all functionality. @@ -150,7 +169,7 @@ namespace AppInstaller::CLI::Execution Args Args; // Creates a copy of this context as it was at construction. - Context Clone(); + virtual std::unique_ptr<Context> Clone(); // Enables reception of CTRL signals. // Only one context can be enabled to handle CTRL signals at a time. @@ -188,19 +207,23 @@ namespace AppInstaller::CLI::Execution return std::get<details::DataIndex(D)>(itr->second); } + // Gets context flags; which can be modified in place. + ContextFlag& GetFlags() + { + return m_flags; + } + #ifndef AICLI_DISABLE_TEST_HOOKS // Enable tests to override behavior virtual bool ShouldExecuteWorkflowTask(const Workflow::WorkflowTask&) { return true; } #endif private: - // Clone the reporter for this constructor. - Context(Execution::Reporter& reporter) : Reporter(reporter, Execution::Reporter::clone_t{}) {} - DestructionToken m_disableCtrlHandlerOnExit = false; bool m_isTerminated = false; HRESULT m_terminationHR = S_OK; std::map<Data, details::DataVariant> m_data; size_t m_CtrlSignalCount = 0; + ContextFlag m_flags = ContextFlag::None; }; } diff --git a/src/AppInstallerCLICore/Resources.h b/src/AppInstallerCLICore/Resources.h @@ -33,6 +33,7 @@ namespace AppInstaller::CLI::Resource WINGET_DEFINE_RESOURCE_STRINGID(AvailableCommands); WINGET_DEFINE_RESOURCE_STRINGID(AvailableOptions); WINGET_DEFINE_RESOURCE_STRINGID(AvailableSubcommands); + WINGET_DEFINE_RESOURCE_STRINGID(BothManifestAndSearchQueryProvided); WINGET_DEFINE_RESOURCE_STRINGID(ChannelArgumentDescription); WINGET_DEFINE_RESOURCE_STRINGID(Command); WINGET_DEFINE_RESOURCE_STRINGID(CommandArgumentDescription); @@ -59,6 +60,7 @@ namespace AppInstaller::CLI::Resource WINGET_DEFINE_RESOURCE_STRINGID(FeaturesStatus); WINGET_DEFINE_RESOURCE_STRINGID(FileArgumentDescription); WINGET_DEFINE_RESOURCE_STRINGID(FlagContainAdjoinedError); + WINGET_DEFINE_RESOURCE_STRINGID(GetManifestResultVersionNotFound); WINGET_DEFINE_RESOURCE_STRINGID(HashCommandLongDescription); WINGET_DEFINE_RESOURCE_STRINGID(HashCommandShortDescription); WINGET_DEFINE_RESOURCE_STRINGID(HelpArgumentDescription); @@ -97,20 +99,26 @@ namespace AppInstaller::CLI::Resource WINGET_DEFINE_RESOURCE_STRINGID(MonikerArgumentDescription); WINGET_DEFINE_RESOURCE_STRINGID(MsixArgumentDescription); WINGET_DEFINE_RESOURCE_STRINGID(MsixSignatureHashFailed); - WINGET_DEFINE_RESOURCE_STRINGID(MSStoreInstallAppBlocked); - WINGET_DEFINE_RESOURCE_STRINGID(MSStoreInstallFailed); + WINGET_DEFINE_RESOURCE_STRINGID(MSStoreAppBlocked); + WINGET_DEFINE_RESOURCE_STRINGID(MSStoreInstallOrUpdateFailed); WINGET_DEFINE_RESOURCE_STRINGID(MSStoreInstallGetEntitlementNetworkError); WINGET_DEFINE_RESOURCE_STRINGID(MSStoreInstallGetEntitlementNoStoreAccount); WINGET_DEFINE_RESOURCE_STRINGID(MSStoreInstallGetEntitlementServerError); WINGET_DEFINE_RESOURCE_STRINGID(MSStoreInstallGetEntitlementSuccess); - WINGET_DEFINE_RESOURCE_STRINGID(MSStoreInstallStoreClientBlocked); + WINGET_DEFINE_RESOURCE_STRINGID(MSStoreStoreClientBlocked); WINGET_DEFINE_RESOURCE_STRINGID(MSStoreInstallTryGetEntitlement); + WINGET_DEFINE_RESOURCE_STRINGID(MultipleInstalledPackagesFound); WINGET_DEFINE_RESOURCE_STRINGID(MultiplePackagesFound); WINGET_DEFINE_RESOURCE_STRINGID(NameArgumentDescription); WINGET_DEFINE_RESOURCE_STRINGID(NoApplicableInstallers); WINGET_DEFINE_RESOURCE_STRINGID(NoExperimentalFeaturesMessage); + WINGET_DEFINE_RESOURCE_STRINGID(NoInstalledPackageFound); WINGET_DEFINE_RESOURCE_STRINGID(NoPackageFound); WINGET_DEFINE_RESOURCE_STRINGID(NoVTArgumentDescription); + WINGET_DEFINE_RESOURCE_STRINGID(OpenSourceFailed); + WINGET_DEFINE_RESOURCE_STRINGID(OpenSourceFailedNoMatch); + WINGET_DEFINE_RESOURCE_STRINGID(OpenSourceFailedNoMatchHelp); + WINGET_DEFINE_RESOURCE_STRINGID(OpenSourceFailedNoSourceDefined); WINGET_DEFINE_RESOURCE_STRINGID(Options); WINGET_DEFINE_RESOURCE_STRINGID(OverrideArgumentDescription); WINGET_DEFINE_RESOURCE_STRINGID(Package); @@ -120,6 +128,7 @@ namespace AppInstaller::CLI::Resource WINGET_DEFINE_RESOURCE_STRINGID(PrivacyStatement); WINGET_DEFINE_RESOURCE_STRINGID(QueryArgumentDescription); WINGET_DEFINE_RESOURCE_STRINGID(RainbowArgumentDescription); + WINGET_DEFINE_RESOURCE_STRINGID(ReportIdentityFound); WINGET_DEFINE_RESOURCE_STRINGID(RequiredArgError); WINGET_DEFINE_RESOURCE_STRINGID(RetroArgumentDescription); WINGET_DEFINE_RESOURCE_STRINGID(SearchCommandLongDescription); @@ -188,11 +197,17 @@ namespace AppInstaller::CLI::Resource WINGET_DEFINE_RESOURCE_STRINGID(TooManyBehaviorsError); WINGET_DEFINE_RESOURCE_STRINGID(UnexpectedErrorExecutingCommand); WINGET_DEFINE_RESOURCE_STRINGID(UnrecognizedCommand); + WINGET_DEFINE_RESOURCE_STRINGID(UpdateAllArgumentDescription); + WINGET_DEFINE_RESOURCE_STRINGID(UpdateNotApplicable); + WINGET_DEFINE_RESOURCE_STRINGID(UpgradeCommandLongDescription); + WINGET_DEFINE_RESOURCE_STRINGID(UpgradeCommandShortDescription); WINGET_DEFINE_RESOURCE_STRINGID(Usage); WINGET_DEFINE_RESOURCE_STRINGID(ValidateCommandLongDescription); WINGET_DEFINE_RESOURCE_STRINGID(ValidateCommandShortDescription); WINGET_DEFINE_RESOURCE_STRINGID(ValidateManifestArgumentDescription); WINGET_DEFINE_RESOURCE_STRINGID(VerboseLogsArgumentDescription); + WINGET_DEFINE_RESOURCE_STRINGID(VerifyFileFailedIsDirectory); + WINGET_DEFINE_RESOURCE_STRINGID(VerifyFileFailedNotExist); WINGET_DEFINE_RESOURCE_STRINGID(VerifyFileSignedMsix); WINGET_DEFINE_RESOURCE_STRINGID(VersionArgumentDescription); WINGET_DEFINE_RESOURCE_STRINGID(VersionsArgumentDescription); diff --git a/src/AppInstallerCLICore/Workflows/InstallFlow.cpp b/src/AppInstallerCLICore/Workflows/InstallFlow.cpp @@ -4,6 +4,7 @@ #include "InstallFlow.h" #include "Resources.h" #include "ShellExecuteInstallerHandler.h" +#include "MSStoreInstallerHandler.h" #include "WorkflowBase.h" namespace AppInstaller::CLI::Workflow @@ -192,6 +193,8 @@ namespace AppInstaller::CLI::Workflow { const auto& installer = context.Get<Execution::Data::Installer>().value(); + bool isUpdate = WI_IsFlagSet(context.GetFlags(), Execution::ContextFlag::InstallerExecutionUseUpdate); + switch (installer.InstallerType) { case ManifestInstaller::InstallerTypeEnum::Exe: @@ -200,6 +203,12 @@ namespace AppInstaller::CLI::Workflow case ManifestInstaller::InstallerTypeEnum::Msi: case ManifestInstaller::InstallerTypeEnum::Nullsoft: case ManifestInstaller::InstallerTypeEnum::Wix: + if (isUpdate && installer.UpdateBehavior == ManifestInstaller::UpdateBehaviorEnum::UninstallPrevious) + { + // TODO: hook up with uninstall when uninstall is implemented + WI_ClearFlag(context.GetFlags(), Execution::ContextFlag::InstallerExecutionUseUpdate); + AICLI_TERMINATE_CONTEXT(HRESULT_FROM_WIN32(ERROR_NOT_SUPPORTED)); + } context << ShellExecuteInstall; break; case ManifestInstaller::InstallerTypeEnum::Msix: @@ -208,7 +217,8 @@ namespace AppInstaller::CLI::Workflow case ManifestInstaller::InstallerTypeEnum::MSStore: context << EnsureFeatureEnabled(Settings::ExperimentalFeature::Feature::ExperimentalMSStore) << - MSStoreInstall; + EnsureStorePolicySatisfied << + (isUpdate ? MSStoreUpdate : MSStoreInstall); break; default: THROW_HR(HRESULT_FROM_WIN32(ERROR_NOT_SUPPORTED)); @@ -266,133 +276,4 @@ namespace AppInstaller::CLI::Workflow std::filesystem::remove(path); } } - - void MSStoreInstall(Execution::Context& context) - { - auto productId = Utility::ConvertToUTF16(context.Get<Execution::Data::Installer>()->ProductId); - - constexpr std::wstring_view s_StoreClientName = L"Microsoft.WindowsStore"sv; - constexpr std::wstring_view s_StoreClientPublisher = L"CN=Microsoft Corporation, O=Microsoft Corporation, L=Redmond, S=Washington, C=US"sv; - - // Policy check - AppInstallManager installManager; - if (installManager.IsStoreBlockedByPolicyAsync(s_StoreClientName, s_StoreClientPublisher).get()) - { - context.Reporter.Error() << Resource::String::MSStoreInstallStoreClientBlocked << std::endl; - AICLI_LOG(CLI, Error, << "Store client is blocked by policy. MSStore install failed."); - AICLI_TERMINATE_CONTEXT(APPINSTALLER_CLI_ERROR_MSSTORE_BLOCKED_BY_POLICY); - } - - if (!installManager.GetIsAppAllowedToInstallAsync(productId).get()) - { - context.Reporter.Error() << Resource::String::MSStoreInstallAppBlocked << std::endl; - AICLI_LOG(CLI, Error, << "App is blocked by policy. MSStore install failed. ProductId: " << Utility::ConvertToUTF8(productId)); - AICLI_TERMINATE_CONTEXT(APPINSTALLER_CLI_ERROR_MSSTORE_APP_BLOCKED_BY_POLICY); - } - - // Verifying/Acquiring product ownership - context.Reporter.Info() << Resource::String::MSStoreInstallTryGetEntitlement << std::endl; - GetEntitlementResult enr = installManager.GetFreeUserEntitlementAsync(productId, winrt::hstring(), winrt::hstring()).get(); - - if (enr.Status() == GetEntitlementStatus::Succeeded) - { - context.Reporter.Info() << Resource::String::MSStoreInstallGetEntitlementSuccess << std::endl; - AICLI_LOG(CLI, Error, << "Get entitlement succeeded."); - } - else - { - if (enr.Status() == GetEntitlementStatus::NoStoreAccount) - { - context.Reporter.Info() << Resource::String::MSStoreInstallGetEntitlementNoStoreAccount << std::endl; - AICLI_LOG(CLI, Error, << "Get entitlement failed. No Store account."); - } - else if (enr.Status() == GetEntitlementStatus::NetworkError) - { - context.Reporter.Info() << Resource::String::MSStoreInstallGetEntitlementNetworkError << std::endl; - AICLI_LOG(CLI, Error, << "Get entitlement failed. Network error."); - } - else if (enr.Status() == GetEntitlementStatus::ServerError) - { - context.Reporter.Info() << Resource::String::MSStoreInstallGetEntitlementServerError << std::endl; - AICLI_LOG(CLI, Error, << "Get entitlement succeeded. Server error. ProductId: " << Utility::ConvertToUTF8(productId)); - } - - AICLI_TERMINATE_CONTEXT(APPINSTALLER_CLI_ERROR_MSSTORE_INSTALL_FAILED); - } - - context.Reporter.Info() << Resource::String::InstallFlowStartingPackageInstall << std::endl; - - IVectorView<AppInstallItem> installItems = installManager.StartProductInstallAsync( - productId, // ProductId - winrt::hstring(), // CatalogId - winrt::hstring(), // FlightId - L"WinGetCli", // ClientId - false, // repair - false, - winrt::hstring(), - nullptr).get(); - - for (auto const& installItem : installItems) - { - AICLI_LOG(CLI, Info, << - "Started MSStore package installation. ProductId: " << Utility::ConvertToUTF8(installItem.ProductId()) << - " PackageFamilyName: " << Utility::ConvertToUTF8(installItem.PackageFamilyName())); - } - - HRESULT errorCode = S_OK; - context.Reporter.ExecuteWithProgress( - [&](IProgressCallback& progress) - { - // We are aggregating all AppInstallItem progresses into one. - // Averaging every progress for now until we have a better way to find overall progress. - uint64_t overallProgressMax = 100 * installItems.Size(); - uint64_t currentProgress = 0; - - while (currentProgress < overallProgressMax) - { - currentProgress = 0; - - for (auto const& installItem : installItems) - { - const auto& status = installItem.GetCurrentStatus(); - currentProgress += static_cast<uint64_t>(status.PercentComplete()); - - errorCode = status.ErrorCode(); - - if (!SUCCEEDED(errorCode)) - { - return; - } - } - - // It may take a while for Store client to pick up the install request. - // So we show indefinite progress here to avoid a progress bar stuck at 0. - if (currentProgress > 0) - { - progress.OnProgress(currentProgress, overallProgressMax, ProgressType::Percent); - } - - if (progress.IsCancelled()) - { - for (auto const& installItem : installItems) - { - installItem.Cancel(); - } - } - - Sleep(100); - } - }); - - if (SUCCEEDED(errorCode)) - { - context.Reporter.Info() << Resource::String::InstallFlowInstallSuccess << std::endl; - } - else - { - context.Reporter.Info() << Resource::String::MSStoreInstallFailed << ' ' << WINGET_OSTREAM_FORMAT_HRESULT(errorCode) << std::endl; - AICLI_LOG(CLI, Error, << "MSStore install failed. ProductId: " << Utility::ConvertToUTF8(productId) << " HResult: " << WINGET_OSTREAM_FORMAT_HRESULT(errorCode)); - AICLI_TERMINATE_CONTEXT(errorCode); - } - } } diff --git a/src/AppInstallerCLICore/Workflows/InstallFlow.h b/src/AppInstallerCLICore/Workflows/InstallFlow.h @@ -71,12 +71,6 @@ namespace AppInstaller::CLI::Workflow // Outputs: None void MsixInstall(Execution::Context& context); - // Deploys the Store app. - // Required Args: None - // Inputs: Manifest?, Installer - // Outputs: None - void MSStoreInstall(Execution::Context& context); - // Deletes the installer file. // Required Args: None // Inputs: InstallerPath diff --git a/src/AppInstallerCLICore/Workflows/MSStoreInstallerHandler.cpp b/src/AppInstallerCLICore/Workflows/MSStoreInstallerHandler.cpp @@ -0,0 +1,196 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. +#include "pch.h" +#include "MSStoreInstallerHandler.h" + + +namespace AppInstaller::CLI::Workflow +{ + using namespace std::string_view_literals; + using namespace winrt::Windows::Foundation; + using namespace winrt::Windows::Foundation::Collections; + using namespace winrt::Windows::ApplicationModel::Store::Preview::InstallControl; + + namespace + { + HRESULT WaitForMSStoreOperation(Execution::Context& context, IVectorView<AppInstallItem>& installItems) + { + for (auto const& installItem : installItems) + { + AICLI_LOG(CLI, Info, << + "Started MSStore package execution. ProductId: " << Utility::ConvertToUTF8(installItem.ProductId()) << + " PackageFamilyName: " << Utility::ConvertToUTF8(installItem.PackageFamilyName())); + } + + HRESULT errorCode = S_OK; + context.Reporter.ExecuteWithProgress( + [&](IProgressCallback& progress) + { + // We are aggregating all AppInstallItem progresses into one. + // Averaging every progress for now until we have a better way to find overall progress. + uint64_t overallProgressMax = 100 * installItems.Size(); + uint64_t currentProgress = 0; + + while (currentProgress < overallProgressMax) + { + currentProgress = 0; + + for (auto const& installItem : installItems) + { + const auto& status = installItem.GetCurrentStatus(); + currentProgress += static_cast<uint64_t>(status.PercentComplete()); + + errorCode = status.ErrorCode(); + + if (!SUCCEEDED(errorCode)) + { + return; + } + } + + // It may take a while for Store client to pick up the install request. + // So we show indefinite progress here to avoid a progress bar stuck at 0. + if (currentProgress > 0) + { + progress.OnProgress(currentProgress, overallProgressMax, ProgressType::Percent); + } + + if (progress.IsCancelled()) + { + for (auto const& installItem : installItems) + { + installItem.Cancel(); + } + } + + Sleep(100); + } + }); + + return errorCode; + } + } + + void MSStoreInstall(Execution::Context& context) + { + auto productId = Utility::ConvertToUTF16(context.Get<Execution::Data::Installer>()->ProductId); + + AppInstallManager installManager; + + // Verifying/Acquiring product ownership + context.Reporter.Info() << Resource::String::MSStoreInstallTryGetEntitlement << std::endl; + GetEntitlementResult enr = installManager.GetFreeUserEntitlementAsync(productId, winrt::hstring(), winrt::hstring()).get(); + + if (enr.Status() == GetEntitlementStatus::Succeeded) + { + context.Reporter.Info() << Resource::String::MSStoreInstallGetEntitlementSuccess << std::endl; + AICLI_LOG(CLI, Error, << "Get entitlement succeeded."); + } + else + { + if (enr.Status() == GetEntitlementStatus::NoStoreAccount) + { + context.Reporter.Info() << Resource::String::MSStoreInstallGetEntitlementNoStoreAccount << std::endl; + AICLI_LOG(CLI, Error, << "Get entitlement failed. No Store account."); + } + else if (enr.Status() == GetEntitlementStatus::NetworkError) + { + context.Reporter.Info() << Resource::String::MSStoreInstallGetEntitlementNetworkError << std::endl; + AICLI_LOG(CLI, Error, << "Get entitlement failed. Network error."); + } + else if (enr.Status() == GetEntitlementStatus::ServerError) + { + context.Reporter.Info() << Resource::String::MSStoreInstallGetEntitlementServerError << std::endl; + AICLI_LOG(CLI, Error, << "Get entitlement succeeded. Server error. ProductId: " << Utility::ConvertToUTF8(productId)); + } + + AICLI_TERMINATE_CONTEXT(APPINSTALLER_CLI_ERROR_MSSTORE_INSTALL_FAILED); + } + + context.Reporter.Info() << Resource::String::InstallFlowStartingPackageInstall << std::endl; + + IVectorView<AppInstallItem> installItems = installManager.StartProductInstallAsync( + productId, // ProductId + winrt::hstring(), // CatalogId + winrt::hstring(), // FlightId + L"WinGetCli", // ClientId + false, // repair + false, + winrt::hstring(), + nullptr).get(); + + HRESULT errorCode = WaitForMSStoreOperation(context, installItems); + + if (SUCCEEDED(errorCode)) + { + context.Reporter.Info() << Resource::String::InstallFlowInstallSuccess << std::endl; + } + else + { + context.Reporter.Info() << Resource::String::MSStoreInstallOrUpdateFailed << ' ' << WINGET_OSTREAM_FORMAT_HRESULT(errorCode) << std::endl; + AICLI_LOG(CLI, Error, << "MSStore install failed. ProductId: " << Utility::ConvertToUTF8(productId) << " HResult: " << WINGET_OSTREAM_FORMAT_HRESULT(errorCode)); + AICLI_TERMINATE_CONTEXT(errorCode); + } + } + + void MSStoreUpdate(Execution::Context& context) + { + auto productId = Utility::ConvertToUTF16(context.Get<Execution::Data::Installer>()->ProductId); + + AppInstallManager installManager; + + context.Reporter.Info() << Resource::String::InstallFlowStartingPackageInstall << std::endl; + + // SearchForUpdateAsync will automatically trigger update if found. + AppInstallItem installItem = installManager.SearchForUpdatesAsync( + productId, // ProductId + winrt::hstring() // SkuId + ).get(); + + if (!installItem) + { + context.Reporter.Info() << Resource::String::UpdateNotApplicable << std::endl; + AICLI_TERMINATE_CONTEXT(APPINSTALLER_CLI_ERROR_UPDATE_NOT_APPLICABLE); + } + + std::vector<AppInstallItem> installItemVector{ installItem }; + IVectorView<AppInstallItem> installItems = winrt::single_threaded_vector(std::move(installItemVector)).GetView(); + + HRESULT errorCode = WaitForMSStoreOperation(context, installItems); + + if (SUCCEEDED(errorCode)) + { + context.Reporter.Info() << Resource::String::InstallFlowInstallSuccess << std::endl; + } + else + { + context.Reporter.Info() << Resource::String::MSStoreInstallOrUpdateFailed << ' ' << WINGET_OSTREAM_FORMAT_HRESULT(errorCode) << std::endl; + AICLI_LOG(CLI, Error, << "MSStore execution failed. ProductId: " << Utility::ConvertToUTF8(productId) << " HResult: " << WINGET_OSTREAM_FORMAT_HRESULT(errorCode)); + AICLI_TERMINATE_CONTEXT(errorCode); + } + } + + void EnsureStorePolicySatisfied(Execution::Context& context) + { + auto productId = Utility::ConvertToUTF16(context.Get<Execution::Data::Installer>()->ProductId); + + constexpr std::wstring_view s_StoreClientName = L"Microsoft.WindowsStore"sv; + constexpr std::wstring_view s_StoreClientPublisher = L"CN=Microsoft Corporation, O=Microsoft Corporation, L=Redmond, S=Washington, C=US"sv; + + // Policy check + AppInstallManager installManager; + if (installManager.IsStoreBlockedByPolicyAsync(s_StoreClientName, s_StoreClientPublisher).get()) + { + context.Reporter.Error() << Resource::String::MSStoreStoreClientBlocked << std::endl; + AICLI_LOG(CLI, Error, << "Store client is blocked by policy. MSStore execution failed."); + AICLI_TERMINATE_CONTEXT(APPINSTALLER_CLI_ERROR_MSSTORE_BLOCKED_BY_POLICY); + } + + if (!installManager.GetIsAppAllowedToInstallAsync(productId).get()) + { + context.Reporter.Error() << Resource::String::MSStoreAppBlocked << std::endl; + AICLI_LOG(CLI, Error, << "App is blocked by policy. MSStore execution failed. ProductId: " << Utility::ConvertToUTF8(productId)); + AICLI_TERMINATE_CONTEXT(APPINSTALLER_CLI_ERROR_MSSTORE_APP_BLOCKED_BY_POLICY); + } + } +}+ \ No newline at end of file diff --git a/src/AppInstallerCLICore/Workflows/MSStoreInstallerHandler.h b/src/AppInstallerCLICore/Workflows/MSStoreInstallerHandler.h @@ -0,0 +1,28 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. +#pragma once +#include "ExecutionContext.h" + + +// ShellExecuteInstallerHandler handles installers run through ShellExecute. +// Exe, Wix, Nullsoft, Msi and Inno should be handled by this installer handler. +namespace AppInstaller::CLI::Workflow +{ + // Deploys the Store app. + // Required Args: None + // Inputs: Installer + // Outputs: None + void MSStoreInstall(Execution::Context& context); + + // Updates the Store app if applicable. + // Required Args: None + // Inputs: Installer + // Outputs: None + void MSStoreUpdate(Execution::Context& context); + + // Ensure the Store app is not blocked by policy. + // Required Args: None + // Inputs: Installer + // Outputs: None + void EnsureStorePolicySatisfied(Execution::Context& context); +}+ \ No newline at end of file diff --git a/src/AppInstallerCLICore/Workflows/ManifestComparator.cpp b/src/AppInstallerCLICore/Workflows/ManifestComparator.cpp @@ -1,6 +1,7 @@ // Copyright (c) Microsoft Corporation. // Licensed under the MIT License. #include "pch.h" +#include "WorkflowBase.h" #include "ManifestComparator.h" using namespace AppInstaller::CLI; @@ -10,6 +11,29 @@ namespace AppInstaller::CLI::Workflow { bool InstallerComparator::operator() (const ManifestInstaller& installer1, const ManifestInstaller& installer2) { + // Applicable architecture should always come before inapplicable architecture + if (Utility::IsApplicableArchitecture(installer1.Arch) != Utility::InapplicableArchitecture && + Utility::IsApplicableArchitecture(installer2.Arch) == Utility::InapplicableArchitecture) + { + return true; + } + + // If there's installation metadata, pick the preferred one or compatible one + auto installerTypeItr = m_installationMetadata.find(s_InstallationMetadata_Key_InstallerType); + if (installerTypeItr != m_installationMetadata.end()) + { + auto installerType = Manifest::ManifestInstaller::ConvertToInstallerTypeEnum(installerTypeItr->second); + if (installer1.InstallerType == installerType && installer2.InstallerType != installerType) + { + return true; + } + if (Manifest::ManifestInstaller::IsInstallerTypeCompatible(installer1.InstallerType, installerType) && + !Manifest::ManifestInstaller::IsInstallerTypeCompatible(installer2.InstallerType, installerType)) + { + return true; + } + } + // Todo: Compare only architecture for now. Need more work and spec. if (Utility::IsApplicableArchitecture(installer1.Arch) > Utility::IsApplicableArchitecture(installer2.Arch)) { @@ -41,14 +65,25 @@ namespace AppInstaller::CLI::Workflow // Sorting the list of available installers according to rules defined in InstallerComparator. auto installers = manifest.Installers; - std::sort(installers.begin(), installers.end(), InstallerComparator()); + std::sort(installers.begin(), installers.end(), m_installerComparator); - // If the first one is inapplicable, then no installer is applicable. - if (Utility::IsApplicableArchitecture(installers[0].Arch) == -1) + // If the first one's architecture is inapplicable, then no installer is applicable. + if (Utility::IsApplicableArchitecture(installers[0].Arch) == Utility::InapplicableArchitecture) { return {}; } + // If the first one's InstallerType is inapplicable, then no installer is applicable. + auto installerTypeItr = m_installationMetadata.find(s_InstallationMetadata_Key_InstallerType); + if (installerTypeItr != m_installationMetadata.end()) + { + auto installerType = Manifest::ManifestInstaller::ConvertToInstallerTypeEnum(installerTypeItr->second); + if (!Manifest::ManifestInstaller::IsInstallerTypeCompatible(installers[0].InstallerType, installerType)) + { + return {}; + } + } + ManifestInstaller& selectedInstaller = installers[0]; Logging::Telemetry().LogSelectedInstaller((int)selectedInstaller.Arch, selectedInstaller.Url, Manifest::ManifestInstaller::InstallerTypeToString(selectedInstaller.InstallerType), selectedInstaller.Scope, selectedInstaller.Language); @@ -66,7 +101,7 @@ namespace AppInstaller::CLI::Workflow if (!manifest.Localization.empty()) { auto localization = manifest.Localization; - std::sort(localization.begin(), localization.end(), LocalizationComparator()); + std::sort(localization.begin(), localization.end(), m_localizationComparator); // TODO: needs to check language applicability here diff --git a/src/AppInstallerCLICore/Workflows/ManifestComparator.h b/src/AppInstallerCLICore/Workflows/ManifestComparator.h @@ -12,9 +12,15 @@ namespace AppInstaller::CLI::Workflow // This is used in sorting the list of available installers to get the best match. struct InstallerComparator { + InstallerComparator(const std::map<std::string, std::string>& installationMetadata) : + m_installationMetadata(installationMetadata) {} + bool operator() ( const Manifest::ManifestInstaller& installer1, const Manifest::ManifestInstaller& installer2); + + private: + const std::map<std::string, std::string>& m_installationMetadata; }; // This is used in sorting the list of available localizations to get the best match. @@ -26,16 +32,19 @@ namespace AppInstaller::CLI::Workflow }; // Class in charge of comparing manifest entries - class ManifestComparator + struct ManifestComparator { - public: - ManifestComparator(const Execution::Args&) {} + ManifestComparator(const Execution::Args&, const std::map<std::string, std::string>& installationMetadata = {}) : + m_installationMetadata(installationMetadata), m_installerComparator(installationMetadata) {} std::optional<Manifest::ManifestInstaller> GetPreferredInstaller(const Manifest::Manifest& manifest); Manifest::ManifestLocalization GetPreferredLocalization(const Manifest::Manifest& manifest); private: // TODO: Handle args to change how we select. + const std::map<std::string, std::string>& m_installationMetadata; + LocalizationComparator m_localizationComparator; + InstallerComparator m_installerComparator; }; } \ No newline at end of file diff --git a/src/AppInstallerCLICore/Workflows/ShellExecuteInstallerHandler.cpp b/src/AppInstallerCLICore/Workflows/ShellExecuteInstallerHandler.cpp @@ -61,34 +61,37 @@ namespace AppInstaller::CLI::Workflow // Gets the escaped installer args. std::string GetInstallerArgsTemplate(Execution::Context& context) { - std::string installerArgs = ""; - const std::map<ManifestInstaller::InstallerSwitchType, Utility::NormalizedString>& installerSwitches = context.Get<Execution::Data::Installer>()->Switches; + bool isUpdate = WI_IsFlagSet(context.GetFlags(), Execution::ContextFlag::InstallerExecutionUseUpdate); + + const auto& installer = context.Get<Execution::Data::Installer>(); + const auto& installerSwitches = installer->Switches; + std::string installerArgs = {}; // Construct install experience arg. // SilentWithProgress is default, so look for it first. - auto argsItr = installerSwitches.find(ManifestInstaller::InstallerSwitchType::SilentWithProgress); + auto experienceArgsItr = installerSwitches.find(ManifestInstaller::InstallerSwitchType::SilentWithProgress); if (context.Args.Contains(Execution::Args::Type::Interactive)) { // If interacive requested, always use Interactive (or nothing). If the installer supports // interactive it is usually the default, and thus it is cumbersome to put a blank entry in // the manifest. - argsItr = installerSwitches.find(ManifestInstaller::InstallerSwitchType::Interactive); + experienceArgsItr = installerSwitches.find(ManifestInstaller::InstallerSwitchType::Interactive); } // If no SilentWithProgress exists, or Silent requested, try to find Silent. - else if (argsItr == installerSwitches.end() || context.Args.Contains(Execution::Args::Type::Silent)) + else if (experienceArgsItr == installerSwitches.end() || context.Args.Contains(Execution::Args::Type::Silent)) { auto silentItr = installerSwitches.find(ManifestInstaller::InstallerSwitchType::Silent); // If Silent requested, but doesn't exist, then continue using SilentWithProgress. if (silentItr != installerSwitches.end()) { - argsItr = silentItr; + experienceArgsItr = silentItr; } } - if (argsItr != installerSwitches.end()) + if (experienceArgsItr != installerSwitches.end()) { - installerArgs += argsItr->second; + installerArgs += experienceArgsItr->second; } // Construct language arg if necessary. @@ -97,12 +100,6 @@ namespace AppInstaller::CLI::Workflow installerArgs += ' ' + installerSwitches.at(ManifestInstaller::InstallerSwitchType::Language); } - // Construct install location arg if necessary. - if (context.Args.Contains(Execution::Args::Type::InstallLocation) && installerSwitches.find(ManifestInstaller::InstallerSwitchType::InstallLocation) != installerSwitches.end()) - { - installerArgs += ' ' + installerSwitches.at(ManifestInstaller::InstallerSwitchType::InstallLocation); - } - // Construct log path arg. if (installerSwitches.find(ManifestInstaller::InstallerSwitchType::Log) != installerSwitches.end()) { @@ -115,6 +112,20 @@ namespace AppInstaller::CLI::Workflow installerArgs += ' ' + installerSwitches.at(ManifestInstaller::InstallerSwitchType::Custom); } + // Construct update arg if applicable + if (isUpdate && installerSwitches.find(ManifestInstaller::InstallerSwitchType::Update) != installerSwitches.end()) + { + installerArgs += ' ' + installerSwitches.at(ManifestInstaller::InstallerSwitchType::Update); + } + + // Construct install location arg if necessary. + if (!isUpdate && + context.Args.Contains(Execution::Args::Type::InstallLocation) && + installerSwitches.find(ManifestInstaller::InstallerSwitchType::InstallLocation) != installerSwitches.end()) + { + installerArgs += ' ' + installerSwitches.at(ManifestInstaller::InstallerSwitchType::InstallLocation); + } + return installerArgs; } diff --git a/src/AppInstallerCLICore/Workflows/UpdateFlow.cpp b/src/AppInstallerCLICore/Workflows/UpdateFlow.cpp @@ -0,0 +1,134 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +#include "pch.h" +#include "WorkflowBase.h" +#include "InstallFlow.h" +#include "UpdateFlow.h" +#include "ManifestComparator.h" + +using namespace AppInstaller::Repository; + +namespace AppInstaller::CLI::Workflow +{ + namespace + { + bool IsUpdateVersionApplicable(Execution::Context& context, const Utility::Version& updateVersion) + { + const auto& installedPackage = context.Get<Execution::Data::InstalledPackageVersion>(); + const auto& installedVersion = Utility::Version(installedPackage->GetProperty(PackageVersionProperty::Version)); + + bool updateApplicable = false; + if (updateVersion > installedVersion) + { + updateApplicable = true; + } + else if (updateVersion == installedVersion) + { + // If installer type is MSStore, we'll let Store api to handle updates later + const auto& installationMetadata = installedPackage->GetInstallationMetadata(); + auto installerTypeItr = installationMetadata.find(s_InstallationMetadata_Key_InstallerType); + if (installerTypeItr != installationMetadata.end() && + Manifest::ManifestInstaller::InstallerTypeEnum::MSStore == Manifest::ManifestInstaller::ConvertToInstallerTypeEnum(installerTypeItr->second)) + { + updateApplicable = true; + } + } + + return updateApplicable; + } + } + + void SelectLatestApplicableUpdate::operator()(Execution::Context& context) const + { + const auto& installationMetadata = context.Get<Execution::Data::InstalledPackageVersion>()->GetInstallationMetadata(); + ManifestComparator manifestComparator(context.Args, installationMetadata); + bool updateFound = false; + + // The version keys should have already been sorted by version + const auto& versionKeys = m_package.GetAvailableVersionKeys(); + for (const auto& key : versionKeys) + { + // Check Update Version + if (IsUpdateVersionApplicable(context, Utility::Version(key.Version))) + { + auto manifest = m_package.GetAvailableVersion(key)->GetManifest(); + + // Check MinOSVersion + if (!manifest.MinOSVersion.empty() && + !Runtime::IsCurrentOSVersionGreaterThanOrEqual(Utility::Version(manifest.MinOSVersion))) + { + continue; + } + + // Check applicable Installer + auto installer = manifestComparator.GetPreferredInstaller(manifest); + if (!installer.has_value()) + { + continue; + } + + // Since we already did installer selection, just populate the context Data + context.Add<Execution::Data::Manifest>(std::move(manifest)); + context.Add<Execution::Data::Installer>(std::move(installer)); + + updateFound = true; + } + else + { + // Any following versions are not applicable + break; + } + } + + if (!updateFound) + { + context.Reporter.Info() << Resource::String::UpdateNotApplicable << std::endl; + AICLI_TERMINATE_CONTEXT(APPINSTALLER_CLI_ERROR_UPDATE_NOT_APPLICABLE); + } + } + + void EnsureUpdateVersionApplicable(Execution::Context& context) + { + Utility::Version updateVersion(context.Get<Execution::Data::Manifest>().Version); + + if (!IsUpdateVersionApplicable(context, updateVersion)) + { + context.Reporter.Info() << Resource::String::UpdateNotApplicable << std::endl; + AICLI_TERMINATE_CONTEXT(APPINSTALLER_CLI_ERROR_UPDATE_NOT_APPLICABLE); + } + } + + void UpdateAllApplicable(Execution::Context& context) + { + const auto& matches = context.Get<Execution::Data::SearchResult>().Matches; + bool updateAllHasFailure = false; + for (const auto& match : matches) + { + // We want to do best effort to update all applicable updates regardless on previous update failure + auto updateContextPtr = context.Clone(); + Execution::Context& updateContext = *updateContextPtr; + updateContext.Reporter.Info() << std::endl; + + updateContext.Add<Execution::Data::InstalledPackageVersion>(match.Package->GetInstalledVersion()); + + updateContext << + SelectLatestApplicableUpdate(*(match.Package)) << + ShowInstallationDisclaimer << + DownloadInstaller << + ExecuteInstaller << + RemoveInstaller; + + if (updateContext.GetTerminationHR() != S_OK && + updateContext.GetTerminationHR() != APPINSTALLER_CLI_ERROR_UPDATE_NOT_APPLICABLE) + { + updateAllHasFailure = true; + } + } + + if (updateAllHasFailure) + { + AICLI_TERMINATE_CONTEXT(APPINSTALLER_CLI_ERROR_UPDATE_ALL_HAS_FAILURE); + } + } +}+ \ No newline at end of file diff --git a/src/AppInstallerCLICore/Workflows/UpdateFlow.h b/src/AppInstallerCLICore/Workflows/UpdateFlow.h @@ -0,0 +1,35 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. +#pragma once +#include "ExecutionContext.h" +#include "WorkflowBase.h" + +namespace AppInstaller::CLI::Workflow +{ + // Iterates through all available versions from a package and find latest applicable update + // Required Args: the package + // Inputs: InstalledPackageVersion + // Outputs: Manifest?, Installer? + struct SelectLatestApplicableUpdate : public WorkflowTask + { + SelectLatestApplicableUpdate(const AppInstaller::Repository::IPackage& package) : + WorkflowTask("SelectLatestApplicableUpdate"), m_package(package) {} + + void operator()(Execution::Context& context) const override; + + private: + const AppInstaller::Repository::IPackage& m_package; + }; + + // Ensures the update package has higher version than installed + // Required Args: None + // Inputs: Manifest, InstalledPackageVersion + // Outputs: None + void EnsureUpdateVersionApplicable(Execution::Context& context); + + // Update all packages from SearchResult to latest if applicable + // Required Args: None + // Inputs: SearchResult + // Outputs: None + void UpdateAllApplicable(Execution::Context& context); +}+ \ No newline at end of file diff --git a/src/AppInstallerCLICore/Workflows/WorkflowBase.cpp b/src/AppInstallerCLICore/Workflows/WorkflowBase.cpp @@ -31,7 +31,7 @@ namespace AppInstaller::CLI::Workflow void ReportIdentity(Execution::Context& context, std::string_view name, std::string_view id) { - context.Reporter.Info() << "Found " << Execution::NameEmphasis << name << " [" << Execution::IdEmphasis << id << ']' << std::endl; + context.Reporter.Info() << Resource::String::ReportIdentityFound << ' ' << Execution::NameEmphasis << name << " [" << Execution::IdEmphasis << id << ']' << std::endl; } void SearchSourceApplyFilters(Execution::Context& context, SearchRequest& searchRequest, MatchType matchType) @@ -107,7 +107,7 @@ namespace AppInstaller::CLI::Workflow } catch (...) { - context.Reporter.Error() << "Failed to open the source; try removing and re-adding it" << std::endl; + context.Reporter.Error() << Resource::String::OpenSourceFailed << std::endl; throw; } @@ -118,8 +118,8 @@ namespace AppInstaller::CLI::Workflow if (context.Args.Contains(Execution::Args::Type::Source) && !sources.empty()) { // A bad name was given, try to help. - context.Reporter.Error() << "No sources match the given value: " << sourceName << std::endl; - context.Reporter.Info() << "The configured sources are:" << std::endl; + context.Reporter.Error() << Resource::String::OpenSourceFailedNoMatch << ' ' << context.Args.GetArg(Execution::Args::Type::Source) << std::endl; + context.Reporter.Info() << Resource::String::OpenSourceFailedNoMatchHelp << std::endl; for (const auto& details : sources) { context.Reporter.Info() << " " << details.Name << std::endl; @@ -130,10 +130,24 @@ namespace AppInstaller::CLI::Workflow else { // Even if a name was given, there are no sources - context.Reporter.Error() << "No sources defined; add one with 'source add' or reset to defaults with 'source reset'" << std::endl; + context.Reporter.Error() << Resource::String::OpenSourceFailedNoSourceDefined << std::endl; AICLI_TERMINATE_CONTEXT(APPINSTALLER_CLI_ERROR_NO_SOURCES_DEFINED); } } + else + { + context.Add<Execution::Data::Source>(std::move(source)); + } + } + + void GetCompositeSourceFromInstalledAndAvailable(Execution::Context& context) + { + std::shared_ptr<Repository::ISource> availableSource = context.Get<Execution::Data::Source>(); + + std::shared_ptr<Repository::ISource> installedSource = context.Reporter.ExecuteWithProgress( + std::bind(Repository::OpenPredefinedSource, PredefinedSource::Installed, std::placeholders::_1), true); + + std::shared_ptr<Repository::ISource> source = CreateCompositeSource(installedSource, availableSource); context.Add<Execution::Data::Source>(std::move(source)); } @@ -281,7 +295,16 @@ namespace AppInstaller::CLI::Workflow if (searchResult.Matches.size() == 0) { Logging::Telemetry().LogNoAppMatch(); - context.Reporter.Info() << Resource::String::NoPackageFound << std::endl; + + if (WI_IsFlagSet(context.GetFlags(), Execution::ContextFlag::InstallerExecutionUseUpdate)) + { + context.Reporter.Info() << Resource::String::NoInstalledPackageFound << std::endl; + } + else + { + context.Reporter.Info() << Resource::String::NoPackageFound << std::endl; + } + AICLI_TERMINATE_CONTEXT(APPINSTALLER_CLI_ERROR_NO_APPLICATIONS_FOUND); } } @@ -297,7 +320,16 @@ namespace AppInstaller::CLI::Workflow if (searchResult.Matches.size() > 1) { Logging::Telemetry().LogMultiAppMatch(); - context.Reporter.Warn() << Resource::String::MultiplePackagesFound << std::endl; + + if (WI_IsFlagSet(context.GetFlags(), Execution::ContextFlag::InstallerExecutionUseUpdate)) + { + context.Reporter.Warn() << Resource::String::MultipleInstalledPackagesFound << std::endl; + } + else + { + context.Reporter.Warn() << Resource::String::MultiplePackagesFound << std::endl; + } + context << ReportSearchResult; AICLI_TERMINATE_CONTEXT(APPINSTALLER_CLI_ERROR_MULTIPLE_APPLICATIONS_FOUND); } @@ -323,7 +355,7 @@ namespace AppInstaller::CLI::Workflow if (!manifest) { - context.Reporter.Error() << "No version found matching: "; + context.Reporter.Error() << Resource::String::GetManifestResultVersionNotFound << ' '; if (!version.empty()) { context.Reporter.Error() << version; @@ -347,13 +379,13 @@ namespace AppInstaller::CLI::Workflow if (!std::filesystem::exists(path)) { - context.Reporter.Error() << "File does not exist: " << path.u8string() << std::endl; + context.Reporter.Error() << Resource::String::VerifyFileFailedNotExist << ' ' << path.u8string() << std::endl; AICLI_TERMINATE_CONTEXT(HRESULT_FROM_WIN32(ERROR_FILE_NOT_FOUND)); } if (std::filesystem::is_directory(path)) { - context.Reporter.Error() << "Path is a directory: " << path.u8string() << std::endl; + context.Reporter.Error() << Resource::String::VerifyFileFailedIsDirectory << ' ' << path.u8string() << std::endl; AICLI_TERMINATE_CONTEXT(HRESULT_FROM_WIN32(ERROR_DIRECTORY_NOT_SUPPORTED)); } } @@ -403,7 +435,15 @@ namespace AppInstaller::CLI::Workflow void SelectInstaller(Execution::Context& context) { - ManifestComparator manifestComparator(context.Args); + bool isUpdate = WI_IsFlagSet(context.GetFlags(), Execution::ContextFlag::InstallerExecutionUseUpdate); + + std::map<std::string, std::string> installationMetadata; + if (isUpdate) + { + installationMetadata = context.Get<Execution::Data::InstalledPackageVersion>()->GetInstallationMetadata(); + } + + ManifestComparator manifestComparator(context.Args, installationMetadata); context.Add<Execution::Data::Installer>(manifestComparator.GetPreferredInstaller(context.Get<Execution::Data::Manifest>())); } @@ -426,6 +466,51 @@ namespace AppInstaller::CLI::Workflow AICLI_TERMINATE_CONTEXT(APPINSTALLER_CLI_ERROR_EXPERIMENTAL_FEATURE_DISABLED); } } + + void SearchSourceUsingManifest(Execution::Context& context) + { + const auto& manifest = context.Get<Execution::Data::Manifest>(); + auto source = context.Get<Execution::Data::Source>(); + + // First try search using ProductId or PackageFamilyName + for (const auto& installer : manifest.Installers) + { + SearchRequest searchRequest; + if (!installer.PackageFamilyName.empty()) + { + searchRequest.Inclusions.emplace_back(PackageMatchFilter(PackageMatchField::PackageFamilyName, MatchType::Exact, installer.PackageFamilyName)); + } + else if (!installer.ProductCode.empty()) + { + searchRequest.Inclusions.emplace_back(PackageMatchFilter(PackageMatchField::ProductCode, MatchType::Exact, installer.ProductCode)); + } + + if (!searchRequest.Inclusions.empty()) + { + auto searchResult = source->Search(searchRequest); + + if (!searchResult.Matches.empty()) + { + context.Add<Execution::Data::SearchResult>(std::move(searchResult)); + return; + } + } + } + + // 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're same Ids from different sources, filter the result using package name + searchRequest.Filters.emplace_back(PackageMatchFilter(PackageMatchField::Name, MatchType::CaseInsensitive, manifest.Name)); + + context.Add<Execution::Data::SearchResult>(source->Search(searchRequest)); + } + + void GetInstalledPackageVersion(Execution::Context& context) + { + const auto& searchResult = context.Get<Execution::Data::SearchResult>(); + context.Add<Execution::Data::InstalledPackageVersion>(searchResult.Matches.at(0).Package->GetInstalledVersion()); + } } AppInstaller::CLI::Execution::Context& operator<<(AppInstaller::CLI::Execution::Context& context, AppInstaller::CLI::Workflow::WorkflowTask::Func f) diff --git a/src/AppInstallerCLICore/Workflows/WorkflowBase.h b/src/AppInstallerCLICore/Workflows/WorkflowBase.h @@ -16,6 +16,8 @@ namespace AppInstaller::CLI::Execution namespace AppInstaller::CLI::Workflow { + static const char* s_InstallationMetadata_Key_InstallerType = "InstallerType"; + // A task in the workflow. struct WorkflowTask { @@ -177,6 +179,24 @@ namespace AppInstaller::CLI::Workflow private: Settings::ExperimentalFeature::Feature m_feature; }; + + // Create a composite source from installed source and available source. + // Required Args: None + // Inputs: Source + // Outputs: Source + void GetCompositeSourceFromInstalledAndAvailable(Execution::Context& context); + + // Performs a search on the source with the semantics of targeting packages matching input manifest + // Required Args: None + // Inputs: Source, Manifest + // Outputs: SearchResult + void SearchSourceUsingManifest(Execution::Context& context); + + // Gets the installed package version + // Required Args: None + // Inputs: SearchResult + // Outputs: InstalledPackageVersion + void GetInstalledPackageVersion(Execution::Context& context); } // Passes the context to the function if it has not been terminated; returns the context. diff --git a/src/AppInstallerCLICore/pch.h b/src/AppInstallerCLICore/pch.h @@ -34,6 +34,7 @@ #include <wrl/client.h> #include <AppxPackaging.h> +#include <AppInstallerArchitecture.h> #include <AppInstallerDateTime.h> #include <AppInstallerDeployment.h> #include <AppInstallerDownloader.h> diff --git a/src/AppInstallerCLIPackage/Shared/Strings/en-us/winget.resw b/src/AppInstallerCLIPackage/Shared/Strings/en-us/winget.resw @@ -341,11 +341,11 @@ They can be configured through the settings file 'winget settings'.</value> <data name="MsixSignatureHashFailed" xml:space="preserve"> <value>Failed to calculate MSIX signature hash.</value> </data> - <data name="MSStoreInstallAppBlocked" xml:space="preserve"> - <value>Failed to install Microsoft Store package because the specific app is blocked by policy</value> + <data name="MSStoreAppBlocked" xml:space="preserve"> + <value>Failed to install or update Microsoft Store package because the specific app is blocked by policy</value> </data> - <data name="MSStoreInstallFailed" xml:space="preserve"> - <value>Failed to install Microsoft Store package. Error code:</value> + <data name="MSStoreInstallOrUpdateFailed" xml:space="preserve"> + <value>Failed to install or update Microsoft Store package. Error code:</value> </data> <data name="MSStoreInstallGetEntitlementNetworkError" xml:space="preserve"> <value>Verifying/Requesting package acquisition failed: network error</value> @@ -359,12 +359,15 @@ They can be configured through the settings file 'winget settings'.</value> <data name="MSStoreInstallGetEntitlementSuccess" xml:space="preserve"> <value>Verifying/Requesting package acquisition success</value> </data> - <data name="MSStoreInstallStoreClientBlocked" xml:space="preserve"> - <value>Failed to install Microsoft Store package because Microsoft Store client is blocked by policy</value> + <data name="MSStoreStoreClientBlocked" xml:space="preserve"> + <value>Failed to install or update 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> </data> + <data name="MultipleInstalledPackagesFound" xml:space="preserve"> + <value>Multiple installed packages found matching input criteria. Please refine the input.</value> + </data> <data name="MultiplePackagesFound" xml:space="preserve"> <value>Multiple packages found matching input criteria. Please refine the input.</value> </data> @@ -377,6 +380,9 @@ They can be configured through the settings file 'winget settings'.</value> <data name="NoExperimentalFeaturesMessage" xml:space="preserve"> <value>There are currently no exprimental features available. </value> </data> + <data name="NoInstalledPackageFound" xml:space="preserve"> + <value>No installed package found matching input criteria.</value> + </data> <data name="NoPackageFound" xml:space="preserve"> <value>No package found matching input criteria.</value> </data> @@ -619,7 +625,7 @@ They can be configured through the settings file 'winget settings'.</value> <value>Argument provided more times than allowed</value> </data> <data name="TooManyBehaviorsError" xml:space="preserve"> - <value>More than one install behavior argument provided</value> + <value>More than one execution behavior argument provided</value> </data> <data name="UnexpectedErrorExecutingCommand" xml:space="preserve"> <value>An unexpected error occurred while executing the command:</value> @@ -627,6 +633,19 @@ They can be configured through the settings file 'winget settings'.</value> <data name="UnrecognizedCommand" xml:space="preserve"> <value>Unrecognized command</value> </data> + <data name="UpdateAllArgumentDescription" xml:space="preserve"> + <value>Update all installed packages to latest if avaialable</value> + </data> + <data name="UpdateNotApplicable" xml:space="preserve"> + <value>No applicable update found.</value> + </data> + <data name="UpgradeCommandLongDescription" xml:space="preserve"> + <value>Updates 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.</value> + <comment>id, name, and moniker are all named values in our context, and may benefit from not being translated.</comment> + </data> + <data name="UpgradeCommandShortDescription" xml:space="preserve"> + <value>Updates the given package</value> + </data> <data name="Usage" xml:space="preserve"> <value>usage</value> <comment>The way to use the software</comment> @@ -655,4 +674,32 @@ They can be configured through the settings file 'winget settings'.</value> <data name="WordArgumentDescription" xml:space="preserve"> <value>The value provided before completion is requested</value> </data> + <data name="GetManifestResultVersionNotFound" xml:space="preserve"> + <value>No version found matching:</value> + </data> + <data name="OpenSourceFailed" xml:space="preserve"> + <value>Failed to open the source; try removing and re-adding it</value> + </data> + <data name="OpenSourceFailedNoMatch" xml:space="preserve"> + <value>No sources match the given value:</value> + </data> + <data name="OpenSourceFailedNoMatchHelp" xml:space="preserve"> + <value>The configured sources are:</value> + </data> + <data name="OpenSourceFailedNoSourceDefined" xml:space="preserve"> + <value>No sources defined; add one with 'source add' or reset to defaults with 'source reset'</value> + <comment>{Locked="source add","source reset"}</comment> + </data> + <data name="ReportIdentityFound" xml:space="preserve"> + <value>Found</value> + </data> + <data name="VerifyFileFailedIsDirectory" xml:space="preserve"> + <value>Path is a directory:</value> + </data> + <data name="VerifyFileFailedNotExist" xml:space="preserve"> + <value>File does not exist:</value> + </data> + <data name="BothManifestAndSearchQueryProvided" xml:space="preserve"> + <value>Both local manifest and search query args are provided</value> + </data> </root> \ No newline at end of file diff --git a/src/AppInstallerCLITests/AppInstallerCLITests.vcxproj b/src/AppInstallerCLITests/AppInstallerCLITests.vcxproj @@ -411,15 +411,21 @@ </CopyFileToFolders> <CopyFileToFolders Include="TestData\Manifest-Bad-PackageFamilyNameOnMSI.yaml"> <DeploymentContent>true</DeploymentContent> - <FileType>Document</FileType> </CopyFileToFolders> <CopyFileToFolders Include="TestData\Manifest-Bad-ProductCodeOnMSIX.yaml"> <DeploymentContent>true</DeploymentContent> - <FileType>Document</FileType> </CopyFileToFolders> <CopyFileToFolders Include="TestData\Manifest-Good-SystemReferenceComplex.yaml"> <DeploymentContent>true</DeploymentContent> - <FileType>Document</FileType> + </CopyFileToFolders> + <CopyFileToFolders Include="TestData\Manifest-Bad-InvalidUpdateBehavior.yaml"> + <DeploymentContent>true</DeploymentContent> + </CopyFileToFolders> + <CopyFileToFolders Include="TestData\UpdateFlowTest_Exe.yaml"> + <DeploymentContent>true</DeploymentContent> + </CopyFileToFolders> + <CopyFileToFolders Include="TestData\UpdateFlowTest_Msix.yaml"> + <DeploymentContent>true</DeploymentContent> </CopyFileToFolders> </ItemGroup> <ItemGroup> diff --git a/src/AppInstallerCLITests/AppInstallerCLITests.vcxproj.filters b/src/AppInstallerCLITests/AppInstallerCLITests.vcxproj.filters @@ -309,5 +309,14 @@ <CopyFileToFolders Include="TestData\Manifest-Bad-ProductCodeOnMSIX.yaml"> <Filter>TestData</Filter> </CopyFileToFolders> + <CopyFileToFolders Include="TestData\Manifest-Bad-InvalidUpdateBehavior.yaml"> + <Filter>TestData</Filter> + </CopyFileToFolders> + <CopyFileToFolders Include="TestData\UpdateFlowTest_Exe.yaml"> + <Filter>TestData</Filter> + </CopyFileToFolders> + <CopyFileToFolders Include="TestData\UpdateFlowTest_Msix.yaml"> + <Filter>TestData</Filter> + </CopyFileToFolders> </ItemGroup> </Project> \ No newline at end of file diff --git a/src/AppInstallerCLITests/TestData/InstallFlowTest_Exe.yaml b/src/AppInstallerCLITests/TestData/InstallFlowTest_Exe.yaml @@ -1,4 +1,4 @@ -Id: AppInstallerCliTest.TestInstaller +Id: AppInstallerCliTest.TestExeInstaller Version: 1.0.0.0 Name: AppInstaller Test Installer Publisher: Microsoft Corporation @@ -8,6 +8,7 @@ Switches: Custom: /custom SilentWithProgress: /silentwithprogress Silent: /silence + Update: /update Installers: - Arch: x64 Url: https://ThisIsNotUsed diff --git a/src/AppInstallerCLITests/TestData/InstallFlowTest_MSStore.yaml b/src/AppInstallerCLITests/TestData/InstallFlowTest_MSStore.yaml @@ -1,5 +1,5 @@ Id: AppInstallerCliTest.TestMSStoreInstaller -Version: 1.0.0.0 +Version: Latest Name: AppInstaller Test Installer Publisher: Microsoft Corporation AppMoniker: AICLITestMSStore diff --git a/src/AppInstallerCLITests/TestData/Manifest-Bad-InvalidUpdateBehavior.yaml b/src/AppInstallerCLITests/TestData/Manifest-Bad-InvalidUpdateBehavior.yaml @@ -0,0 +1,13 @@ +# Bad manifest. Invalid UpdateBehavior +Id: microsoft.msixsdk +Name: MSIX SDK +Version: 1.7.32 +Publisher: Microsoft +InstallerType: Zip +License: Test +Installers: + - Arch: x86 + Url: NotAUrl + Sha256: 98B67758CEAFFCBB3FE47838FD0A8D7BD581C2650842D6B2B0E0D49A23270CCD + UpdateBehavior: InvalidUpdate +ManifestVersion: 0.1.0 diff --git a/src/AppInstallerCLITests/TestData/Manifest-Good.yaml b/src/AppInstallerCLITests/TestData/Manifest-Good.yaml @@ -19,6 +19,7 @@ FileExtensions: "appx,appxbundle,msix,msixbundle" InstallerType: Zip PackageFamilyName: Microsoft.DesktopAppInstaller_8wekyb3d8bbwe ProductCode: "{Foo}" +UpdateBehavior: UninstallPrevious Switches: Custom: /custom SilentWithProgress: /silentwithprogress @@ -27,6 +28,7 @@ Switches: Language: /en-us Log: /log=<LOGPATH> InstallLocation: /dir=<INSTALLPATH> + Update: /update Installers: - Arch: x86 Url: https://rubengustorage.blob.core.windows.net/publiccontainer/msixsdkx86.zip @@ -34,6 +36,7 @@ Installers: Language: en-US InstallerType: Zip Scope: user + UpdateBehavior: Install Switches: Custom: /c SilentWithProgress: /sp @@ -42,6 +45,7 @@ Installers: Language: /en Log: /l=<LOGPATH> InstallLocation: /d=<INSTALLPATH> + Update: /u - Arch: x64 Url: https://rubengustorage.blob.core.windows.net/publiccontainer/msixsdkx64.zip Sha256: 69D84CA8899800A5575CE31798293CD4FEBAB1D734A07C2E51E56A28E0DF0000 diff --git a/src/AppInstallerCLITests/TestData/UpdateFlowTest_Exe.yaml b/src/AppInstallerCLITests/TestData/UpdateFlowTest_Exe.yaml @@ -0,0 +1,18 @@ +# Same content with InstallFlowTest_Exe.yaml but with higher version +Id: AppInstallerCliTest.TestExeInstaller +Version: 2.0.0.0 +Name: AppInstaller Test Installer +Publisher: Microsoft Corporation +AppMoniker: AICLITestExe +License: Test +Switches: + Custom: /custom + SilentWithProgress: /silentwithprogress + Silent: /silence + Update: /update +Installers: + - Arch: x64 + Url: https://ThisIsNotUsed + InstallerType: exe + Sha256: 65DB2F2AC2686C7F2FD69D4A4C6683B888DC55BFA20A0E32CA9F838B51689A3B +ManifestVersion: 0.1.0 diff --git a/src/AppInstallerCLITests/TestData/UpdateFlowTest_Msix.yaml b/src/AppInstallerCLITests/TestData/UpdateFlowTest_Msix.yaml @@ -0,0 +1,14 @@ +# Same content with InstallFlowTest_Msix_StreamingFlow.yaml but with higher version +Id: AppInstallerCliTest.TestMsixInstaller +Version: 2.0.0.0 +Name: AppInstaller Test MSIX Installer +Publisher: Microsoft Corporation +AppMoniker: AICLITestMsix +License: Test +Installers: + - Arch: x64 + Url: https://github.com/microsoft/msix-packaging/blob/master/src/test/testData/unpack/TestAppxPackage_x64.appx?raw=true + InstallerType: msix + Sha256: 6a2d3683fa19bf00e58e07d1313d20a5f5735ebbd6a999d33381d28740ee07ea + SignatureSha256: 138781c3e6f635240353f3d14d1d57bdcb89413e49be63b375e6a5d7b93b0d07 +ManifestVersion: 0.1.0 diff --git a/src/AppInstallerCLITests/WorkFlow.cpp b/src/AppInstallerCLITests/WorkFlow.cpp @@ -2,10 +2,13 @@ // Licensed under the MIT License. #include "pch.h" #include "TestCommon.h" +#include <AppInstallerErrors.h> #include <AppInstallerLogging.h> #include <AppInstallerDownloader.h> #include <AppInstallerStrings.h> #include <Workflows/InstallFlow.h> +#include <Workflows/UpdateFlow.h> +#include <Workflows/MSStoreInstallerHandler.h> #include <Workflows/ShowFlow.h> #include <Workflows/ShellExecuteInstallerHandler.h> #include <Workflows/WorkflowBase.h> @@ -13,6 +16,7 @@ #include <Public/AppInstallerRepositorySearch.h> #include <Commands/InstallCommand.h> #include <Commands/ShowCommand.h> +#include <Commands/UpgradeCommand.h> #include <winget/LocIndependent.h> #include <winget/ManifestYamlParser.h> #include <Resources.h> @@ -34,82 +38,103 @@ using namespace AppInstaller::Utility; namespace { - struct TestSource : public ISource + struct TestPackageVersion : public IPackageVersion { - struct TestPackageVersion : public IPackageVersion - { - TestPackageVersion(const Manifest& manifest) : m_manifest(manifest) {} - - LocIndString GetProperty(PackageVersionProperty property) const override - { - switch (property) - { - case PackageVersionProperty::Id: - return LocIndString{ m_manifest.Id }; - case PackageVersionProperty::Name: - return LocIndString{ m_manifest.Name }; - case PackageVersionProperty::Version: - return LocIndString{ m_manifest.Version }; - case PackageVersionProperty::Channel: - return LocIndString{ m_manifest.Channel }; - default: - return {}; - } - } + TestPackageVersion(const Manifest& manifest, std::map<std::string, std::string> installationMetadata = {}) : + m_manifest(manifest), m_installationMetadata(std::move(installationMetadata)) {} - Manifest GetManifest() const override - { - return m_manifest; - } - - std::map<std::string, std::string> GetInstallationMetadata() const override + LocIndString GetProperty(PackageVersionProperty property) const override + { + switch (property) { + case PackageVersionProperty::Id: + return LocIndString{ m_manifest.Id }; + case PackageVersionProperty::Name: + return LocIndString{ m_manifest.Name }; + case PackageVersionProperty::Version: + return LocIndString{ m_manifest.Version }; + case PackageVersionProperty::Channel: + return LocIndString{ m_manifest.Channel }; + default: return {}; } + } - Manifest m_manifest; - }; + Manifest GetManifest() const override + { + return m_manifest; + } - struct TestPackage : public IPackage + std::map<std::string, std::string> GetInstallationMetadata() const override { - TestPackage(const Manifest& manifest) : m_manifest(manifest) {} + return m_installationMetadata; + } + + Manifest m_manifest; + std::map<std::string, std::string> m_installationMetadata; + }; + + struct TestPackage : public IPackage + { + // The input manifest list should have been sorted, GetAvailableVersions will just return in the order of input manifest list.. + // installedIndex is the index of the manifest in the list to be returned by GetInstalledVersion(), + // -1 mean no installed version + TestPackage(std::vector<Manifest> manifestList, int installedIndex = -1, std::map<std::string, std::string> installationMetadata = {}) : + m_manifestList(manifestList), m_installedIndex(installedIndex), m_installationMetadata(installationMetadata){} - std::shared_ptr<IPackageVersion> GetInstalledVersion() const override + std::shared_ptr<IPackageVersion> GetInstalledVersion() const override + { + if (m_installedIndex >= 0) { - return {}; + return std::make_shared<TestPackageVersion>(m_manifestList.at(m_installedIndex), m_installationMetadata); } - - std::vector<PackageVersionKey> GetAvailableVersionKeys() const override + else { - return { { "", m_manifest.Version, m_manifest.Channel } }; + return {}; } + } - std::shared_ptr<IPackageVersion> GetLatestAvailableVersion() const override + std::vector<PackageVersionKey> GetAvailableVersionKeys() const override + { + std::vector<PackageVersionKey> result; + for (const auto& manifest : m_manifestList) { - return std::make_shared<TestPackageVersion>(m_manifest); + result.emplace_back(PackageVersionKey("", manifest.Version, manifest.Channel)); } + return result; + } + + std::shared_ptr<IPackageVersion> GetLatestAvailableVersion() const override + { + return std::make_shared<TestPackageVersion>(m_manifestList.at(0)); + } - std::shared_ptr<IPackageVersion> GetAvailableVersion(const PackageVersionKey& versionKey) const override + std::shared_ptr<IPackageVersion> GetAvailableVersion(const PackageVersionKey& versionKey) const override + { + for (const auto& manifest : m_manifestList) { - if ((versionKey.Version.empty() || versionKey.Version == m_manifest.Version) && - (versionKey.Channel.empty() || versionKey.Channel == m_manifest.Channel)) - { - return std::make_shared<TestPackageVersion>(m_manifest); - } - else + if ((versionKey.Version.empty() || versionKey.Version == manifest.Version) && + (versionKey.Channel.empty() || versionKey.Channel == manifest.Channel)) { - return {}; + return std::make_shared<TestPackageVersion>(manifest); } } - bool IsUpdateAvailable() const override - { - return false; - } + return {}; + } + + bool IsUpdateAvailable() const override + { + return false; + } - Manifest m_manifest; - }; + std::vector<Manifest> m_manifestList; + int m_installedIndex; + std::map<std::string, std::string> m_installationMetadata; + }; + struct TestSource : public ISource + { SearchResult Search(const SearchRequest& request) const override { SearchResult result; @@ -130,7 +155,7 @@ namespace auto manifest = YamlParser::CreateFromPath(TestDataFile("InstallFlowTest_Exe.yaml")); result.Matches.emplace_back( ResultMatch( - std::make_unique<TestPackage>(manifest), + std::make_unique<TestPackage>(std::vector<Manifest>{ manifest }), PackageMatchFilter(PackageMatchField::Id, MatchType::Exact, "TestQueryReturnOne"))); } else if (input == "TestQueryReturnTwo") @@ -138,13 +163,13 @@ namespace auto manifest = YamlParser::CreateFromPath(TestDataFile("InstallFlowTest_Exe.yaml")); result.Matches.emplace_back( ResultMatch( - std::make_unique<TestPackage>(manifest), + std::make_unique<TestPackage>(std::vector<Manifest>{ manifest }), PackageMatchFilter(PackageMatchField::Id, MatchType::Exact, "TestQueryReturnTwo"))); auto manifest2 = YamlParser::CreateFromPath(TestDataFile("Manifest-Good.yaml")); result.Matches.emplace_back( ResultMatch( - std::make_unique<TestPackage>(manifest2), + std::make_unique<TestPackage>(std::vector<Manifest>{ manifest2 }), PackageMatchFilter(PackageMatchField::Id, MatchType::Exact, "TestQueryReturnTwo"))); } @@ -156,6 +181,86 @@ namespace const std::string& GetIdentifier() const override { THROW_HR(E_NOTIMPL); } }; + struct TestCompositeInstalledSource : public ISource + { + SearchResult Search(const SearchRequest& request) const override + { + SearchResult result; + + std::string input; + + if (request.Query) + { + input = request.Query->Value; + } + else if (!request.Inclusions.empty()) + { + input = request.Inclusions[0].Value; + } + + // Empty query should return all exe, msix and msstore installer + if (input.empty() || input == "AppInstallerCliTest.TestExeInstaller") + { + auto manifest = YamlParser::CreateFromPath(TestDataFile("InstallFlowTest_Exe.yaml")); + auto manifest2 = YamlParser::CreateFromPath(TestDataFile("UpdateFlowTest_Exe.yaml")); + result.Matches.emplace_back( + ResultMatch( + std::make_unique<TestPackage>(std::vector<Manifest>{ manifest2, manifest }, 1, + std::map<std::string, std::string>{ { s_InstallationMetadata_Key_InstallerType, "Exe" } }), + PackageMatchFilter(PackageMatchField::Id, MatchType::Exact, "AppInstallerCliTest.TestExeInstaller"))); + } + + if (input.empty() || input == "AppInstallerCliTest.TestMsixInstaller") + { + auto manifest = YamlParser::CreateFromPath(TestDataFile("InstallFlowTest_Msix_StreamingFlow.yaml")); + auto manifest2 = YamlParser::CreateFromPath(TestDataFile("UpdateFlowTest_Msix.yaml")); + result.Matches.emplace_back( + ResultMatch( + std::make_unique<TestPackage>(std::vector<Manifest>{ manifest2, manifest }, 1, + std::map<std::string, std::string>{ { s_InstallationMetadata_Key_InstallerType, "Msix" } }), + PackageMatchFilter(PackageMatchField::Id, MatchType::Exact, "AppInstallerCliTest.TestMsixInstaller"))); + } + + if (input.empty() || input == "AppInstallerCliTest.TestMSStoreInstaller") + { + auto manifest = YamlParser::CreateFromPath(TestDataFile("InstallFlowTest_MSStore.yaml")); + result.Matches.emplace_back( + ResultMatch( + std::make_unique<TestPackage>(std::vector<Manifest>{ manifest }, 0, + std::map<std::string, std::string>{ { s_InstallationMetadata_Key_InstallerType, "MSStore" } }), + PackageMatchFilter(PackageMatchField::Id, MatchType::Exact, "AppInstallerCliTest.TestMSStoreInstaller"))); + } + + if (input == "TestExeInstallerWithLatestInstalled") + { + auto manifest = YamlParser::CreateFromPath(TestDataFile("InstallFlowTest_Exe.yaml")); + auto manifest2 = YamlParser::CreateFromPath(TestDataFile("UpdateFlowTest_Exe.yaml")); + result.Matches.emplace_back( + ResultMatch( + std::make_unique<TestPackage>(std::vector<Manifest>{ manifest2, manifest }, 0, + std::map<std::string, std::string>{ { s_InstallationMetadata_Key_InstallerType, "Exe" } }), + PackageMatchFilter(PackageMatchField::Id, MatchType::Exact, "AppInstallerCliTest.TestExeInstaller"))); + } + + if (input == "TestExeInstallerWithIncompatibleInstallerType") + { + auto manifest = YamlParser::CreateFromPath(TestDataFile("InstallFlowTest_Exe.yaml")); + auto manifest2 = YamlParser::CreateFromPath(TestDataFile("UpdateFlowTest_Exe.yaml")); + result.Matches.emplace_back( + ResultMatch( + std::make_unique<TestPackage>(std::vector<Manifest>{ manifest2, manifest }, 1, + std::map<std::string, std::string>{ { s_InstallationMetadata_Key_InstallerType, "Msix" } }), + PackageMatchFilter(PackageMatchField::Id, MatchType::Exact, "AppInstallerCliTest.TestExeInstaller"))); + } + + return result; + } + + const SourceDetails& GetDetails() const override { THROW_HR(E_NOTIMPL); } + + const std::string& GetIdentifier() const override { THROW_HR(E_NOTIMPL); } + }; + struct TestContext; struct WorkflowTaskOverride @@ -177,8 +282,10 @@ namespace // Enables overriding the behavior of specific workflow tasks. struct TestContext : public Context { - TestContext(std::ostream& out, std::istream& in) : Context(out, in) + TestContext(std::ostream& out, std::istream& in) : m_out(out), m_in(in), Context(out, in) { + m_overrides = std::make_shared<std::vector<WorkflowTaskOverride>>(); + WorkflowTaskOverride wto { RemoveInstaller, [](TestContext&) { @@ -191,22 +298,29 @@ namespace Override(wto); } + // For clone + TestContext(std::ostream& out, std::istream& in, std::shared_ptr<std::vector<WorkflowTaskOverride>> overrides) : + m_out(out), m_in(in), m_overrides(overrides), m_isClone(true), Context(out, in) {} + ~TestContext() { - for (const auto& wto : m_overrides) + if (!m_isClone) { - if (!wto.Used) + for (const auto& wto : *m_overrides) { - FAIL("Unused override"); + if (!wto.Used) + { + FAIL("Unused override"); + } } } } bool ShouldExecuteWorkflowTask(const Workflow::WorkflowTask& task) override { - auto itr = std::find_if(m_overrides.begin(), m_overrides.end(), [&](const WorkflowTaskOverride& wto) { return wto.Target == task; }); + auto itr = std::find_if(m_overrides->begin(), m_overrides->end(), [&](const WorkflowTaskOverride& wto) { return wto.Target == task; }); - if (itr == m_overrides.end()) + if (itr == m_overrides->end()) { return true; } @@ -220,11 +334,21 @@ namespace void Override(const WorkflowTaskOverride& wto) { - m_overrides.emplace_back(wto); + m_overrides->emplace_back(wto); + } + + std::unique_ptr<Context> Clone() override + { + auto clone = std::make_unique<TestContext>(m_out, m_in, m_overrides); + clone->GetFlags() = this->GetFlags(); + return clone; } private: - std::vector<WorkflowTaskOverride> m_overrides; + std::shared_ptr<std::vector<WorkflowTaskOverride>> m_overrides; + std::ostream& m_out; + std::istream& m_in; + bool m_isClone = false; }; } @@ -236,6 +360,18 @@ void OverrideForOpenSource(TestContext& context) } }); } +void OverrideForCompositeInstalledSource(TestContext& context) +{ + context.Override({ Workflow::OpenSource, [](TestContext&) + { + } }); + + context.Override({ Workflow::GetCompositeSourceFromInstalledAndAvailable, [](TestContext& context) + { + context.Add<Execution::Data::Source>(std::make_shared<TestCompositeInstalledSource>()); + } }); +} + void OverrideForShellExecute(TestContext& context) { context.Override({ DownloadInstallerFile, [](TestContext& context) @@ -270,20 +406,38 @@ void OverrideForMSIX(TestContext& context) } }); } -void OverrideForMSStore(TestContext& context) +void OverrideForMSStore(TestContext& context, bool isUpdate) { - context.Override({ MSStoreInstall, [](TestContext& context) + if (isUpdate) { - std::filesystem::path temp = std::filesystem::temp_directory_path(); - temp /= "TestMSStoreInstalled.txt"; - std::ofstream file(temp, std::ofstream::out); - file << context.Get<Execution::Data::Installer>()->ProductId; - file.close(); - } }); + context.Override({ MSStoreUpdate, [](TestContext& context) + { + std::filesystem::path temp = std::filesystem::temp_directory_path(); + temp /= "TestMSStoreUpdated.txt"; + std::ofstream file(temp, std::ofstream::out); + file << context.Get<Execution::Data::Installer>()->ProductId; + file.close(); + } }); + } + else + { + context.Override({ MSStoreInstall, [](TestContext& context) + { + std::filesystem::path temp = std::filesystem::temp_directory_path(); + temp /= "TestMSStoreInstalled.txt"; + std::ofstream file(temp, std::ofstream::out); + file << context.Get<Execution::Data::Installer>()->ProductId; + file.close(); + } }); + } context.Override({ "EnsureFeatureEnabled", [](TestContext&) { } }); + + context.Override({ Workflow::EnsureStorePolicySatisfied, [](TestContext&) + { + } }); } TEST_CASE("ExeInstallFlowWithTestManifest", "[InstallFlow]") @@ -333,7 +487,7 @@ TEST_CASE("MSStoreInstallFlowWithTestManifest", "[InstallFlow]") std::ostringstream installOutput; TestContext context{ installOutput, std::cin }; - OverrideForMSStore(context); + OverrideForMSStore(context, false); context.Args.AddArg(Execution::Args::Type::Manifest, TestDataFile("InstallFlowTest_MSStore.yaml").GetPath().u8string()); InstallCommand install({}); @@ -573,7 +727,7 @@ TEST_CASE("InstallFlow_SearchAndShowAppInfo", "[ShowFlow]") INFO(showOutput.str()); // Verify AppInfo is printed - REQUIRE(showOutput.str().find("AppInstallerCliTest.TestInstaller") != std::string::npos); + REQUIRE(showOutput.str().find("AppInstallerCliTest.TestExeInstaller") != std::string::npos); REQUIRE(showOutput.str().find("AppInstaller Test Installer") != std::string::npos); REQUIRE(showOutput.str().find("1.0.0.0") != std::string::npos); REQUIRE(showOutput.str().find("https://ThisIsNotUsed") != std::string::npos); @@ -595,4 +749,257 @@ TEST_CASE("InstallFlow_SearchAndShowAppVersion", "[ShowFlow]") REQUIRE(showOutput.str().find("1.0.0.0") != std::string::npos); // No manifest info is printed REQUIRE(showOutput.str().find(" Download Url: https://ThisIsNotUsed") == std::string::npos); +} + +TEST_CASE("UpdateFlow_UpdateWithManifest", "[UpdateFlow]") +{ + TestCommon::TempFile updateResultPath("TestExeInstalled.txt"); + + std::ostringstream updateOutput; + TestContext context{ updateOutput, std::cin }; + OverrideForCompositeInstalledSource(context); + OverrideForShellExecute(context); + context.Args.AddArg(Execution::Args::Type::Manifest, TestDataFile("UpdateFlowTest_Exe.yaml").GetPath().u8string()); + + UpgradeCommand update({}); + update.Execute(context); + INFO(updateOutput.str()); + + // Verify Installer is called and parameters are passed in. + REQUIRE(std::filesystem::exists(updateResultPath.GetPath())); + std::ifstream updateResultFile(updateResultPath.GetPath()); + REQUIRE(updateResultFile.is_open()); + std::string updateResultStr; + std::getline(updateResultFile, updateResultStr); + REQUIRE(updateResultStr.find("/update") != std::string::npos); + REQUIRE(updateResultStr.find("/silentwithprogress") != std::string::npos); +} + +TEST_CASE("UpdateFlow_UpdateWithManifestMSStore", "[UpdateFlow]") +{ + TestCommon::TempFile updateResultPath("TestMSStoreUpdated.txt"); + + std::ostringstream updateOutput; + TestContext context{ updateOutput, std::cin }; + OverrideForCompositeInstalledSource(context); + OverrideForMSStore(context, true); + context.Args.AddArg(Execution::Args::Type::Manifest, TestDataFile("InstallFlowTest_MSStore.yaml").GetPath().u8string()); + + UpgradeCommand update({}); + update.Execute(context); + INFO(updateOutput.str()); + + // Verify Installer is called and parameters are passed in. + REQUIRE(std::filesystem::exists(updateResultPath.GetPath())); + std::ifstream updateResultFile(updateResultPath.GetPath()); + REQUIRE(updateResultFile.is_open()); + std::string updateResultStr; + std::getline(updateResultFile, updateResultStr); + REQUIRE(updateResultStr.find("9WZDNCRFJ364") != std::string::npos); +} + +TEST_CASE("UpdateFlow_UpdateWithManifestAppNotInstalled", "[UpdateFlow]") +{ + TestCommon::TempFile updateResultPath("TestExeInstalled.txt"); + + std::ostringstream updateOutput; + TestContext context{ updateOutput, std::cin }; + OverrideForCompositeInstalledSource(context); + context.Args.AddArg(Execution::Args::Type::Manifest, TestDataFile("InstallerArgTest_Inno_NoSwitches.yaml").GetPath().u8string()); + + UpgradeCommand update({}); + update.Execute(context); + INFO(updateOutput.str()); + + // Verify Installer is not called. + REQUIRE(!std::filesystem::exists(updateResultPath.GetPath())); + REQUIRE(updateOutput.str().find(Resource::LocString(Resource::String::NoInstalledPackageFound).get()) != std::string::npos); + REQUIRE(context.GetTerminationHR() == APPINSTALLER_CLI_ERROR_NO_APPLICATIONS_FOUND); +} + +TEST_CASE("UpdateFlow_UpdateWithManifestVersionAlreadyInstalled", "[UpdateFlow]") +{ + TestCommon::TempFile updateResultPath("TestExeInstalled.txt"); + + std::ostringstream updateOutput; + TestContext context{ updateOutput, std::cin }; + OverrideForCompositeInstalledSource(context); + context.Args.AddArg(Execution::Args::Type::Manifest, TestDataFile("InstallFlowTest_Exe.yaml").GetPath().u8string()); + + UpgradeCommand update({}); + update.Execute(context); + INFO(updateOutput.str()); + + // Verify Installer is not called. + REQUIRE(!std::filesystem::exists(updateResultPath.GetPath())); + REQUIRE(updateOutput.str().find(Resource::LocString(Resource::String::UpdateNotApplicable).get()) != std::string::npos); + REQUIRE(context.GetTerminationHR() == APPINSTALLER_CLI_ERROR_UPDATE_NOT_APPLICABLE); +} + +TEST_CASE("UpdateFlow_UpdateExe", "[UpdateFlow]") +{ + TestCommon::TempFile updateResultPath("TestExeInstalled.txt"); + + std::ostringstream updateOutput; + TestContext context{ updateOutput, std::cin }; + OverrideForCompositeInstalledSource(context); + OverrideForShellExecute(context); + context.Args.AddArg(Execution::Args::Type::Query, "AppInstallerCliTest.TestExeInstaller"sv); + context.Args.AddArg(Execution::Args::Type::Silent); + + UpgradeCommand update({}); + update.Execute(context); + INFO(updateOutput.str()); + + // Verify Installer is called and parameters are passed in. + REQUIRE(std::filesystem::exists(updateResultPath.GetPath())); + std::ifstream updateResultFile(updateResultPath.GetPath()); + REQUIRE(updateResultFile.is_open()); + std::string updateResultStr; + std::getline(updateResultFile, updateResultStr); + REQUIRE(updateResultStr.find("/update") != std::string::npos); + REQUIRE(updateResultStr.find("/silence") != std::string::npos); +} + +TEST_CASE("UpdateFlow_UpdateMsix", "[UpdateFlow]") +{ + TestCommon::TempFile updateResultPath("TestMsixInstalled.txt"); + + std::ostringstream updateOutput; + TestContext context{ updateOutput, std::cin }; + OverrideForCompositeInstalledSource(context); + OverrideForMSIX(context); + context.Args.AddArg(Execution::Args::Type::Query, "AppInstallerCliTest.TestMsixInstaller"sv); + + UpgradeCommand update({}); + update.Execute(context); + INFO(updateOutput.str()); + + // Verify Installer is called. + REQUIRE(std::filesystem::exists(updateResultPath.GetPath())); +} + +TEST_CASE("UpdateFlow_UpdateMSStore", "[UpdateFlow]") +{ + TestCommon::TempFile updateResultPath("TestMSStoreUpdated.txt"); + + std::ostringstream updateOutput; + TestContext context{ updateOutput, std::cin }; + OverrideForCompositeInstalledSource(context); + OverrideForMSStore(context, true); + context.Args.AddArg(Execution::Args::Type::Query, "AppInstallerCliTest.TestMSStoreInstaller"sv); + + UpgradeCommand update({}); + update.Execute(context); + INFO(updateOutput.str()); + + // Verify Installer is called. + REQUIRE(std::filesystem::exists(updateResultPath.GetPath())); + std::ifstream updateResultFile(updateResultPath.GetPath()); + REQUIRE(updateResultFile.is_open()); + std::string updateResultStr; + std::getline(updateResultFile, updateResultStr); + REQUIRE(updateResultStr.find("9WZDNCRFJ364") != std::string::npos); +} + +TEST_CASE("UpdateFlow_UpdateExeLatestAlreadyInstalled", "[UpdateFlow]") +{ + TestCommon::TempFile updateResultPath("TestExeInstalled.txt"); + + std::ostringstream updateOutput; + TestContext context{ updateOutput, std::cin }; + OverrideForCompositeInstalledSource(context); + context.Args.AddArg(Execution::Args::Type::Query, "TestExeInstallerWithLatestInstalled"sv); + + UpgradeCommand update({}); + update.Execute(context); + INFO(updateOutput.str()); + + // Verify Installer is not called. + REQUIRE(!std::filesystem::exists(updateResultPath.GetPath())); + REQUIRE(updateOutput.str().find(Resource::LocString(Resource::String::UpdateNotApplicable).get()) != std::string::npos); + REQUIRE(context.GetTerminationHR() == APPINSTALLER_CLI_ERROR_UPDATE_NOT_APPLICABLE); +} + +TEST_CASE("UpdateFlow_UpdateExeInstallerTypeNotApplicable", "[UpdateFlow]") +{ + TestCommon::TempFile updateResultPath("TestExeInstalled.txt"); + + std::ostringstream updateOutput; + TestContext context{ updateOutput, std::cin }; + OverrideForCompositeInstalledSource(context); + context.Args.AddArg(Execution::Args::Type::Query, "TestExeInstallerWithIncompatibleInstallerType"sv); + + UpgradeCommand update({}); + update.Execute(context); + INFO(updateOutput.str()); + + // Verify Installer is not called. + REQUIRE(!std::filesystem::exists(updateResultPath.GetPath())); + REQUIRE(updateOutput.str().find(Resource::LocString(Resource::String::UpdateNotApplicable).get()) != std::string::npos); + REQUIRE(context.GetTerminationHR() == APPINSTALLER_CLI_ERROR_UPDATE_NOT_APPLICABLE); +} + +TEST_CASE("UpdateFlow_UpdateExeSpecificVersionNotFound", "[UpdateFlow]") +{ + TestCommon::TempFile updateResultPath("TestExeInstalled.txt"); + + std::ostringstream updateOutput; + TestContext context{ updateOutput, std::cin }; + OverrideForCompositeInstalledSource(context); + context.Args.AddArg(Execution::Args::Type::Query, "AppInstallerCliTest.TestExeInstaller"sv); + context.Args.AddArg(Execution::Args::Type::Version, "1.2.3.4"sv); + + UpgradeCommand update({}); + update.Execute(context); + INFO(updateOutput.str()); + + // Verify Installer is not called. + REQUIRE(!std::filesystem::exists(updateResultPath.GetPath())); + REQUIRE(updateOutput.str().find(Resource::LocString(Resource::String::GetManifestResultVersionNotFound).get()) != std::string::npos); + REQUIRE(context.GetTerminationHR() == APPINSTALLER_CLI_ERROR_NO_MANIFEST_FOUND); +} + +TEST_CASE("UpdateFlow_UpdateExeSpecificVersionNotApplicable", "[UpdateFlow]") +{ + TestCommon::TempFile updateResultPath("TestExeInstalled.txt"); + + std::ostringstream updateOutput; + TestContext context{ updateOutput, std::cin }; + OverrideForCompositeInstalledSource(context); + context.Args.AddArg(Execution::Args::Type::Query, "TestExeInstallerWithIncompatibleInstallerType"sv); + context.Args.AddArg(Execution::Args::Type::Version, "1.0.0.0"sv); + + UpgradeCommand update({}); + update.Execute(context); + INFO(updateOutput.str()); + + // Verify Installer is not called. + REQUIRE(!std::filesystem::exists(updateResultPath.GetPath())); + REQUIRE(updateOutput.str().find(Resource::LocString(Resource::String::UpdateNotApplicable).get()) != std::string::npos); + REQUIRE(context.GetTerminationHR() == APPINSTALLER_CLI_ERROR_UPDATE_NOT_APPLICABLE); +} + +TEST_CASE("UpdateFlow_UpdateAllApplicable", "[UpdateFlow]") +{ + TestCommon::TempFile updateExeResultPath("TestExeInstalled.txt"); + TestCommon::TempFile updateMsixResultPath("TestMsixInstalled.txt"); + TestCommon::TempFile updateMSStoreResultPath("TestMSStoreUpdated.txt"); + + std::ostringstream updateOutput; + TestContext context{ updateOutput, std::cin }; + OverrideForCompositeInstalledSource(context); + OverrideForShellExecute(context); + OverrideForMSIX(context); + OverrideForMSStore(context, true); + context.Args.AddArg(Execution::Args::Type::All); + + UpgradeCommand update({}); + update.Execute(context); + INFO(updateOutput.str()); + + // Verify installers are called. + REQUIRE(std::filesystem::exists(updateExeResultPath.GetPath())); + REQUIRE(std::filesystem::exists(updateMsixResultPath.GetPath())); + REQUIRE(std::filesystem::exists(updateMSStoreResultPath.GetPath())); } \ No newline at end of file diff --git a/src/AppInstallerCLITests/YamlManifest.cpp b/src/AppInstallerCLITests/YamlManifest.cpp @@ -51,6 +51,7 @@ TEST_CASE("ReadGoodManifestAndVerifyContents", "[ManifestValidation]") REQUIRE(manifest.InstallerType == ManifestInstaller::InstallerTypeEnum::Zip); REQUIRE(manifest.PackageFamilyName == "Microsoft.DesktopAppInstaller_8wekyb3d8bbwe"); REQUIRE(manifest.ProductCode == "{Foo}"); + REQUIRE(manifest.UpdateBehavior == ManifestInstaller::UpdateBehaviorEnum::UninstallPrevious); // default switches auto switches = manifest.Switches; @@ -61,6 +62,7 @@ TEST_CASE("ReadGoodManifestAndVerifyContents", "[ManifestValidation]") REQUIRE(switches.at(ManifestInstaller::InstallerSwitchType::Language) == "/en-us"); REQUIRE(switches.at(ManifestInstaller::InstallerSwitchType::Log) == "/log=<LOGPATH>"); REQUIRE(switches.at(ManifestInstaller::InstallerSwitchType::InstallLocation) == "/dir=<INSTALLPATH>"); + REQUIRE(switches.at(ManifestInstaller::InstallerSwitchType::Update) == "/update"); // installers REQUIRE(manifest.Installers.size() == 2); @@ -73,6 +75,7 @@ TEST_CASE("ReadGoodManifestAndVerifyContents", "[ManifestValidation]") REQUIRE(installer1.Scope == "user"); REQUIRE(installer1.PackageFamilyName == ""); REQUIRE(installer1.ProductCode == ""); + REQUIRE(installer1.UpdateBehavior == ManifestInstaller::UpdateBehaviorEnum::Install); auto installer1Switches = installer1.Switches; REQUIRE(installer1Switches.at(ManifestInstaller::InstallerSwitchType::Custom) == "/c"); @@ -82,6 +85,7 @@ TEST_CASE("ReadGoodManifestAndVerifyContents", "[ManifestValidation]") REQUIRE(installer1Switches.at(ManifestInstaller::InstallerSwitchType::Language) == "/en"); REQUIRE(installer1Switches.at(ManifestInstaller::InstallerSwitchType::Log) == "/l=<LOGPATH>"); REQUIRE(installer1Switches.at(ManifestInstaller::InstallerSwitchType::InstallLocation) == "/d=<INSTALLPATH>"); + REQUIRE(installer1Switches.at(ManifestInstaller::InstallerSwitchType::Update) == "/u"); ManifestInstaller installer2 = manifest.Installers.at(1); REQUIRE(installer2.Arch == Architecture::X64); @@ -92,6 +96,7 @@ TEST_CASE("ReadGoodManifestAndVerifyContents", "[ManifestValidation]") REQUIRE(installer2.Scope == "user"); REQUIRE(installer2.PackageFamilyName == ""); REQUIRE(installer2.ProductCode == ""); + REQUIRE(installer2.UpdateBehavior == ManifestInstaller::UpdateBehaviorEnum::UninstallPrevious); // Installer2 does not declare switches, it inherits switches from package default. auto installer2Switches = installer2.Switches; @@ -102,6 +107,7 @@ TEST_CASE("ReadGoodManifestAndVerifyContents", "[ManifestValidation]") REQUIRE(installer2Switches.at(ManifestInstaller::InstallerSwitchType::Language) == "/en-us"); REQUIRE(installer2Switches.at(ManifestInstaller::InstallerSwitchType::Log) == "/log=<LOGPATH>"); REQUIRE(installer2Switches.at(ManifestInstaller::InstallerSwitchType::InstallLocation) == "/dir=<INSTALLPATH>"); + REQUIRE(installer2Switches.at(ManifestInstaller::InstallerSwitchType::Update) == "/update"); // Localization REQUIRE(manifest.Localization.size() == 1); @@ -235,6 +241,7 @@ TEST_CASE("ReadBadManifests", "[ManifestValidation]") { "InstallFlowTest_MSStore.yaml", "Field value is not supported. Field: InstallerType Value: MSStore" }, { "Manifest-Bad-PackageFamilyNameOnMSI.yaml", "The specified installer type does not support PackageFamilyName. Field: InstallerType Value: Msi" }, { "Manifest-Bad-ProductCodeOnMSIX.yaml", "The specified installer type does not support ProductCode. Field: InstallerType Value: Msix" }, + { "Manifest-Bad-InvalidUpdateBehavior.yaml", "Invalid field value. Field: UpdateBehavior" }, }; for (auto const& testCase : TestCases) diff --git a/src/AppInstallerCommonCore/Architecture.cpp b/src/AppInstallerCommonCore/Architecture.cpp @@ -111,7 +111,7 @@ namespace AppInstaller::Utility } else { - return -1; + return InapplicableArchitecture; } } } \ No newline at end of file diff --git a/src/AppInstallerCommonCore/ExperimentalFeature.cpp b/src/AppInstallerCommonCore/ExperimentalFeature.cpp @@ -21,6 +21,8 @@ namespace AppInstaller::Settings return User().Get<Setting::EFExperimentalArg>(); case Feature::ExperimentalMSStore: return User().Get<Setting::EFExperimentalMSStore>(); + case Feature::ExperimentalUpgrade: + return User().Get<Setting::EFExperimentalUpgrade>(); default: THROW_HR(E_UNEXPECTED); } @@ -36,6 +38,8 @@ namespace AppInstaller::Settings return ExperimentalFeature{ "Argument Sample", "experimentalArg", "https://aka.ms/winget-settings", Feature::ExperimentalArg }; case Feature::ExperimentalMSStore: return ExperimentalFeature{ "Microsoft Store Support", "experimentalMSStore", "https://aka.ms/winget-settings", Feature::ExperimentalMSStore }; + case Feature::ExperimentalUpgrade: + return ExperimentalFeature{ "Upgrade Command", "upgrade", "https://aka.ms/winget-settings", Feature::ExperimentalUpgrade }; default: THROW_HR(E_UNEXPECTED); } diff --git a/src/AppInstallerCommonCore/Manifest/ManifestInstaller.cpp b/src/AppInstallerCommonCore/Manifest/ManifestInstaller.cpp @@ -50,6 +50,22 @@ namespace AppInstaller::Manifest return result; } + ManifestInstaller::UpdateBehaviorEnum ManifestInstaller::ConvertToUpdateBehaviorEnum(const std::string& in) + { + UpdateBehaviorEnum result = UpdateBehaviorEnum::Unknown; + + if (Utility::CaseInsensitiveEquals(in, "install")) + { + result = UpdateBehaviorEnum::Install; + } + else if (Utility::CaseInsensitiveEquals(in, "uninstallprevious")) + { + result = UpdateBehaviorEnum::UninstallPrevious; + } + + return result; + } + std::string ManifestInstaller::InstallerTypeToString(ManifestInstaller::InstallerTypeEnum installerType) { std::string result = "Unknown"; @@ -106,4 +122,30 @@ namespace AppInstaller::Manifest installerType == InstallerTypeEnum::Burn ); } + + bool ManifestInstaller::IsInstallerTypeCompatible(InstallerTypeEnum type1, InstallerTypeEnum type2) + { + if (type1 == InstallerTypeEnum::Unknown || type2 == InstallerTypeEnum::Unknown) + { + return false; + } + + std::vector<InstallerTypeEnum> compatList1 = + { + InstallerTypeEnum::Exe, + InstallerTypeEnum::Inno, + InstallerTypeEnum::Nullsoft, + InstallerTypeEnum::Burn, + }; + + std::vector<InstallerTypeEnum> compatList2 = + { + InstallerTypeEnum::Msi, + InstallerTypeEnum::Wix + }; + + return type1 == type2 || + (std::find(compatList1.begin(), compatList1.end(), type1) != compatList1.end() && std::find(compatList1.begin(), compatList1.end(), type2) != compatList1.end()) || + (std::find(compatList2.begin(), compatList2.end(), type1) != compatList2.end() && std::find(compatList2.begin(), compatList2.end(), type2) != compatList2.end()); + } } diff --git a/src/AppInstallerCommonCore/Manifest/ManifestValidation.cpp b/src/AppInstallerCommonCore/Manifest/ManifestValidation.cpp @@ -80,6 +80,11 @@ namespace AppInstaller::Manifest resultErrors.emplace_back(ManifestError::InvalidFieldValue, "InstallerType"); } + if (installer.UpdateBehavior == ManifestInstaller::UpdateBehaviorEnum::Unknown) + { + resultErrors.emplace_back(ManifestError::InvalidFieldValue, "UpdateBehavior"); + } + // Validate system reference strings if they are set at the installer level if (!installer.PackageFamilyName.empty() && !ManifestInstaller::DoesInstallerTypeUsePackageFamilyName(installer.InstallerType)) { diff --git a/src/AppInstallerCommonCore/Manifest/YamlParser.cpp b/src/AppInstallerCommonCore/Manifest/YamlParser.cpp @@ -72,6 +72,7 @@ namespace AppInstaller::Manifest { "Protocols", [this](const YAML::Node& value) { m_p_manifest->Protocols = SplitMultiValueField(value.as<std::string>()); } }, { "FileExtensions", [this](const YAML::Node& value) { m_p_manifest->FileExtensions = SplitMultiValueField(value.as<std::string>()); } }, { "InstallerType", [this](const YAML::Node& value) { m_p_manifest->InstallerType = ManifestInstaller::ConvertToInstallerTypeEnum(value.as<std::string>()); } }, + { "UpdateBehavior", [this](const YAML::Node& value) { m_p_manifest->UpdateBehavior = ManifestInstaller::ConvertToUpdateBehaviorEnum(value.as<std::string>()); } }, { "PackageFamilyName", [this](const YAML::Node& value) { m_p_manifest->PackageFamilyName = value.as<std::string>(); }, false, "[-.A-Za-z0-9]+_[A-Za-z0-9]{13}" }, { "ProductCode", [this](const YAML::Node& value) { m_p_manifest->ProductCode = value.as<std::string>(); } }, { "Description", [this](const YAML::Node& value) { m_p_manifest->Description = value.as<std::string>(); } }, @@ -91,6 +92,7 @@ namespace AppInstaller::Manifest { "Language", [this](const YAML::Node& value) { m_p_installer->Language = value.as<std::string>(); } }, { "Scope", [this](const YAML::Node& value) { m_p_installer->Scope = value.as<std::string>(); } }, { "InstallerType", [this](const YAML::Node& value) { m_p_installer->InstallerType = ManifestInstaller::ConvertToInstallerTypeEnum(value.as<std::string>()); } }, + { "UpdateBehavior", [this](const YAML::Node& value) { m_p_installer->UpdateBehavior = ManifestInstaller::ConvertToUpdateBehaviorEnum(value.as<std::string>()); } }, { "PackageFamilyName", [this](const YAML::Node& value) { m_p_installer->PackageFamilyName = value.as<std::string>(); }, false, "[-.A-Za-z0-9]+_[A-Za-z0-9]{13}" }, { "ProductCode", [this](const YAML::Node& value) { m_p_installer->ProductCode = value.as<std::string>(); } }, { "Switches", [this](const YAML::Node& value) { *m_p_switchesNode = value; } }, @@ -105,6 +107,7 @@ namespace AppInstaller::Manifest { "Language", [this](const YAML::Node& value) { (*m_p_switches)[ManifestInstaller::InstallerSwitchType::Language] = value.as<std::string>(); } }, { "Log", [this](const YAML::Node& value) { (*m_p_switches)[ManifestInstaller::InstallerSwitchType::Log] = value.as<std::string>(); } }, { "InstallLocation", [this](const YAML::Node& value) { (*m_p_switches)[ManifestInstaller::InstallerSwitchType::InstallLocation] = value.as<std::string>(); } }, + { "Update", [this](const YAML::Node& value) { (*m_p_switches)[ManifestInstaller::InstallerSwitchType::Update] = value.as<std::string>(); } }, }; LocalizationFieldInfos = @@ -246,6 +249,7 @@ namespace AppInstaller::Manifest // Populate defaults installer.InstallerType = manifest.InstallerType; + installer.UpdateBehavior = manifest.UpdateBehavior; installer.Scope = "user"; m_p_installer = &installer; @@ -421,7 +425,8 @@ namespace AppInstaller::Manifest {ManifestInstaller::InstallerSwitchType::Silent, ManifestInstaller::string_t("/quiet")}, {ManifestInstaller::InstallerSwitchType::SilentWithProgress, ManifestInstaller::string_t("/passive")}, {ManifestInstaller::InstallerSwitchType::Log, ManifestInstaller::string_t("/log \"" + std::string(ARG_TOKEN_LOGPATH) + "\"")}, - {ManifestInstaller::InstallerSwitchType::InstallLocation, ManifestInstaller::string_t("TARGETDIR=\"" + std::string(ARG_TOKEN_INSTALLPATH) + "\"")} + {ManifestInstaller::InstallerSwitchType::InstallLocation, ManifestInstaller::string_t("TARGETDIR=\"" + std::string(ARG_TOKEN_INSTALLPATH) + "\"")}, + {ManifestInstaller::InstallerSwitchType::Update, ManifestInstaller::string_t("REINSTALL=ALL REINSTALLMODE=vamus")} }; case ManifestInstaller::InstallerTypeEnum::Nullsoft: return diff --git a/src/AppInstallerCommonCore/Public/AppInstallerArchitecture.h b/src/AppInstallerCommonCore/Public/AppInstallerArchitecture.h @@ -6,6 +6,8 @@ namespace AppInstaller::Utility { + static const int InapplicableArchitecture = -1; + enum class Architecture { Unknown = -1, diff --git a/src/AppInstallerCommonCore/Public/AppInstallerErrors.h b/src/AppInstallerCommonCore/Public/AppInstallerErrors.h @@ -55,6 +55,8 @@ #define APPINSTALLER_CLI_ERROR_MANIFEST_VALIDATION_WARNING ((HRESULT)0x8A150028) #define APPINSTALLER_CLI_ERROR_MANIFEST_VALIDATION_FAILURE ((HRESULT)0x8A150029) #define APPINSTALLER_CLI_ERROR_INVALID_MANIFEST ((HRESULT)0x8A15002A) +#define APPINSTALLER_CLI_ERROR_UPDATE_NOT_APPLICABLE ((HRESULT)0x8A15002B) +#define APPINSTALLER_CLI_ERROR_UPDATE_ALL_HAS_FAILURE ((HRESULT)0x8A15002C) namespace AppInstaller { diff --git a/src/AppInstallerCommonCore/Public/winget/ExperimentalFeature.h b/src/AppInstallerCommonCore/Public/winget/ExperimentalFeature.h @@ -22,7 +22,8 @@ namespace AppInstaller::Settings ExperimentalCmd = 0x1, ExperimentalArg = 0x2, ExperimentalMSStore = 0x4, - Max = 0x8, // This MUST always be last + ExperimentalUpgrade = 0x8, + Max = 0x10, // This MUST always be last }; using Feature_t = std::underlying_type_t<ExperimentalFeature::Feature>; diff --git a/src/AppInstallerCommonCore/Public/winget/Manifest.h b/src/AppInstallerCommonCore/Public/winget/Manifest.h @@ -73,6 +73,9 @@ namespace AppInstaller::Manifest ManifestInstaller::InstallerTypeEnum InstallerType = ManifestInstaller::InstallerTypeEnum::Unknown; + // Default is Install if not specified + ManifestInstaller::UpdateBehaviorEnum UpdateBehavior = ManifestInstaller::UpdateBehaviorEnum::Install; + // Package family name for MSIX packaged installers. string_t PackageFamilyName; diff --git a/src/AppInstallerCommonCore/Public/winget/ManifestInstaller.h b/src/AppInstallerCommonCore/Public/winget/ManifestInstaller.h @@ -30,7 +30,14 @@ namespace AppInstaller::Manifest Exe, Burn, MSStore, - Unknown + Unknown, + }; + + enum class UpdateBehaviorEnum + { + Install, + UninstallPrevious, + Unknown, }; enum class InstallerSwitchType @@ -42,6 +49,7 @@ namespace AppInstaller::Manifest Language, Log, InstallLocation, + Update }; // Required. Values: x86, x64, arm, arm64, all. @@ -75,11 +83,16 @@ namespace AppInstaller::Manifest // If present, has more precedence than root InstallerTypeEnum InstallerType; + // Default is Install if not specified + UpdateBehaviorEnum UpdateBehavior; + // If present, has more precedence than root std::map<InstallerSwitchType, string_t> Switches; static InstallerTypeEnum ConvertToInstallerTypeEnum(const std::string& in); + static UpdateBehaviorEnum ConvertToUpdateBehaviorEnum(const std::string& in); + static std::string InstallerTypeToString(InstallerTypeEnum installerType); // Gets a value indicating whether the given installer type uses the PackageFamilyName system reference. @@ -87,5 +100,8 @@ namespace AppInstaller::Manifest // Gets a value indicating whether the given installer type uses the ProductCode system reference. static bool DoesInstallerTypeUseProductCode(InstallerTypeEnum installerType); + + // Checks whether 2 installer types are compatible. E.g. inno and exe are update compatible + static bool IsInstallerTypeCompatible(InstallerTypeEnum type1, InstallerTypeEnum type2); }; } \ No newline at end of file diff --git a/src/AppInstallerCommonCore/Public/winget/UserSettings.h b/src/AppInstallerCommonCore/Public/winget/UserSettings.h @@ -49,6 +49,7 @@ namespace AppInstaller::Settings EFExperimentalCmd, EFExperimentalArg, EFExperimentalMSStore, + EFExperimentalUpgrade, Max }; @@ -81,6 +82,7 @@ namespace AppInstaller::Settings SETTINGMAPPING_SPECIALIZATION(Setting::EFExperimentalCmd, bool, bool, false, ".experimentalFeatures.experimentalCmd"sv); SETTINGMAPPING_SPECIALIZATION(Setting::EFExperimentalArg, bool, bool, false, ".experimentalFeatures.experimentalArg"sv); SETTINGMAPPING_SPECIALIZATION(Setting::EFExperimentalMSStore, bool, bool, false, ".experimentalFeatures.experimentalMSStore"sv); + SETTINGMAPPING_SPECIALIZATION(Setting::EFExperimentalUpgrade, bool, bool, false, ".experimentalFeatures.experimentalUpgrade"sv); // Used to deduce the SettingVariant type; making a variant that includes std::monostate and all SettingMapping types. diff --git a/src/AppInstallerCommonCore/UserSettings.cpp b/src/AppInstallerCommonCore/UserSettings.cpp @@ -196,6 +196,12 @@ namespace AppInstaller::Settings { return value; } + + std::optional<SettingMapping<Setting::EFExperimentalUpgrade>::value_t> + SettingMapping<Setting::EFExperimentalUpgrade>::Validate(const SettingMapping<Setting::EFExperimentalUpgrade>::json_t& value) + { + return value; + } } UserSettings::UserSettings() : m_type(UserSettingsType::Default) diff --git a/src/AppInstallerRepositoryCore/Public/AppInstallerRepositorySource.h b/src/AppInstallerRepositoryCore/Public/AppInstallerRepositorySource.h @@ -102,6 +102,10 @@ namespace AppInstaller::Repository // These sources are not under the direct control of the user, such as packages installed on the system. std::shared_ptr<ISource> OpenPredefinedSource(PredefinedSource source, IProgressCallback& progress); + // Creates a composite source from input sources. + // The composite source will correlate entries from input sources. + std::shared_ptr<ISource> CreateCompositeSource(std::shared_ptr<ISource>& source1, std::shared_ptr<ISource>& source2); + // Updates an existing source. // Return value indicates whether the named source was found. bool UpdateSource(std::string_view name, IProgressCallback& progress); diff --git a/src/AppInstallerRepositoryCore/RepositorySource.cpp b/src/AppInstallerRepositoryCore/RepositorySource.cpp @@ -604,6 +604,12 @@ namespace AppInstaller::Repository THROW_HR(E_UNEXPECTED); } + std::shared_ptr<ISource> CreateCompositeSource(std::shared_ptr<ISource>& source1, std::shared_ptr<ISource>&) + { + // TODO: needs implementation + return source1; + } + bool UpdateSource(std::string_view name, IProgressCallback& progress) { THROW_HR_IF(E_INVALIDARG, name.empty());