Repository navigation
Add opt-in cross-source package deduplication - #6590
AmirMS (AmelBawa-msft) wants to merge 7 commits into
Conversation
This comment has been minimized.
This comment has been minimized.
This comment has been minimized.
This comment has been minimized.
There was a problem hiding this comment.
🔵 Needs a closer look
Automatic package-source selection affects install security and reliability across several core workflows and warrants final human validation.
0 open findings
What changed in this PR
Adds opt-in cross-source package deduplication while preserving source priority, trust, agreements, and installer verification.
Changes:
- Adds the
packageDeduplicationexperimental setting. - Compares versions, hashes, applicability, and dependencies before automatic selection.
- Adds extensive workflow tests, documentation, resources, and truncation handling.
| File | Description |
|---|---|
src/AppInstallerRepositoryCore/CompositeSource.cpp |
Propagates child-source truncation. |
src/AppInstallerCommonCore/UserSettings.cpp |
Validates the new setting. |
src/AppInstallerCommonCore/Public/winget/UserSettings.h |
Defines the setting mapping. |
src/AppInstallerCommonCore/Public/winget/ExperimentalFeature.h |
Adds the feature flag. |
src/AppInstallerCommonCore/ExperimentalFeature.cpp |
Implements feature discovery and enablement. |
src/AppInstallerCLITests/SourceFlow.cpp |
Adds deduplication workflow coverage. |
src/AppInstallerCLITests/PromptFlow.cpp |
Tests truncated composite results. |
src/AppInstallerCLITests/ExperimentalFeature.cpp |
Tests setting and policy behavior. |
src/AppInstallerCLITests/CompositeSource.cpp |
Tests truncation propagation. |
src/AppInstallerCLIPackage/Shared/Strings/en-us/winget.resw |
Adds user-facing messages. |
src/AppInstallerCLICore/Workflows/WorkflowBase.h |
Declares selection flags and prepared matches. |
src/AppInstallerCLICore/Workflows/WorkflowBase.cpp |
Implements comparison and selection. |
src/AppInstallerCLICore/Workflows/UpdateFlow.cpp |
Handles prepared install matches. |
src/AppInstallerCLICore/Workflows/ShowFlow.cpp |
Enables prepared show matches. |
src/AppInstallerCLICore/Workflows/InstallFlow.h |
Exposes argument compatibility checking. |
src/AppInstallerCLICore/Workflows/InstallFlow.cpp |
Implements non-mutating argument checks. |
src/AppInstallerCLICore/Resources.h |
Registers new resources. |
src/AppInstallerCLICore/ExecutionContextData.h |
Stores prepared package matches. |
src/AppInstallerCLICore/Commands/ShowCommand.cpp |
Opts show into deduplication. |
src/AppInstallerCLICore/Commands/InstallCommand.cpp |
Opts install into deduplication. |
src/AppInstallerCLICore/Commands/DownloadCommand.cpp |
Opts download into deduplication. |
schemas/JSON/settings/settings.schema.0.2.json |
Documents the setting schema. |
doc/windows/package-manager/winget/source.md |
Documents source behavior. |
doc/specs/#5345 - Interactive package selection.md |
Describes interaction with prompting. |
doc/specs/#5345 - Cross-source package deduplication.md |
Adds the feature specification. |
doc/Settings.md |
Documents configuration. |
doc/ReleaseNotes.md |
Announces the experimental feature. |
🧠 Review effort: Balanced
💡 Add a code-review agent skill or configure MCP servers for context-aware, tailored reviews. Learn more in the docs.
ranm-msft
left a comment
There was a problem hiding this comment.
Thanks for this - the implementation is careful and the opt-in is genuinely surgical. I read WorkflowBase.cpp/.h, the three command opt-in sites, and the InstallFlow/ShowFlow/UpdateFlow/PromptFlow hunks. The THROW_HR_IF identity/operation/version/manifest asserts, the single-use reset of Data::PreparedPackageMatch in SelectInstaller, the GetDetails().Priority skip that preserves source precedence, and the fact that OpenSource and InstallSinglePackage are untouched (so no source or package agreement is bypassed) all look right to me.
One question is holding me back from approving.
Is byte-identical SHA-256 sufficient to call two installers equivalent?
GetEquivalentInstallers requires the preferred installer hash to be present, correctly sized, non-zero and identical across candidates, plus matching version/channel, an allowlisted installer type, and non-conflicting explicit scope. That proves the two candidates point at the same payload. It does not appear to prove they would execute the same way.
A single bootstrapper or unified installer can install different products, editions, workloads or channels purely as a function of its command line. Two manifests from different sources can legitimately share a hash and a nominal version while differing in InstallerSwitches, generated arguments, expected return codes, or ExpectedReturnCodes interpretation. In that case the dedup path would silently pick one source's execution contract and suppress the disambiguation prompt that exists precisely to let the user choose.
Concretely, I did not find a comparison of the fields that determine what the installer actually does - switches and generated arguments in particular - only of the payload identity and the surrounding eligibility gates.
Two ways this could be closed out, either of which would let me approve:
- Extend the equivalence predicate to cover the effective execution contract (switches / generated arguments / scope and elevation behaviour), and add a negative test using the same hash and version but product-selecting switches that differ, asserting fallback to the normal prompt.
- If the shared-bootstrapper false positive is considered acceptable in practice, say so explicitly in the spec and I will take that as the answer - it is a product call, not a code defect.
I appreciate that the feature is experimental and off by default, which keeps the blast radius small. I did not want to lean on that to wave through a predicate I could not evidence, since the user-facing message describes the results as equivalent.
Everything else I read I am comfortable with, and the test coverage across SourceFlow.cpp, CompositeSource.cpp, PromptFlow.cpp and ExperimentalFeature.cpp is substantial.
|
|
||
| ### Comparison rules | ||
|
|
||
| Use existing command-specific version, channel, pin, and installer-applicability rules. Compare the preferred installer, not any installer with a matching hash. For install without an explicit version, evaluate only the first permitted version; an inapplicable installer or missing requested version leaves the set unresolved. Never probe older manifests. |
There was a problem hiding this comment.
What if the sources differ in preferred installer? For example, if Source A has installer entries for msi and exe but Source B has only installer entries for exe - then the preferred comparison would be msi vs exe.
Even if the exe installers have byte-for-byte identical SHA256, the sources wouldn't be de-duplicated, despite Source B being a subset-type duplicate of Source A ?
| On selection, emit localized text, then continue normal command output: | ||
|
|
||
| ```text | ||
| Equivalent installers found. Selected Contoso Editor [Contoso.Editor] from source Alpha. |
There was a problem hiding this comment.
Will logs show the equivalent installers that were compared without needing to enable verbose logging?
| <comment>{Locked="{0}","{1}","{2}"} {0} is the selected package name, {1} its identifier, and {2} its source name. Explains automatic selection across sources.</comment> | ||
| </data> | ||
| <data name="PackageDeduplicationFailed" xml:space="preserve"> | ||
| <value>Could not compare packages across sources. Refine the search or choose a package.</value> |
There was a problem hiding this comment.
Do we really need a message that states: "I couldn't do the new thing so here is the old thing."? Presumably the second sentence here is redundant anyway.
I think logging should be good enough, no output required.
| using namespace AppInstaller::Repository; | ||
| using namespace AppInstaller::Settings; | ||
|
|
||
| namespace |
There was a problem hiding this comment.
Why are these tests in this file? They are more install flow or generic workflow, not source specific (as evidenced by not changing the product source flow file).
| template <> | ||
| struct DataMapping<Data::PreparedPackageMatch> | ||
| { | ||
| using value_t = std::optional<Workflow::PreparedPackageMatch>; |
There was a problem hiding this comment.
| using value_t = std::optional<Workflow::PreparedPackageMatch>; | |
| using value_t = Workflow::PreparedPackageMatch; |
I really don't like having an optional inside an optional item.
| GetManifestFromPackage(bool considerPins, std::optional<OperationType> preparedOperation = {}) : | ||
| WorkflowTask("GetManifestFromPackage"), m_considerPins(considerPins), m_preparedOperation(preparedOperation) {} |
There was a problem hiding this comment.
Why do we need this "secret phrase" type value whose only purpose is to allow us to use the prepared data if it matches? Since it is a hard error when not set properly, it doesn't appear to be intended as control flow. It only serves to double check that we expected the possibility of prepared data for the operation type.
It should be removed unless there is some justification.
| Utility::LocIndView{ prepared->PackageVersion->GetSource().GetDetails().Name }) << std::endl; | ||
| Logging::Telemetry().LogAppFound(name, manifest.Id); | ||
| context.Add<Execution::Data::Package>(std::move(package)); | ||
| context.Add<Execution::Data::PreparedPackageMatch>(std::move(prepared)); |
There was a problem hiding this comment.
I really don't like this alternate data item that we then have to merge back in when the alternate data it carries is requested. I'm assuming that the goal is to optimize on the fact that we needed all of this data to make the choice, so lets use it.
I would prefer that we set the data items that are needed and update the code that would normally choose them to detect their presence and no-op.
|
|
||
| if ((operation != OperationType::Install && operation != OperationType::Download && operation != OperationType::Show) || | ||
| context.Args.Contains(Execution::Args::Type::Manifest) || context.Args.Contains(Execution::Args::Type::ListVersions) || | ||
| context.Args.GetCount(Execution::Args::Type::MultiQuery) > 1 || |
There was a problem hiding this comment.
Can we even get here with a multi-query? Shouldn't we be in a subcontext with a single query by this point?
| WI_IsAnyFlagSet(context.GetFlags(), Execution::ContextFlag::InstallerExecutionUseUpdate | Execution::ContextFlag::InstallerExecutionUseRepair) || | ||
| (context.Contains(Execution::Data::InstalledPackageVersion) && context.Get<Execution::Data::InstalledPackageVersion>())) | ||
| { | ||
| return skip("unsupported operation or context"); |
There was a problem hiding this comment.
We need to break this down into individual checks and messages because I don't want to have to reverse engineer which of these was not allowed after the fact.
| if (priority && *priority != opened->GetDetails().Priority) | ||
| { | ||
| return skip("results have not been filtered by source priority"); | ||
| } |
There was a problem hiding this comment.
I don't understand how this is relevant.
| return std::nullopt; | ||
| } | ||
|
|
||
| std::optional<size_t> GetDeduplicationCandidateIndex(const SearchResult& result, const Source& source) |
There was a problem hiding this comment.
This whole function seems off-base. The only way the deduplication should work is if, for the highest priority level with packages, there are either 0 or 1 packages per-source.
I might be missing something here, but this seems like this code could support 2 sources with 2 packages from one of them. There is no point trying to deduplicate multiple packages from the same source.
The code that handles priority in EnsureOneMatchFromSearchResult already creates a "highest priority only" result set. It explicitly excludes anything that targets installed items right now, but you could build the list in more cases and then only use it when necessary. That gets you the priority reduced set to use here, but then it becomes simply a filter rather than this attempt selection ("are there no more than 1 package per source"). As long as that is true, the candidate index will always be 0.
|
|
||
| Every candidate must have: | ||
|
|
||
| 1. Equal resolved versions under existing version comparison (`2.4` equals `2.4.0`) and case-insensitively equal channels. Reject empty, `Unknown`, `Latest`, or approximate versions. Unknown search metadata is acceptable if retrieval resolves a concrete version. |
There was a problem hiding this comment.
I think I would be ok with a little more flexibility in versions, like:
- Choose the lower of the latest versions, then do the comparison at that version
- Then always install from the source with the highest version if deduped
📖 Description
Add opt-in cross-source package deduplication for single-package
install,download, and ordinaryshowcommands.Enable it with
experimentalFeatures.packageDeduplication: true. It is disabled by default and independent of interactive package selection.This does not merge search rows or apply to
show --versions, upgrade flows, multi-package operations, or COM/PowerShell/DSC callers. Payload equivalence does not guarantee identical installer behavior; the selected source's instructions remain authoritative.🎞️ Demo
Installed
winget v1.30.180-previewbefore, and the PR'swingetdevbuild with deduplication enabled after. Actual read-only CLI output is replayed with excerpts and condensed timing.PuTTY: equivalent installers resolve automatically. Both sources report version
0.85.0.0and the same full installer SHA256, with compatible MSI/WiX installer types.wingetreports ambiguity;wingetdevselects one candidate without prompting. The Store candidate wins becausemsstorecame first in the opened source order at equal priority, not because the heuristic prefers the Store. Result-table order does not determine the selection.OBS Studio: different versions remain separate. The sources report
32.2.2fromwingetand32.0.1frommsstore, with different installer hashes. Both builds retain ambiguity. Deduplication does not choose an older release just to obtain a match.🔗 References
Related to #5345.
🔍 Validation
From
src\x64\Debug\AppInstallerCLITests:.\AppInstallerCLITests.exe '[PackageDeduplication]' -r compactPassed: 43 test cases, 7,718 assertions.
Live comparisons with the deployed PR build:
winget show PuTTY --disable-interactivitywingetdev show PuTTY --disable-interactivitywinget show "OBS Studio" --disable-interactivitywingetdev show "OBS Studio" --disable-interactivityAlso verified both scenarios on the same PR build with deduplication disabled and enabled to isolate this feature's effect. Source-specific metadata confirmed PuTTY's matching version/hash and OBS Studio's differing versions/hashes.
Development settings were restored after capture. No packages were installed by the demo.
The full native suite and installation/download E2E tests were not run.
✅ Checklist
🤖 AI Assistance
AI assisted with code refactoring, validation, spelling fixes, and demo/PR preparation.
📋 Issue Type
Microsoft Reviewers: Open in CodeFlow