diff --git a/doc/release-notes/1.31/6516-upgrade-all-skip-reason.md b/doc/release-notes/1.31/6516-upgrade-all-skip-reason.md new file mode 100644 index 0000000000..3cb679f591 --- /dev/null +++ b/doc/release-notes/1.31/6516-upgrade-all-skip-reason.md @@ -0,0 +1,5 @@ +## Bug Fixes + +### Minor Bug Fixes + +* `winget upgrade --all` now reports how many available upgrades were skipped because they do not apply to the selected scope or other requirements. diff --git a/src/AppInstallerCLICore/ExecutionContextData.h b/src/AppInstallerCLICore/ExecutionContextData.h index 40cf11537d..f9762abfdd 100644 --- a/src/AppInstallerCLICore/ExecutionContextData.h +++ b/src/AppInstallerCLICore/ExecutionContextData.h @@ -70,6 +70,8 @@ namespace AppInstaller::CLI::Execution MsixDigests, InstallerDownloadAuthenticators, SelectedIndex, + // Indicates that update selection found newer versions, but none had an applicable installer. + InstallerInapplicable, Max }; @@ -306,5 +308,11 @@ namespace AppInstaller::CLI::Execution // The authenticator map shared with sub contexts using value_t = std::shared_ptr>; }; + + template<> + struct DataMapping + { + using value_t = bool; + }; } } diff --git a/src/AppInstallerCLICore/Resources.h b/src/AppInstallerCLICore/Resources.h index 4b574f5155..d7e0f4fdd9 100644 --- a/src/AppInstallerCLICore/Resources.h +++ b/src/AppInstallerCLICore/Resources.h @@ -821,6 +821,7 @@ namespace AppInstaller::CLI::Resource WINGET_DEFINE_RESOURCE_STRINGID(UpgradeIsPinned); WINGET_DEFINE_RESOURCE_STRINGID(UpgradePinnedByUserCount); WINGET_DEFINE_RESOURCE_STRINGID(UpgradeRequireExplicitCount); + WINGET_DEFINE_RESOURCE_STRINGID(UpgradeNotApplicableCount); WINGET_DEFINE_RESOURCE_STRINGID(UpgradeUnknownVersionCount); WINGET_DEFINE_RESOURCE_STRINGID(UpgradeUnknownVersionExplanation); WINGET_DEFINE_RESOURCE_STRINGID(UriNotWellFormed); diff --git a/src/AppInstallerCLICore/Workflows/UpdateFlow.cpp b/src/AppInstallerCLICore/Workflows/UpdateFlow.cpp index a2c243673e..9c97d13bf6 100644 --- a/src/AppInstallerCLICore/Workflows/UpdateFlow.cpp +++ b/src/AppInstallerCLICore/Workflows/UpdateFlow.cpp @@ -55,6 +55,8 @@ namespace AppInstaller::CLI::Workflow bool versionFound = false; bool installedTypeInapplicable = false; bool packagePinned = false; + bool upgradeVersionPassedPin = false; + bool installerInapplicable = false; if (isUpgrade && installedVersion.IsUnknown() && !context.Args.Contains(Execution::Args::Type::IncludeUnknown)) { @@ -109,12 +111,14 @@ namespace AppInstaller::CLI::Workflow } } + upgradeVersionPassedPin = true; auto manifest = packageVersion->GetManifest(); // Check applicable Installer auto [installer, inapplicabilities] = manifestComparator.GetPreferredInstaller(manifest); if (!installer.has_value()) { + installerInapplicable = true; // If there is at least one installer whose only reason is InstalledType. auto onlyInstalledType = std::find(inapplicabilities.begin(), inapplicabilities.end(), Manifest::InapplicabilityFlags::InstalledType); if (onlyInstalledType != inapplicabilities.end()) @@ -191,6 +195,11 @@ namespace AppInstaller::CLI::Workflow AICLI_TERMINATE_CONTEXT(APPINSTALLER_CLI_ERROR_UPDATE_INSTALL_TECHNOLOGY_MISMATCH); } + if (isUpgrade && upgradeVersionPassedPin && installerInapplicable) + { + context.Add(true); + } + AICLI_TERMINATE_CONTEXT(isUpgrade ? APPINSTALLER_CLI_ERROR_UPDATE_NOT_APPLICABLE : APPINSTALLER_CLI_ERROR_NO_APPLICABLE_INSTALLER); } } @@ -217,6 +226,7 @@ namespace AppInstaller::CLI::Workflow int packagesWithUnknownVersionSkipped = 0; int packagesThatRequireExplicitSkipped = 0; int packagesSkippedInstallTechnologyMismatch = 0; + int packagesSkippedNotApplicable = 0; for (const auto& match : matches) { @@ -253,6 +263,11 @@ namespace AppInstaller::CLI::Workflow if (updateContext.GetTerminationHR() == APPINSTALLER_CLI_ERROR_UPDATE_NOT_APPLICABLE) { + if (updateContext.Contains(Execution::Data::InstallerInapplicable) && updateContext.Get(Execution::Data::InstallerInapplicable)) + { + ++packagesSkippedNotApplicable; + } + continue; } @@ -314,6 +329,12 @@ namespace AppInstaller::CLI::Workflow AICLI_LOG(CLI, Info, << packagesSkippedInstallTechnologyMismatch << " package(s) skipped due to install technology mismatch"); context.Reporter.Info() << Resource::String::UpgradeInstallTechnologyMismatchCount(packagesSkippedInstallTechnologyMismatch) << std::endl; } + + if (packagesSkippedNotApplicable > 0) + { + AICLI_LOG(CLI, Info, << packagesSkippedNotApplicable << " package(s) skipped because the available upgrade was not applicable"); + context.Reporter.Info() << Resource::String::UpgradeNotApplicableCount(packagesSkippedNotApplicable) << std::endl; + } } void SelectSinglePackageVersionForInstallOrUpgrade::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 index af04b9f319..5f7330c69f 100644 --- a/src/AppInstallerCLIPackage/Shared/Strings/en-us/winget.resw +++ b/src/AppInstallerCLIPackage/Shared/Strings/en-us/winget.resw @@ -1429,6 +1429,10 @@ Please specify one of them using the --source option to proceed. {0} package(s) are pinned and need to be explicitly upgraded. {Locked="{0}"} {0} is a placeholder that is replaced by an integer number of packages that require explicit upgrades. + + {0} package(s) were skipped because an available upgrade does not apply to the selected scope or other requirements. + {Locked="{0}"} {0} is the number of packages skipped for this reason during winget upgrade --all. + The arguments provided can only be used with a query. diff --git a/src/AppInstallerCLITests/AppInstallerCLITests.vcxproj b/src/AppInstallerCLITests/AppInstallerCLITests.vcxproj index 07f7dca337..e98f7dec92 100644 --- a/src/AppInstallerCLITests/AppInstallerCLITests.vcxproj +++ b/src/AppInstallerCLITests/AppInstallerCLITests.vcxproj @@ -760,6 +760,9 @@ true + + true + true diff --git a/src/AppInstallerCLITests/AppInstallerCLITests.vcxproj.filters b/src/AppInstallerCLITests/AppInstallerCLITests.vcxproj.filters index 0311271140..0c3e5139f1 100644 --- a/src/AppInstallerCLITests/AppInstallerCLITests.vcxproj.filters +++ b/src/AppInstallerCLITests/AppInstallerCLITests.vcxproj.filters @@ -822,6 +822,9 @@ TestData + + TestData + TestData diff --git a/src/AppInstallerCLITests/TestData/UpdateFlowTest_Exe_MachineScope.yaml b/src/AppInstallerCLITests/TestData/UpdateFlowTest_Exe_MachineScope.yaml new file mode 100644 index 0000000000..2821cba4d3 --- /dev/null +++ b/src/AppInstallerCLITests/TestData/UpdateFlowTest_Exe_MachineScope.yaml @@ -0,0 +1,19 @@ +# Update manifest with only a machine-scope installer. +Id: AppInstallerCliTest.TestExeInstaller +Version: 2.0.0.0 +Name: AppInstaller Test Installer Machine Scope +Publisher: Microsoft Corporation +AppMoniker: AICLITestExeMachineScope +License: Test +Switches: + Custom: /custom + SilentWithProgress: /silentwithprogress + Silent: /silence + Update: /update +Installers: + - Arch: x64 + Url: https://ThisIsNotUsed + InstallerType: exe + Scope: machine + Sha256: 65DB2F2AC2686C7F2FD69D4A4C6683B888DC55BFA20A0E32CA9F838B51689A3B +ManifestVersion: 0.1.0 diff --git a/src/AppInstallerCLITests/UpdateFlow.cpp b/src/AppInstallerCLITests/UpdateFlow.cpp index 9d70283c78..2196353f94 100644 --- a/src/AppInstallerCLITests/UpdateFlow.cpp +++ b/src/AppInstallerCLITests/UpdateFlow.cpp @@ -3,6 +3,8 @@ #include "pch.h" #include "WorkflowCommon.h" #include "TestHooks.h" +#include +#include #include #include #include @@ -556,6 +558,77 @@ TEST_CASE("UpdateFlow_UpdateAllApplicable", "[UpdateFlow][workflow]") REQUIRE(std::filesystem::exists(updatePortableResultPath.GetPath())); } +TEST_CASE("UpdateFlow_UpdateAll_ReportsNotApplicable", "[UpdateFlow][workflow]") +{ + std::ostringstream updateOutput; + TestContext context{ updateOutput, std::cin }; + auto previousThreadGlobals = context.SetForCurrentThread(); + OverrideForCompositeInstalledSource(context, CreateTestSource({ + TSR::TestInstaller_Exe_NotApplicable, + TSR::TestInstaller_Exe_LatestInstalled, + TSR::TestInstaller_Msix_UpgradeRequiresExplicit, + })); + context.Args.AddArg(Execution::Args::Type::All); + context.Args.AddArg(Execution::Args::Type::InstallScope, "user"sv); + + UpgradeCommand update({}); + update.Execute(context); + INFO(updateOutput.str()); + + REQUIRE(updateOutput.str().find(Resource::String::UpgradeNotApplicableCount(1)) != std::string::npos); +} + +TEST_CASE("UpdateFlow_UpdateAll_ReportsNotApplicableWithPinnedVersion", "[UpdateFlow][workflow]") +{ + TestCommon::TempFile indexFile("pinningIndex", ".db"); + TestHook::SetPinningIndex_Override pinningIndexOverride(indexFile.GetPath()); + + auto pinningIndex = AppInstaller::Repository::Microsoft::PinningIndex::OpenOrCreateDefault(); + REQUIRE(pinningIndex); + pinningIndex->AddPin(AppInstaller::Pinning::Pin::CreateGatingPin( + AppInstaller::Pinning::PinKey{ "AppInstallerCliTest.TestExeInstaller", "*TestSource" }, + AppInstaller::Pinning::GatedVersion{ "1.*"sv })); + + TestSourceResult mixedPinnedAndNotApplicable( + "TestInstallerMixedPinnedAndNotApplicable"sv, + [](std::vector& matches, std::weak_ptr source) + { + auto installed = AppInstaller::Manifest::YamlParser::CreateFromPath(TestDataFile("InstallFlowTest_Exe.yaml")); + auto available = AppInstaller::Manifest::YamlParser::CreateFromPath(TestDataFile("UpdateFlowTest_Exe_MachineScope.yaml")); + auto allowed = available; + allowed.Version = "1.1.0.0"; + auto blocked = available; + blocked.Version = "2.0.0.0"; + matches.emplace_back(AppInstaller::Repository::ResultMatch( + TestCompositePackage::Make( + installed, + TestCompositePackage::MetadataMap{ { AppInstaller::Repository::PackageVersionMetadata::InstalledType, "Exe" } }, + std::vector{ blocked, allowed, installed }, + source), + AppInstaller::Repository::PackageMatchFilter( + AppInstaller::Repository::PackageMatchField::Id, + AppInstaller::Repository::MatchType::Exact, + "AppInstallerCliTest.TestExeInstaller"))); + }); + + std::ostringstream updateOutput; + TestContext context{ updateOutput, std::cin }; + auto previousThreadGlobals = context.SetForCurrentThread(); + OverrideForCompositeInstalledSource(context, CreateTestSource({ + std::move(mixedPinnedAndNotApplicable), + TSR::TestInstaller_Exe_LatestInstalled, + TSR::TestInstaller_Msix_UpgradeRequiresExplicit, + })); + context.Args.AddArg(Execution::Args::Type::All); + context.Args.AddArg(Execution::Args::Type::InstallScope, "user"sv); + + UpgradeCommand update({}); + update.Execute(context); + INFO(updateOutput.str()); + + REQUIRE(updateOutput.str().find(Resource::String::UpgradeNotApplicableCount(1)) != std::string::npos); +} + TEST_CASE("UpdateFlow_UpdateAll_IncludeUnknown", "[UpdateFlow][workflow]") { TestCommon::TempFile updateExeResultPath("TestExeInstalled.txt"); diff --git a/src/AppInstallerCLITests/WorkflowCommon.cpp b/src/AppInstallerCLITests/WorkflowCommon.cpp index 93a42282a4..b80beca475 100644 --- a/src/AppInstallerCLITests/WorkflowCommon.cpp +++ b/src/AppInstallerCLITests/WorkflowCommon.cpp @@ -276,6 +276,22 @@ namespace TestCommon PackageMatchFilter(PackageMatchField::Id, MatchType::Exact, "AppInstallerCliTest.TestExeInstaller"))); }); + const TestSourceResult TestInstaller_Exe_NotApplicable( + "TestExeInstallerNotApplicable"sv, + [](std::vector& matches, std::weak_ptr source) { + auto manifest = YamlParser::CreateFromPath(TestDataFile("InstallFlowTest_Exe.yaml")); + auto manifest2 = YamlParser::CreateFromPath(TestDataFile("UpdateFlowTest_Exe_MachineScope.yaml")); + matches.emplace_back( + ResultMatch( + TestCompositePackage::Make( + manifest, + TestCompositePackage::MetadataMap{ { PackageVersionMetadata::InstalledType, "Exe" } }, + std::vector{ manifest2, manifest }, + source + ), + PackageMatchFilter(PackageMatchField::Id, MatchType::Exact, "AppInstallerCliTest.TestExeInstaller"))); + }); + const TestSourceResult TestInstaller_Exe_DifferentInstallerType( "TestExeInstallerWithDifferentInstalledType"sv, [](std::vector& matches, std::weak_ptr source) { diff --git a/src/AppInstallerCLITests/WorkflowCommon.h b/src/AppInstallerCLITests/WorkflowCommon.h index e4f6333780..3c4507d03e 100644 --- a/src/AppInstallerCLITests/WorkflowCommon.h +++ b/src/AppInstallerCLITests/WorkflowCommon.h @@ -33,6 +33,7 @@ namespace TestCommon const extern TestSourceResult TestQuery_ReturnOne; const extern TestSourceResult TestQuery_ReturnTwo; const extern TestSourceResult TestInstaller_Exe; + const extern TestSourceResult TestInstaller_Exe_NotApplicable; const extern TestSourceResult TestInstaller_Exe_Dependencies; const extern TestSourceResult TestInstaller_Exe_DifferentInstallerType; const extern TestSourceResult TestInstaller_Exe_ExpectedReturnCodes;