winget-cli

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

commit d0ff81ed62ca582a5def366f561668909fc4838d
parent a5e202c7ceff44515489b077aefc122f0ac1cec0
Author: Flor Chacón <lechacon@users.noreply.github.com>
Date:   Wed,  3 Aug 2022 15:46:18 -0600

Add support for `RequireExplicitUpgrade` manifest element (#1795)


Diffstat:
Msrc/AppInstallerCLICore/Commands/UpgradeCommand.cpp | 31++++++++++++++++++-------------
Msrc/AppInstallerCLICore/ExecutionArgs.h | 6++++--
Msrc/AppInstallerCLICore/Resources.h | 6++++--
Msrc/AppInstallerCLICore/Workflows/UpdateFlow.cpp | 109+++++++++++++++++++++++++++++++++++++++++++++++++++----------------------------
Msrc/AppInstallerCLICore/Workflows/WorkflowBase.cpp | 89++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++-----------------
Msrc/AppInstallerCLIPackage/Shared/Strings/en-us/winget.resw | 15+++++++++++++++
Msrc/AppInstallerCLITests/WorkFlow.cpp | 131+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++--------
Msrc/AppInstallerRepositoryCore/PackageTrackingCatalog.cpp | 6+++++-
Msrc/AppInstallerRepositoryCore/Public/winget/RepositorySearch.h | 13+++++++++++++
Msrc/AppInstallerRepositoryCore/RepositorySearch.cpp | 24++++++++++++++++++++++++
10 files changed, 343 insertions(+), 87 deletions(-)

diff --git a/src/AppInstallerCLICore/Commands/UpgradeCommand.cpp b/src/AppInstallerCLICore/Commands/UpgradeCommand.cpp @@ -43,7 +43,8 @@ namespace AppInstaller::CLI // either for upgrading or for listing available upgrades. bool HasArgumentsForMultiplePackages(Execution::Args& execArgs) { - return execArgs.Contains(Args::Type::All); + return execArgs.Contains(Args::Type::All) || + execArgs.Contains(Args::Type::IncludeUnknown); } // Determines whether there are any arguments only used as options during an upgrade, @@ -102,7 +103,7 @@ namespace AppInstaller::CLI Argument::ForType(Args::Type::AcceptSourceAgreements), Argument::ForType(Execution::Args::Type::CustomHeader), Argument{ "all", Argument::NoAlias, Args::Type::All, Resource::String::UpdateAllArgumentDescription, ArgumentType::Flag }, - Argument{ "include-unknown", Argument::NoAlias, Args::Type::IncludeUnknown, Resource::String::IncludeUnknownArgumentDescription, ArgumentType::Flag } + Argument{ "include-unknown", Argument::NoAlias, Args::Type::IncludeUnknown, Resource::String::IncludeUnknownArgumentDescription, ArgumentType::Flag }, }; } @@ -158,25 +159,29 @@ namespace AppInstaller::CLI void UpgradeCommand::ValidateArgumentsInternal(Execution::Args& execArgs) const { - if (execArgs.Contains(Execution::Args::Type::Manifest) && + if (execArgs.Contains(Execution::Args::Type::Manifest) && (HasSearchQueryArguments(execArgs) || - HasArgumentsForMultiplePackages(execArgs) || - HasArgumentsForSource(execArgs))) + HasArgumentsForMultiplePackages(execArgs) || + HasArgumentsForSource(execArgs))) { throw CommandException(Resource::String::BothManifestAndSearchQueryProvided); } - - else if (!ShouldListUpgrade(execArgs) - && !HasSearchQueryArguments(execArgs) - && (execArgs.Contains(Args::Type::Log) || - execArgs.Contains(Args::Type::Override) || - execArgs.Contains(Args::Type::InstallLocation) || - execArgs.Contains(Args::Type::HashOverride) || - execArgs.Contains(Args::Type::AcceptPackageAgreements))) + if (!ShouldListUpgrade(execArgs) + && !HasSearchQueryArguments(execArgs) + && (execArgs.Contains(Args::Type::Log) || + execArgs.Contains(Args::Type::Override) || + execArgs.Contains(Args::Type::InstallLocation) || + execArgs.Contains(Args::Type::HashOverride) || + execArgs.Contains(Args::Type::AcceptPackageAgreements))) { throw CommandException(Resource::String::InvalidArgumentWithoutQueryError); } + + if (HasArgumentsForSinglePackage(execArgs) && HasArgumentsForMultiplePackages(execArgs)) + { + throw CommandException(Resource::String::IncompatibleArgumentsProvided); + } } void UpgradeCommand::ExecuteInternal(Execution::Context& context) const diff --git a/src/AppInstallerCLICore/ExecutionArgs.h b/src/AppInstallerCLICore/ExecutionArgs.h @@ -79,8 +79,11 @@ namespace AppInstaller::CLI::Execution AdminSettingEnable, AdminSettingDisable, + // Upgrade Command + All, // Update all installed packages to latest + IncludeUnknown, // Allow upgrades of packages with unknown versions + // Other - All, // Used in Update command to update all installed packages to latest ListVersions, // Used in Show command to list all available versions of an app NoVT, // Disable VirtualTerminal outputs RetroStyle, // Makes progress display as retro @@ -91,7 +94,6 @@ namespace AppInstaller::CLI::Execution DependencySource, // Index source to be queried against for finding dependencies CustomHeader, // Optional Rest source header AcceptSourceAgreements, // Accept all source agreements - IncludeUnknown, // Used in Upgrade command to allow upgrades of packages with unknown versions Wait, // Prompts the user to press any key before exiting // Used for demonstration purposes diff --git a/src/AppInstallerCLICore/Resources.h b/src/AppInstallerCLICore/Resources.h @@ -109,6 +109,7 @@ namespace AppInstaller::CLI::Resource WINGET_DEFINE_RESOURCE_STRINGID(ImportSearchFailed); WINGET_DEFINE_RESOURCE_STRINGID(ImportSourceNotInstalled); WINGET_DEFINE_RESOURCE_STRINGID(IncludeUnknownArgumentDescription); + WINGET_DEFINE_RESOURCE_STRINGID(IncompatibleArgumentsProvided); WINGET_DEFINE_RESOURCE_STRINGID(InstallAndUpgradeCommandsReportDependencies); WINGET_DEFINE_RESOURCE_STRINGID(InstallArchitectureArgumentDescription); WINGET_DEFINE_RESOURCE_STRINGID(InstallationAbandoned); @@ -373,12 +374,13 @@ namespace AppInstaller::CLI::Resource WINGET_DEFINE_RESOURCE_STRINGID(UnsupportedArgument); WINGET_DEFINE_RESOURCE_STRINGID(UpdateAllArgumentDescription); WINGET_DEFINE_RESOURCE_STRINGID(UpdateNotApplicable); + WINGET_DEFINE_RESOURCE_STRINGID(UpgradeAvailableForPinned); WINGET_DEFINE_RESOURCE_STRINGID(UpgradeCommandLongDescription); WINGET_DEFINE_RESOURCE_STRINGID(UpgradeCommandShortDescription); WINGET_DEFINE_RESOURCE_STRINGID(UpgradeDifferentInstallTechnology); WINGET_DEFINE_RESOURCE_STRINGID(UpgradeDifferentInstallTechnologyInNewerVersions); - WINGET_DEFINE_RESOURCE_STRINGID(UpgradeUnknownCount); - WINGET_DEFINE_RESOURCE_STRINGID(UpgradeUnknownCountSingle); + WINGET_DEFINE_RESOURCE_STRINGID(UpgradeRequireExplicitCount); + WINGET_DEFINE_RESOURCE_STRINGID(UpgradeUnknownVersionCount); WINGET_DEFINE_RESOURCE_STRINGID(UpgradeUnknownVersionExplanation); WINGET_DEFINE_RESOURCE_STRINGID(Usage); WINGET_DEFINE_RESOURCE_STRINGID(ValidateCommandLongDescription); diff --git a/src/AppInstallerCLICore/Workflows/UpdateFlow.cpp b/src/AppInstallerCLICore/Workflows/UpdateFlow.cpp @@ -44,32 +44,41 @@ namespace AppInstaller::CLI::Workflow bool updateFound = false; bool installedTypeInapplicable = false; - if (!installedVersion.IsUnknown() || context.Args.Contains(Execution::Args::Type::IncludeUnknown)) + if (installedVersion.IsUnknown() && !context.Args.Contains(Execution::Args::Type::IncludeUnknown)) { - // The version keys should have already been sorted by version - const auto& versionKeys = package->GetAvailableVersionKeys(); - for (const auto& key : versionKeys) + // the package has an unknown version and the user did not request to upgrade it anyway. + if (m_reportUpdateNotFound) { - // Check Update Version - if (IsUpdateVersionApplicable(installedVersion, Utility::Version(key.Version))) - { - auto packageVersion = package->GetAvailableVersion(key); - auto manifest = packageVersion->GetManifest(); + context.Reporter.Info() << Resource::String::UpgradeUnknownVersionExplanation << std::endl; + } + + AICLI_TERMINATE_CONTEXT(APPINSTALLER_CLI_ERROR_UPDATE_NOT_APPLICABLE); + } + + // The version keys should have already been sorted by version + const auto& versionKeys = package->GetAvailableVersionKeys(); + for (const auto& key : versionKeys) + { + // Check Update Version + if (IsUpdateVersionApplicable(installedVersion, Utility::Version(key.Version))) + { + auto packageVersion = package->GetAvailableVersion(key); + auto manifest = packageVersion->GetManifest(); - // Check applicable Installer - auto [installer, inapplicabilities] = manifestComparator.GetPreferredInstaller(manifest); - if (!installer.has_value()) + // Check applicable Installer + auto [installer, inapplicabilities] = manifestComparator.GetPreferredInstaller(manifest); + if (!installer.has_value()) + { + // If there is at least one installer whose only reason is InstalledType. + auto onlyInstalledType = std::find(inapplicabilities.begin(), inapplicabilities.end(), InapplicabilityFlags::InstalledType); + if (onlyInstalledType != inapplicabilities.end()) { - // If there is at least one installer whose only reason is InstalledType. - auto onlyInstalledType = std::find(inapplicabilities.begin(), inapplicabilities.end(), InapplicabilityFlags::InstalledType); - if (onlyInstalledType != inapplicabilities.end()) - { - installedTypeInapplicable = true; - } - - continue; + installedTypeInapplicable = true; } + continue; + } + Logging::Telemetry().LogSelectedInstaller( static_cast<int>(installer->Arch), installer->Url, @@ -88,25 +97,16 @@ namespace AppInstaller::CLI::Workflow context.Add<Execution::Data::PackageVersion>(std::move(packageVersion)); context.Add<Execution::Data::Installer>(std::move(installer)); - updateFound = true; - break; - } - else - { - // Any following versions are not applicable - break; - } + updateFound = true; + break; } - } - else - { - // the package has an unknown version and the user did not request to upgrade it anyway. - if (m_reportUpdateNotFound) + else { - context.Reporter.Info() << Resource::String::UpgradeUnknownVersionExplanation << std::endl; + // Any following versions are not applicable + break; } - AICLI_TERMINATE_CONTEXT(APPINSTALLER_CLI_ERROR_UPDATE_NOT_APPLICABLE); } + if (!updateFound) { if (m_reportUpdateNotFound) @@ -143,7 +143,8 @@ namespace AppInstaller::CLI::Workflow const auto& matches = context.Get<Execution::Data::SearchResult>().Matches; std::vector<std::unique_ptr<Execution::Context>> packagesToInstall; bool updateAllFoundUpdate = false; - int unknownPackagesCount = 0; + int packagesWithUnknownVersionSkipped = 0; + int packagesThatRequireExplicitSkipped = 0; for (const auto& match : matches) { @@ -154,7 +155,8 @@ namespace AppInstaller::CLI::Workflow auto installedVersion = match.Package->GetInstalledVersion(); updateContext.Add<Execution::Data::Package>(match.Package); - + + // Filter out packages with unknown installed versions if (context.Args.Contains(Execution::Args::Type::IncludeUnknown)) { updateContext.Args.AddArg(Execution::Args::Type::IncludeUnknown); @@ -162,7 +164,8 @@ namespace AppInstaller::CLI::Workflow else if (Utility::Version(installedVersion->GetProperty(PackageVersionProperty::Version)).IsUnknown()) { // we don't know what the package's version is and the user didn't ask to upgrade it anyway. - unknownPackagesCount++; + AICLI_LOG(CLI, Info, << "Skipping " << match.Package->GetProperty(PackageProperty::Id) << " as it has unknown installed version"); + ++packagesWithUnknownVersionSkipped; continue; } @@ -176,6 +179,24 @@ namespace AppInstaller::CLI::Workflow continue; } + // Filter out packages that require explicit upgrades. + // We require explicit upgrades only if the installed version is pinned, + // either because it was manually pinned or because the manifest indicated + // RequireExplicitUpgrade. + // Note that this does not consider whether the update to be installed has + // RequireExplicitUpgrade. While this has the downside of not working with + // packages installed from another source, it ensures consistency with the + // list of available updates (there we don't have the selected installer) + // and at most we will update each package like this once. + auto installedMetadata = updateContext.Get<Execution::Data::InstalledPackageVersion>()->GetMetadata(); + auto pinnedState = ConvertToPackagePinnedStateEnum(installedMetadata[PackageVersionMetadata::PinnedState]); + if (pinnedState != PackagePinnedState::NotPinned) + { + AICLI_LOG(CLI, Info, << "Skipping " << match.Package->GetProperty(PackageProperty::Id) << " as it requires explicit upgrade"); + ++packagesThatRequireExplicitSkipped; + continue; + } + updateAllFoundUpdate = true; AddToPackagesToInstallIfNotPresent(packagesToInstall, std::move(updateContextPtr)); @@ -191,5 +212,17 @@ namespace AppInstaller::CLI::Workflow APPINSTALLER_CLI_ERROR_UPDATE_ALL_HAS_FAILURE, { APPINSTALLER_CLI_ERROR_UPDATE_NOT_APPLICABLE }); } + + if (packagesWithUnknownVersionSkipped > 0) + { + AICLI_LOG(CLI, Info, << packagesWithUnknownVersionSkipped << " package(s) skipped due to unknown installed version"); + context.Reporter.Info() << packagesWithUnknownVersionSkipped << " " << Resource::String::UpgradeUnknownVersionCount << std::endl; + } + + if (packagesThatRequireExplicitSkipped > 0) + { + AICLI_LOG(CLI, Info, << packagesThatRequireExplicitSkipped << " package(s) skipped due to requiring explicit upgrade"); + context.Reporter.Info() << packagesThatRequireExplicitSkipped << " " << Resource::String::UpgradeRequireExplicitCount << std::endl; + } } } diff --git a/src/AppInstallerCLICore/Workflows/WorkflowBase.cpp b/src/AppInstallerCLICore/Workflows/WorkflowBase.cpp @@ -218,6 +218,44 @@ namespace AppInstaller::CLI::Workflow return accepted; } + + // Data shown on a line of a table displaying installed packages + struct InstalledPackagesTableLine + { + InstalledPackagesTableLine(Utility::LocIndString name, Utility::LocIndString id, Utility::LocIndString installedVersion, Utility::LocIndString availableVersion, Utility::LocIndString source) + : Name(name), Id(id), InstalledVersion(installedVersion), AvailableVersion(availableVersion), Source(source) {} + + Utility::LocIndString Name; + Utility::LocIndString Id; + Utility::LocIndString InstalledVersion; + Utility::LocIndString AvailableVersion; + Utility::LocIndString Source; + }; + + void OutputInstalledPackagesTable(Execution::Context& context, const std::vector<InstalledPackagesTableLine>& lines) + { + Execution::TableOutput<5> table(context.Reporter, + { + Resource::String::SearchName, + Resource::String::SearchId, + Resource::String::SearchVersion, + Resource::String::AvailableHeader, + Resource::String::SearchSource + }); + + for (const auto& line : lines) + { + table.OutputLine({ + line.Name, + line.Id, + line.InstalledVersion, + line.AvailableVersion, + line.Source + }); + } + + table.Complete(); + } } bool WorkflowTask::operator==(const WorkflowTask& other) const @@ -701,17 +739,11 @@ namespace AppInstaller::CLI::Workflow { auto& searchResult = context.Get<Execution::Data::SearchResult>(); - Execution::TableOutput<5> table(context.Reporter, - { - Resource::String::SearchName, - Resource::String::SearchId, - Resource::String::SearchVersion, - Resource::String::AvailableHeader, - Resource::String::SearchSource - }); + std::vector<InstalledPackagesTableLine> lines; + std::vector<InstalledPackagesTableLine> linesForExplicitUpgrade; int availableUpgradesCount = 0; - int unknownPackagesCount = 0; + int packagesWithUnknownVersionSkipped = 0; auto &source = context.Get<Execution::Data::Source>(); bool shouldShowSource = source.IsComposite() && source.GetAvailableSources().size() > 1; @@ -727,7 +759,7 @@ namespace AppInstaller::CLI::Workflow if (m_onlyShowUpgrades && !context.Args.Contains(Execution::Args::Type::IncludeUnknown) && Utility::Version(installedVersion->GetProperty(PackageVersionProperty::Version)).IsUnknown() && updateAvailable) { // We are only showing upgrades, and the user did not request to include packages with unknown versions. - unknownPackagesCount++; + ++packagesWithUnknownVersionSkipped; continue; } @@ -751,22 +783,31 @@ namespace AppInstaller::CLI::Workflow // Output using the local PackageName instead of the name in the manifest, to prevent confusion for packages that add multiple // Add/Remove Programs entries. // TODO: De-duplicate this list, and only show (by default) one entry per matched package. - table.OutputLine({ + InstalledPackagesTableLine line( installedVersion->GetProperty(PackageVersionProperty::Name), match.Package->GetProperty(PackageProperty::Id), installedVersion->GetProperty(PackageVersionProperty::Version), availableVersion, - shouldShowSource ? sourceName : ""s - }); - - + shouldShowSource ? sourceName : Utility::LocIndString() + ); + + auto pinnedState = ConvertToPackagePinnedStateEnum(installedVersion->GetMetadata()[PackageVersionMetadata::PinnedState]); + bool requiresExplicitUpgrade = m_onlyShowUpgrades && pinnedState != PackagePinnedState::NotPinned; + if (requiresExplicitUpgrade) + { + linesForExplicitUpgrade.push_back(std::move(line)); + } + else + { + lines.push_back(std::move(line)); + } } } } - table.Complete(); + OutputInstalledPackagesTable(context, lines); - if (table.IsEmpty()) + if (lines.empty()) { context.Reporter.Info() << Resource::String::NoInstalledPackageFound << std::endl; } @@ -782,11 +823,21 @@ namespace AppInstaller::CLI::Workflow context.Reporter.Info() << availableUpgradesCount << ' ' << Resource::String::AvailableUpgrades << std::endl; } } - if (m_onlyShowUpgrades && unknownPackagesCount > 0 && !context.Args.Contains(Execution::Args::Type::IncludeUnknown)) + + if (!linesForExplicitUpgrade.empty()) { - context.Reporter.Info() << unknownPackagesCount << " " << (unknownPackagesCount == 1 ? Resource::String::UpgradeUnknownCountSingle : Resource::String::UpgradeUnknownCount) << std::endl; + context.Reporter.Info() << std::endl << Resource::String::UpgradeAvailableForPinned << std::endl; + OutputInstalledPackagesTable(context, linesForExplicitUpgrade); } + if (m_onlyShowUpgrades) + { + if (packagesWithUnknownVersionSkipped > 0) + { + AICLI_LOG(CLI, Info, << packagesWithUnknownVersionSkipped << " package(s) skipped due to unknown installed version"); + context.Reporter.Info() << packagesWithUnknownVersionSkipped << " " << Resource::String::UpgradeUnknownVersionCount << std::endl; + } + } } void EnsureMatchesFromSearchResult::operator()(Execution::Context& context) const diff --git a/src/AppInstallerCLIPackage/Shared/Strings/en-us/winget.resw b/src/AppInstallerCLIPackage/Shared/Strings/en-us/winget.resw @@ -1275,6 +1275,14 @@ Please specify one of them using the `--source` option to proceed.</value> <value>package has a version number that cannot be determined. Using "--include-unknown" may show more results.</value> <comment>{Locked="--include-unknown"} This string is preceded by a (integer) number of packages that do not have notated versions.</comment> </data> + <data name="UpgradeUnknownVersionCount" xml:space="preserve"> + <value>package(s) have version numbers that cannot be determined. Use "--include-unknown" to see all results.</value> + <comment>{Locked="--include-unknown"} This string is preceded by a (integer) number of packages that do not have notated versions. </comment> + </data> + <data name="UpgradeRequireExplicitCount" xml:space="preserve"> + <value>package(s) is pinned and needs to be explicitly upgraded.</value> + <comment>This string is preceded by a (integer) number of packages that require explicit upgrades.</comment> + </data> <data name="InvalidArgumentWithoutQueryError" xml:space="preserve"> <value>The arguments provided can only be used with a query.</value> </data> @@ -1382,4 +1390,11 @@ Please specify one of them using the `--source` option to proceed.</value> <data name="MultipleNonPortableNestedInstallersSpecified" xml:space="preserve"> <value>Only one non-portable nested installer can be specified for an archive installer</value> </data> + <data name="IncompatibleArgumentsProvided" xml:space="preserve"> + <value>Incompatible command line arguments provided</value> + </data> + <data name="UpgradeAvailableForPinned" xml:space="preserve"> + <value>The following packages have an upgrade available, but require explicit targeting for upgrade:</value> + <comment>"require explicit targeting for upgrade" means that the package will not be upgraded with all others unless an extra flag is added, or the package is mentioned explicitly</comment> + </data> </root> \ No newline at end of file diff --git a/src/AppInstallerCLITests/WorkFlow.cpp b/src/AppInstallerCLITests/WorkFlow.cpp @@ -105,9 +105,16 @@ namespace } }; + enum TestSourceSearchOptions + { + None = 0, + UpgradeUsesAgreements, + UpgradeRequiresExplicit, + }; + struct WorkflowTestCompositeSource : public TestSource { - WorkflowTestCompositeSource(bool upgradeUsesLicenses) : m_upgradeUsesLicenses(upgradeUsesLicenses) {} + WorkflowTestCompositeSource(TestSourceSearchOptions searchOptions = TestSourceSearchOptions::None) : m_searchOptions(searchOptions) {} SearchResult Search(const SearchRequest& request) const override { @@ -133,7 +140,17 @@ namespace { auto manifest = YamlParser::CreateFromPath(TestDataFile("InstallFlowTest_Exe.yaml")); auto manifest2 = YamlParser::CreateFromPath(TestDataFile("UpdateFlowTest_Exe.yaml")); - auto manifest3 = YamlParser::CreateFromPath(TestDataFile(m_upgradeUsesLicenses ? "UpdateFlowTest_Exe_2_LicenseAgreement.yaml" : "UpdateFlowTest_Exe_2.yaml")); + Manifest manifest3; + switch (m_searchOptions) + { + case TestSourceSearchOptions::UpgradeUsesAgreements: + manifest3 = YamlParser::CreateFromPath(TestDataFile("UpdateFlowTest_Exe_2_LicenseAgreement.yaml")); + break; + default: + manifest3 = YamlParser::CreateFromPath(TestDataFile("UpdateFlowTest_Exe_2.yaml")); + break; + } + auto testPackage = TestPackage::Make( manifest, @@ -171,12 +188,33 @@ namespace if (input.empty() || input == "AppInstallerCliTest.TestMsixInstaller") { auto manifest = YamlParser::CreateFromPath(TestDataFile("InstallFlowTest_Msix_StreamingFlow.yaml")); - auto manifest2 = YamlParser::CreateFromPath(TestDataFile(m_upgradeUsesLicenses ? "UpdateFlowTest_Msix_LicenseAgreement.yaml" : "UpdateFlowTest_Msix.yaml")); + Manifest manifest2; + switch (m_searchOptions) + { + case TestSourceSearchOptions::UpgradeUsesAgreements: + manifest2 = YamlParser::CreateFromPath(TestDataFile("UpdateFlowTest_Msix_LicenseAgreement.yaml")); + break; + case TestSourceSearchOptions::None: + default: + manifest2 = YamlParser::CreateFromPath(TestDataFile("UpdateFlowTest_Msix.yaml")); + break; + } + + TestPackage::MetadataMap packageMetadata + { + { PackageVersionMetadata::InstalledType, "Msix" }, + }; + + if (m_searchOptions == TestSourceSearchOptions::UpgradeRequiresExplicit) + { + packageMetadata[PackageVersionMetadata::PinnedState] = "PinnedByManifest"; + } + result.Matches.emplace_back( ResultMatch( TestPackage::Make( manifest, - TestPackage::MetadataMap{ { PackageVersionMetadata::InstalledType, "Msix" } }, + packageMetadata, std::vector<Manifest>{ manifest2, manifest }, shared_from_this() ), @@ -338,7 +376,7 @@ namespace { auto manifest = YamlParser::CreateFromPath(TestDataFile("InstallFlowTest_Exe.yaml")); auto manifest2 = YamlParser::CreateFromPath(TestDataFile("UpdateFlowTest_Exe.yaml")); - auto manifest3 = YamlParser::CreateFromPath(TestDataFile(m_upgradeUsesLicenses ? "UpdateFlowTest_Exe_2_LicenseAgreement.yaml" : "UpdateFlowTest_Exe_2.yaml")); + auto manifest3 = YamlParser::CreateFromPath(TestDataFile("UpdateFlowTest_Exe_2.yaml")); result.Matches.emplace_back( ResultMatch( TestPackage::Make( @@ -373,7 +411,7 @@ namespace } private: - bool m_upgradeUsesLicenses; + TestSourceSearchOptions m_searchOptions; }; struct TestContext; @@ -479,7 +517,7 @@ void OverrideForOpenSource(TestContext& context) } }); } -void OverrideForCompositeInstalledSource(TestContext& context, bool upgradeUsesLicenses = false) +void OverrideForCompositeInstalledSource(TestContext& context, TestSourceSearchOptions searchOptions = TestSourceSearchOptions::None) { context.Override({ "OpenSource", [](TestContext&) { @@ -487,7 +525,7 @@ void OverrideForCompositeInstalledSource(TestContext& context, bool upgradeUsesL context.Override({ "OpenCompositeSource", [=](TestContext& context) { - context.Add<Execution::Data::Source>(Source{ std::make_shared<WorkflowTestCompositeSource>(upgradeUsesLicenses) }); + context.Add<Execution::Data::Source>(Source{ std::make_shared<WorkflowTestCompositeSource>(searchOptions) }); } }); } @@ -495,13 +533,13 @@ void OverrideForImportSource(TestContext& context, bool useTestCompositeSource = { context.Override({ "OpenPredefinedSource", [=](TestContext& context) { - auto installedSource = useTestCompositeSource? std::make_shared<WorkflowTestCompositeSource>(false) : std::make_shared<TestSource>(); + auto installedSource = useTestCompositeSource ? std::make_shared<WorkflowTestCompositeSource>() : std::make_shared<TestSource>(); context.Add<Execution::Data::Source>(Source{ installedSource }); } }); context.Override({ Workflow::OpenSourcesForImport, [](TestContext& context) { - context.Add<Execution::Data::Sources>(std::vector<Source>{ Source{ std::make_shared<WorkflowTestCompositeSource>(false) } }); + context.Add<Execution::Data::Sources>(std::vector<Source>{ Source{ std::make_shared<WorkflowTestCompositeSource>() } }); } }); } @@ -2212,7 +2250,7 @@ TEST_CASE("UpdateFlow_All_LicenseAgreement", "[UpdateFlow][workflow]") std::ostringstream updateOutput; TestContext context{ updateOutput, std::cin }; auto previousThreadGlobals = context.SetForCurrentThread(); - OverrideForCompositeInstalledSource(context, /* upgradeUsesLicenses */ true); + OverrideForCompositeInstalledSource(context, TestSourceSearchOptions::UpgradeUsesAgreements); OverrideForShellExecute(context); OverrideForMSIX(context); OverrideForMSStore(context, true); @@ -2249,7 +2287,7 @@ TEST_CASE("UpdateFlow_All_LicenseAgreement_NotAccepted", "[UpdateFlow][workflow] std::ostringstream updateOutput; TestContext context{ updateOutput, updateInput }; auto previousThreadGlobals = context.SetForCurrentThread(); - OverrideForCompositeInstalledSource(context, /* upgradeUsesLicenses */ true); + OverrideForCompositeInstalledSource(context, TestSourceSearchOptions::UpgradeUsesAgreements); context.Args.AddArg(Execution::Args::Type::All); UpgradeCommand update({}); @@ -2286,6 +2324,75 @@ TEST_CASE("UninstallFlow_UninstallPortable", "[UninstallFlow][workflow]") REQUIRE(std::filesystem::exists(uninstallResultPath.GetPath())); } +TEST_CASE("UpdateFlow_RequireExplicit", "[UpdateFlow][workflow]") +{ + TestCommon::TempFile updateExeResultPath("TestExeInstalled.txt"); + TestCommon::TempFile updateMsixResultPath("TestMsixInstalled.txt"); + + std::ostringstream updateOutput; + TestContext context{ updateOutput, std::cin }; + auto previousThreadGlobals = context.SetForCurrentThread(); + + // Msix package has an update that requires explicit upgrade. + // Exe, Portable, MSStore, Zip are also listed with an available upgrade. + OverrideForCompositeInstalledSource(context, TestSourceSearchOptions::UpgradeRequiresExplicit); + + SECTION("List available upgrades") + { + UpgradeCommand update({}); + update.Execute(context); + INFO(updateOutput.str()); + + // The package that requires explicit upgrade is listed below the header for pinned packages + REQUIRE(updateOutput.str().find("AppInstallerCliTest.TestExeInstaller") != std::string::npos); + + auto pinnedPackagesHeaderPosition = updateOutput.str().find(Resource::LocString(Resource::String::UpgradeAvailableForPinned)); + auto pinnedPackageLinePosition = updateOutput.str().find("AppInstallerCliTest.TestMsixInstaller"); + REQUIRE(pinnedPackagesHeaderPosition != std::string::npos); + REQUIRE(pinnedPackageLinePosition != std::string::npos); + REQUIRE(pinnedPackagesHeaderPosition < pinnedPackageLinePosition); + REQUIRE(updateOutput.str().find(Resource::LocString(Resource::String::UpgradeRequireExplicitCount)) == std::string::npos); + } + + SECTION("Upgrade all except pinned") + { + context.Args.AddArg(Args::Type::All); + OverrideForMSStore(context, true); + OverrideForPortableInstall(context); + OverrideForShellExecute(context); + OverrideForExtractInstallerFromArchive(context); + OverrideForVerifyAndSetNestedInstaller(context); + + UpgradeCommand update({}); + update.Execute(context); + INFO(updateOutput.str()); + + auto s = updateOutput.str(); + + // Verify message is printed for skipped package + REQUIRE(updateOutput.str().find(Resource::LocString(Resource::String::UpgradeRequireExplicitCount)) != std::string::npos); + + // Verify package is not installed, but all others are + REQUIRE(std::filesystem::exists(updateExeResultPath.GetPath())); + REQUIRE(!std::filesystem::exists(updateMsixResultPath.GetPath())); + } + + SECTION("Upgrade explicitly") + { + context.Args.AddArg(Execution::Args::Type::Query, "AppInstallerCliTest.TestMsixInstaller"sv); + OverrideForMSIX(context); + + UpgradeCommand update({}); + update.Execute(context); + INFO(updateOutput.str()); + + REQUIRE(std::filesystem::exists(updateMsixResultPath.GetPath())); + } + + // Command should always succeed + REQUIRE(context.GetTerminationHR() == S_OK); +} + TEST_CASE("UninstallFlow_UninstallExe", "[UninstallFlow][workflow]") { TestCommon::TempFile uninstallResultPath("TestExeUninstalled.txt"); diff --git a/src/AppInstallerRepositoryCore/PackageTrackingCatalog.cpp b/src/AppInstallerRepositoryCore/PackageTrackingCatalog.cpp @@ -215,7 +215,6 @@ namespace AppInstaller::Repository bool isUpgrade) { // TODO: Store additional information from these if needed - UNREFERENCED_PARAMETER(installer); UNREFERENCED_PARAMETER(isUpgrade); auto& index = m_implementation->Source->GetIndex(); @@ -239,6 +238,11 @@ namespace AppInstaller::Repository strstr << Utility::GetCurrentUnixEpoch(); index.SetMetadataByManifestId(manifestId, PackageVersionMetadata::TrackingWriteTime, strstr.str()); + if (installer.RequireExplicitUpgrade) + { + index.SetMetadataByManifestId(manifestId, PackageVersionMetadata::PinnedState, ToString(PackagePinnedState::PinnedByManifest)); + } + std::shared_ptr<Version::implementation> result = std::make_shared<Version::implementation>(); result->Id = manifestId; return { std::move(result) }; diff --git a/src/AppInstallerRepositoryCore/Public/winget/RepositorySearch.h b/src/AppInstallerRepositoryCore/Public/winget/RepositorySearch.h @@ -178,11 +178,24 @@ namespace AppInstaller::Repository TrackingWriteTime, // The Architecture of an installed package InstalledArchitecture, + // The PackagePinnedState of the installed package + PinnedState, }; // Convert a PackageVersionMetadata to a string. std::string_view ToString(PackageVersionMetadata pvm); + // Possible pinned states for a package. + // Pinned packages need to be explicitly updated (i.e., are not included in `upgrade --all`) + enum class PackagePinnedState + { + NotPinned, + PinnedByManifest, + }; + + std::string_view ToString(PackagePinnedState state); + PackagePinnedState ConvertToPackagePinnedStateEnum(std::string_view in); + // A single package version. struct IPackageVersion { diff --git a/src/AppInstallerRepositoryCore/RepositorySearch.cpp b/src/AppInstallerRepositoryCore/RepositorySearch.cpp @@ -88,6 +88,30 @@ namespace AppInstaller::Repository } } + std::string_view ToString(PackagePinnedState state) + { + switch (state) + { + case PackagePinnedState::PinnedByManifest: return "PinnedByManifest"sv; + case PackagePinnedState::NotPinned: + default: + return "Unknown"; + } + } + + PackagePinnedState ConvertToPackagePinnedStateEnum(std::string_view in) + { + if (Utility::CaseInsensitiveEquals(in, "PinnedByManifest"sv)) + { + return PackagePinnedState::PinnedByManifest; + } + else + { + return PackagePinnedState::NotPinned; + } + } + + const char* UnsupportedRequestException::what() const noexcept { if (m_whatMessage.empty())