commit 8e82bfe7fea0326ff22d6c87beab06edb7340175 parent 834806b5392c2f21f677135846e23eb3348812d9 Author: JohnMcPMS <johnmcp@microsoft.com> Date: Fri, 22 Mar 2024 16:06:22 -0700 Add experimental support for multiple installed versions (#4282) This change introduces a new experimental feature flag called `sideBySide` that enables multiple instances of an installed package to be found by search results. The `CompositeSource` will fold these all together into a single package result through their common association with available packages. > [!WARNING] > This experimental feature affects the search results that the rest of the Windows Package Manager code relies on. Enabling it may change the behavior of downstream components. > [!IMPORTANT] > Only the commands in `winget.exe` have been updated to account for this change. Other interfaces will get reasonable defaults, but may not be able to interact properly in all cases. ### Installed identifier change The most visible effect of this feature being enabled is that the identifier generated for installed items is no longer simply the registry key name or package family name but is instead constructed to make it fully unique. This is mostly relevant to packages without an available counterpart. For packages that are registered with the system through the registry, the identifier is now of the form `ARP\User\x64\ProductCode`. For MSIX, the form is `MSIX\Package_Full_Name`. The previous identifiers are still searchable data though, so it is hopefully not more difficult to `list` installed items. ### Installed item correlations The implementation in `CompositeSource` tries to separate the steps into: 1. enumerating the basic results with each having at most one installed item and its correlated available packages. 2. folding the results together to create the final return value. In an effort to prevent overly broad correlations from available to installed items, there is an additional attempt to ensure that there are no stronger correlations from installed to available. As an example, if a package is split by version while still sharing a name that is reasonably close, searching for one of the version subsets should not attempt to associate all of the installed items so long as the other versions have stronger correlations. 1. Package `A1` has versions 1.0 - 1.5 with names like `A Package (1.0)` 2. Package `A2` has versions 2.0 - 2.2 with names like `A Package (2.0)` 3. Searching for `A2` would correlate with version `1.0` because the normalized names are the same 4. With the above check, it should not return installed version 1.0 as long as the product code is associated with `A1` Once the basic results are discovered, the results are folded together. A comment on that function indicates the methodology and possible future improvements: ```C++ // Group results in an attempt to have a single result that covers all installed versions. // This is expected to be called immediately after the installed search portion, // when each result will contain a single installed version and some number of available packages. // // The folds that happen are: // 1. When results have the same primary available package (the primary available package is set due to tracking data) // 2. When a result has no primary available package, but another result does have a primary that matches one of the available // a. Choose the latest primary if there are multiple // 3. When multiple results have no primary available package and share the same available package set // a. There are many potential additional rules that could be made here, but we will start with the simplest version. // // Potential improvements: // 1. Attempting correlation of non-primary available packages to allow folding in more complex cases // a. For example, if installed A has {source1:package1, source2:package2} and installed B has {source1:package1}, can we // make sure that source1:package1 and source2:package2 are in fact "the same" to confidently say that installed A and B // are side by side versions. // 2. Attempt correlation by installed data only // a. We can potentially detect multiple instances of the same installed item with the same correlation logic turned back on // the installed source. This would allow for folding even when the package is not in any available source. ``` ### Command changes #### list `list` has been updated to simply output all versions contained within a single search result as independent lines. This keeps the resulting output basically the same, but that has the downside of not explicitly indicating the correlation between the items. #### repair `repair` was updated internally so that passing in a `--version` would cause that specific version to be targeted for repair. #### uninstall `uninstall` should behave as before if only a single installed version is present. When multiple installed versions exist, an error is produced unless one of two new arguments is supplied: 1. `--all-versions` / `--all` will uninstall all installed versions 2. `--version` / `-v` will uninstall the targeted version only ### Discussion I have created a discussion for this experimental feature: #4281 The goal is to allow feedback to be better collected and threaded, although I expect that there will be issues created independently as well. ### Additionally... I added a script in the `tools` that launches the sandbox with the dev package registered: Launch-DevPackageInSandbox.ps1 Since it is dev registered in the sandbox, all you need to do to update it is build and deploy from Visual Studio and the sandbox version will just use those latest files. Diffstat:
43 files changed, 1766 insertions(+), 511 deletions(-)
diff --git a/.github/actions/spelling/excludes.txt b/.github/actions/spelling/excludes.txt @@ -91,5 +91,6 @@ # Because it doesn't handle argument -Words well ^src/PowerShell/tests/ ^tools/CorrelationTestbed/.*\.ps1$ +^tools/DevInSandbox/.*\.ps1$ ^tools/COMTrace/ComTrace.wprp$ ignore$ diff --git a/doc/Settings.md b/doc/Settings.md @@ -280,17 +280,6 @@ You can enable the feature as shown below. }, ``` -### configuration - -This feature enables the configuration commands. These commands allow configuring the system into a desired state. -You can enable the feature as shown below. - -```json - "experimentalFeatures": { - "configuration": true - }, -``` - ### resume This feature enables support for some commands to resume. @@ -323,3 +312,14 @@ You can enable the feature as shown below. "proxy": true }, ``` + +### sideBySide + +This feature enables experimental improvements for supporting multiple instances of a package being installed on a system. +You can enable the feature as shown below. + +```json + "experimentalFeatures": { + "sideBySide": true + }, +``` diff --git a/schemas/JSON/settings/settings.schema.0.2.json b/schemas/JSON/settings/settings.schema.0.2.json @@ -275,6 +275,11 @@ "description": "Enable support for proxies", "type": "boolean", "default": false + }, + "sideBySide": { + "description": "Enable support for improved side-by-side handling", + "type": "boolean", + "default": false } } } diff --git a/src/AppInstallerCLICore/Argument.cpp b/src/AppInstallerCLICore/Argument.cpp @@ -30,7 +30,7 @@ namespace AppInstaller::CLI case Execution::Args::Type::Query: return { type, "query"_liv, 'q', ArgTypeCategory::PackageQuery | ArgTypeCategory::SinglePackageQuery }; case Execution::Args::Type::MultiQuery: - return { type, "query"_liv, 'q', ArgTypeCategory::PackageQuery }; + return { type, "query"_liv, 'q', ArgTypeCategory::PackageQuery | ArgTypeCategory::MultiplePackages }; case Execution::Args::Type::Manifest: return { type, "manifest"_liv, 'm', ArgTypeCategory::Manifest }; @@ -101,6 +101,10 @@ namespace AppInstaller::CLI return { type, "preserve"_liv, ArgTypeCategory::None, ArgTypeExclusiveSet::PurgePreserve }; case Execution::Args::Type::ProductCode: return { type, "product-code"_liv, ArgTypeCategory::SinglePackageQuery }; + case Execution::Args::Type::AllVersions: + return { type, "all-versions"_liv, "all"_liv, ArgTypeCategory::CopyFlagToSubContext, ArgTypeExclusiveSet::AllAndTargetVersion }; + case Execution::Args::Type::TargetVersion: + return { type, "version"_liv, 'v', ArgTypeCategory::SinglePackageQuery, ArgTypeExclusiveSet::AllAndTargetVersion }; //Source Command case Execution::Args::Type::SourceName: @@ -388,6 +392,10 @@ namespace AppInstaller::CLI return Argument{ type, Resource::String::AllowRebootArgumentDescription, ArgumentType::Flag }; case Args::Type::IgnoreResumeLimit: return Argument{ type, Resource::String::IgnoreResumeLimitArgumentDescription, ArgumentType::Flag, ExperimentalFeature::Feature::Resume }; + case Args::Type::AllVersions: + return Argument{ type, Resource::String::UninstallAllVersionsArgumentDescription, ArgumentType::Flag, Argument::Visibility::Help, ExperimentalFeature::Feature::SideBySide }; + case Args::Type::TargetVersion: + return Argument{ type, Resource::String::TargetVersionArgumentDescription, ArgumentType::Standard }; case Args::Type::Proxy: return Argument{ type, Resource::String::ProxyArgumentDescription, ArgumentType::Standard, ExperimentalFeature::Feature::Proxy, TogglePolicy::Policy::ProxyCommandLineOptions, BoolAdminSetting::ProxyCommandLineOptions }; case Args::Type::NoProxy: diff --git a/src/AppInstallerCLICore/Argument.h b/src/AppInstallerCLICore/Argument.h @@ -86,6 +86,7 @@ namespace AppInstaller::CLI PinType = 0x8, StubType = 0x10, Proxy = 0x20, + AllAndTargetVersion = 0x40, // This must always be at the end Max diff --git a/src/AppInstallerCLICore/Commands/RepairCommand.cpp b/src/AppInstallerCLICore/Commands/RepairCommand.cpp @@ -20,7 +20,7 @@ namespace AppInstaller::CLI Argument::ForType(Args::Type::Name), // -n Argument::ForType(Args::Type::Channel), Argument::ForType(Args::Type::Moniker), // -mn - Argument::ForType(Args::Type::Version), // -v + Argument::ForType(Args::Type::TargetVersion), // -v Argument::ForType(Args::Type::ProductCode), Argument::ForType(Args::Type::InstallArchitecture), // -arch Argument{ Execution::Args::Type::InstallScope, Resource::String::InstalledScopeArgumentDescription, ArgumentType::Standard, Argument::Visibility::Help }, @@ -60,12 +60,16 @@ namespace AppInstaller::CLI return; } + context << + Workflow::OpenSource() << + Workflow::OpenCompositeSource(Repository::PredefinedSource::Installed); + switch (valueType) { case Execution::Args::Type::Id: case Execution::Args::Type::Name: case Execution::Args::Type::Moniker: - case Execution::Args::Type::Version: + case Execution::Args::Type::TargetVersion: case Execution::Args::Type::Channel: case Execution::Args::Type::Source: context << diff --git a/src/AppInstallerCLICore/Commands/UninstallCommand.cpp b/src/AppInstallerCLICore/Commands/UninstallCommand.cpp @@ -23,11 +23,12 @@ namespace AppInstaller::CLI Argument::ForType(Args::Type::Name), Argument::ForType(Args::Type::Moniker), Argument::ForType(Args::Type::ProductCode), - Argument::ForType(Args::Type::Version), + Argument::ForType(Args::Type::TargetVersion), + Argument::ForType(Args::Type::AllVersions), Argument::ForType(Args::Type::Channel), Argument::ForType(Args::Type::Source), Argument::ForType(Args::Type::Exact), - Argument{ Execution::Args::Type::InstallScope, Resource::String::InstalledScopeArgumentDescription, ArgumentType::Standard, Argument::Visibility::Help }, + Argument{ Args::Type::InstallScope, Resource::String::InstalledScopeArgumentDescription, ArgumentType::Standard, Argument::Visibility::Help }, Argument::ForType(Args::Type::Interactive), Argument::ForType(Args::Type::Silent), Argument::ForType(Args::Type::Force), @@ -75,7 +76,7 @@ namespace AppInstaller::CLI case Execution::Args::Type::Id: case Execution::Args::Type::Name: case Execution::Args::Type::Moniker: - case Execution::Args::Type::Version: + case Execution::Args::Type::TargetVersion: case Execution::Args::Type::Channel: case Execution::Args::Type::Source: case Execution::Args::Type::ProductCode: @@ -111,7 +112,6 @@ namespace AppInstaller::CLI // --manifest case where new manifest is provided context << Workflow::GetManifestFromArg << - Workflow::ReportManifestIdentity << Workflow::SearchSourceUsingManifest << Workflow::EnsureOneMatchFromSearchResult(OperationType::Uninstall) << Workflow::UninstallSinglePackage; @@ -125,7 +125,6 @@ namespace AppInstaller::CLI Workflow::SearchSourceForSingle << Workflow::HandleSearchResultFailures << Workflow::EnsureOneMatchFromSearchResult(OperationType::Uninstall) << - Workflow::ReportPackageIdentity << Workflow::UninstallSinglePackage; } else diff --git a/src/AppInstallerCLICore/ExecutionArgs.h b/src/AppInstallerCLICore/ExecutionArgs.h @@ -54,6 +54,8 @@ namespace AppInstaller::CLI::Execution Purge, // Removes all files and directories related to a package during an uninstall. Only applies to the portable installerType. Preserve, // Retains any files and directories created by the portable exe. ProductCode, // Uninstalls using the product code as the identifier. + AllVersions, // Uninstall all versions of the package + TargetVersion, // The specific version to target //Source Command SourceName, diff --git a/src/AppInstallerCLICore/Resources.h b/src/AppInstallerCLICore/Resources.h @@ -574,6 +574,7 @@ namespace AppInstaller::CLI::Resource WINGET_DEFINE_RESOURCE_STRINGID(StateHeader); WINGET_DEFINE_RESOURCE_STRINGID(SystemArchitecture); WINGET_DEFINE_RESOURCE_STRINGID(TagArgumentDescription); + WINGET_DEFINE_RESOURCE_STRINGID(TargetVersionArgumentDescription); WINGET_DEFINE_RESOURCE_STRINGID(ThankYou); WINGET_DEFINE_RESOURCE_STRINGID(ThirdPartSoftwareNotices); WINGET_DEFINE_RESOURCE_STRINGID(ToolDescription); @@ -585,9 +586,11 @@ namespace AppInstaller::CLI::Resource WINGET_DEFINE_RESOURCE_STRINGID(Unavailable); WINGET_DEFINE_RESOURCE_STRINGID(UnexpectedErrorExecutingCommand); WINGET_DEFINE_RESOURCE_STRINGID(UninstallAbandoned); + WINGET_DEFINE_RESOURCE_STRINGID(UninstallAllVersionsArgumentDescription); WINGET_DEFINE_RESOURCE_STRINGID(UninstallCommandLongDescription); WINGET_DEFINE_RESOURCE_STRINGID(UninstallCommandReportDependencies); WINGET_DEFINE_RESOURCE_STRINGID(UninstallCommandShortDescription); + WINGET_DEFINE_RESOURCE_STRINGID(UninstallFailedDueToMultipleVersions); WINGET_DEFINE_RESOURCE_STRINGID(UninstallFailedWithCode); WINGET_DEFINE_RESOURCE_STRINGID(UninstallFlowStartingPackageUninstall); WINGET_DEFINE_RESOURCE_STRINGID(UninstallFlowUninstallSuccess); diff --git a/src/AppInstallerCLICore/Workflows/CompletionFlow.cpp b/src/AppInstallerCLICore/Workflows/CompletionFlow.cpp @@ -82,6 +82,25 @@ namespace AppInstaller::CLI::Workflow } } + void CompleteWithSearchResultInstalledVersions(Execution::Context& context) + { + const std::string& word = context.Get<Data::CompletionData>().Word(); + auto stream = context.Reporter.Completion(); + + auto installedPackage = context.Get<Execution::Data::Package>()->GetInstalled(); + + if (installedPackage) + { + for (const auto& vc : installedPackage->GetVersionKeys()) + { + if (word.empty() || Utility::ICUCaseInsensitiveStartsWith(vc.Version, word)) + { + OutputCompletionString(stream, vc.Version); + } + } + } + } + void CompleteWithSearchResultChannels(Execution::Context& context) { const std::string& word = context.Get<Data::CompletionData>().Word(); @@ -174,6 +193,13 @@ namespace AppInstaller::CLI::Workflow Workflow::EnsureOneMatchFromSearchResult(OperationType::Completion) << Workflow::CompleteWithSearchResultVersions; break; + case Execution::Args::Type::TargetVersion: + // Here we require that the standard search finds a single entry, and we list the installed versions. + context << + Workflow::SearchSourceForSingle << + Workflow::EnsureOneMatchFromSearchResult(OperationType::Completion) << + Workflow::CompleteWithSearchResultInstalledVersions; + break; case Execution::Args::Type::Channel: // Here we require that the standard search finds a single entry, and we list those channels. context << diff --git a/src/AppInstallerCLICore/Workflows/PinFlow.cpp b/src/AppInstallerCLICore/Workflows/PinFlow.cpp @@ -31,6 +31,26 @@ namespace AppInstaller::CLI::Workflow } } + void GetPinKeysForInstalled(const std::shared_ptr<IPackageVersion>& installedVersion, std::set<Pinning::PinKey>& pinKeys) + { + auto installedType = Manifest::ConvertToInstallerTypeEnum(installedVersion->GetMetadata()[PackageVersionMetadata::InstalledType]); + std::vector<Utility::LocIndString> propertyStrings; + + if (Manifest::DoesInstallerTypeUsePackageFamilyName(installedType)) + { + propertyStrings = installedVersion->GetMultiProperty(PackageVersionMultiProperty::PackageFamilyName); + } + else if (Manifest::DoesInstallerTypeUseProductCode(installedType)) + { + propertyStrings = installedVersion->GetMultiProperty(PackageVersionMultiProperty::ProductCode); + } + + for (const auto& value : propertyStrings) + { + pinKeys.emplace(Pinning::PinKey::GetPinKeyForInstalled(value)); + } + } + std::set<Pinning::PinKey> GetPinKeysForPackage(Execution::Context& context) { auto package = context.Get<Execution::Data::Package>(); @@ -42,7 +62,7 @@ namespace AppInstaller::CLI::Workflow auto installedVersion = GetInstalledVersion(package); if (installedVersion) { - pinKeys.emplace(Pinning::PinKey::GetPinKeyForInstalled(installedVersion->GetProperty(PackageVersionProperty::Id))); + GetPinKeysForInstalled(installedVersion, pinKeys); } } else diff --git a/src/AppInstallerCLICore/Workflows/RepairFlow.cpp b/src/AppInstallerCLICore/Workflows/RepairFlow.cpp @@ -441,7 +441,7 @@ namespace AppInstaller::CLI::Workflow AICLI_TERMINATE_CONTEXT(APPINSTALLER_CLI_ERROR_NO_APPLICABLE_INSTALLER); } - std::string_view requestedVersion = context.Args.Contains(Execution::Args::Type::Version) ? context.Args.GetArg(Execution::Args::Type::Version) : installedVersion.ToString(); + std::string_view requestedVersion = context.Args.Contains(Execution::Args::Type::TargetVersion) ? context.Args.GetArg(Execution::Args::Type::TargetVersion) : installedVersion.ToString(); // If it's Store source with only one version unknown, use the unknown version for available version mapping. const auto& package = context.Get<Execution::Data::Package>(); auto packageVersions = GetAvailableVersionsForInstalledVersion(package, installedPackage); diff --git a/src/AppInstallerCLICore/Workflows/UninstallFlow.cpp b/src/AppInstallerCLICore/Workflows/UninstallFlow.cpp @@ -70,8 +70,81 @@ namespace AppInstaller::CLI::Workflow void UninstallSinglePackage(Execution::Context& context) { + std::shared_ptr<ICompositePackage> package = context.Get<Execution::Data::Package>(); + std::shared_ptr<IPackage> installed = package->GetInstalled(); + std::vector<Repository::PackageVersionKey> installedVersionKeys; + if (installed) + { + installedVersionKeys = installed->GetVersionKeys(); + } + + // Handle multiple installed versions when we have been told to uninstall all of them. + if (installedVersionKeys.size() > 1 && context.Args.Contains(Execution::Args::Type::AllVersions)) + { + bool allSucceeded = true; + size_t versionsCount = installedVersionKeys.size(); + size_t versionsProgress = 0; + + for (const auto& key : installedVersionKeys) + { + context.Reporter.Info() << '(' << ++versionsProgress << '/' << versionsCount << ") "_liv; + + // We want to do best effort to uninstall all versions regardless of previous failures + auto subContextPtr = context.CreateSubContext(); + Execution::Context& uninstallContext = *subContextPtr; + auto previousThreadGlobals = uninstallContext.SetForCurrentThread(); + + uninstallContext.Add<Execution::Data::Package>(package); + uninstallContext.Add<Execution::Data::InstalledPackageVersion>(installed->GetVersion(key)); + + // Prevent individual exceptions from breaking out of the loop + try + { + uninstallContext << + Workflow::UninstallSinglePackageVersion; + } + catch (...) + { + uninstallContext.SetTerminationHR(Workflow::HandleException(uninstallContext, std::current_exception())); + } + + uninstallContext.Reporter.Info() << std::endl; + + if (uninstallContext.IsTerminated()) + { + if (context.IsTerminated() && context.GetTerminationHR() == E_ABORT) + { + // This means that the subcontext being terminated is due to an overall abort + context.Reporter.Info() << Resource::String::Cancelled << std::endl; + return; + } + + allSucceeded = false; + } + } + + if (!allSucceeded) + { + AICLI_TERMINATE_CONTEXT(APPINSTALLER_CLI_ERROR_MULTIPLE_UNINSTALL_FAILED); + } + } + else if (installedVersionKeys.size() > 1 && !context.Args.Contains(Execution::Args::Type::TargetVersion)) + { + context.Reporter.Error() << Resource::String::UninstallFailedDueToMultipleVersions << std::endl; + AICLI_TERMINATE_CONTEXT(APPINSTALLER_CLI_ERROR_MULTIPLE_APPLICATIONS_FOUND); + } + else + { + context << + Workflow::GetInstalledPackageVersion << + Workflow::UninstallSinglePackageVersion; + } + } + + void UninstallSinglePackageVersion(Execution::Context& context) + { context << - Workflow::GetInstalledPackageVersion << + Workflow::ReportInstalledPackageVersionIdentity << Workflow::EnsureSupportForUninstall << Workflow::GetUninstallInfo << Workflow::GetDependenciesInfoForUninstall << diff --git a/src/AppInstallerCLICore/Workflows/UninstallFlow.h b/src/AppInstallerCLICore/Workflows/UninstallFlow.h @@ -12,6 +12,13 @@ namespace AppInstaller::CLI::Workflow // Outputs: None void UninstallSinglePackage(Execution::Context& context); + // Uninstalls a single package version. This also does the reporting, user interaction, and recording + // for single-package uninstallation. + // RequiredArgs: None + // Inputs: InstalledPackageVersion + // Outputs: None + void UninstallSinglePackageVersion(Execution::Context& context); + // Uninstalls multiple packages. // RequiredArgs: None // Inputs: PackageSubContexts diff --git a/src/AppInstallerCLICore/Workflows/UpdateFlow.cpp b/src/AppInstallerCLICore/Workflows/UpdateFlow.cpp @@ -87,6 +87,7 @@ namespace AppInstaller::CLI::Workflow // The only way to enter this portion of the statement with isUpgrade is if the version is available if (isUpgrade) { + AICLI_LOG(CLI, Verbose, << "Updating from [" << installedVersion.ToString() << "] to [" << key.Version << "]"); upgradeVersionAvailable = true; } diff --git a/src/AppInstallerCLICore/Workflows/WorkflowBase.cpp b/src/AppInstallerCLICore/Workflows/WorkflowBase.cpp @@ -800,15 +800,27 @@ namespace AppInstaller::CLI::Workflow for (const auto& match : searchResult.Matches) { - auto installedVersion = GetInstalledVersion(match.Package); + auto installedPackage = match.Package->GetInstalled(); + if (!installedPackage) + { + continue; + } - if (installedVersion) + // We only want to evaluate update availability for the latest version. + bool isFirstInstalledVersion = true; + + for (const auto& installedVersionKey : installedPackage->GetVersionKeys()) { + bool isFirstInstalledVersionLocal = isFirstInstalledVersion; + isFirstInstalledVersion = false; + + auto installedVersion = installedPackage->GetVersion(installedVersionKey); + auto evaluator = pinningData.CreatePinStateEvaluator(pinBehavior, installedVersion); auto availableVersions = GetAvailableVersionsForInstalledVersion(match.Package, installedVersion); auto latestVersion = evaluator.GetLatestAvailableVersionForPins(availableVersions); - bool updateAvailable = evaluator.IsUpdate(latestVersion); + bool updateAvailable = isFirstInstalledVersionLocal && evaluator.IsUpdate(latestVersion); bool updateIsPinned = false; if (m_onlyShowUpgrades && !context.Args.Contains(Execution::Args::Type::IncludeUnknown) && Utility::Version(installedVersion->GetProperty(PackageVersionProperty::Version)).IsUnknown() && updateAvailable) @@ -818,7 +830,7 @@ namespace AppInstaller::CLI::Workflow continue; } - if (m_onlyShowUpgrades && !updateAvailable) + if (m_onlyShowUpgrades && !updateAvailable && isFirstInstalledVersionLocal) { // Reuse the evaluator to check if there is an update outside of the pinning auto unpinnedLatestVersion = availableVersions->GetLatestVersion(); @@ -1214,6 +1226,13 @@ namespace AppInstaller::CLI::Workflow ReportIdentity(context, {}, Resource::String::ReportIdentityFound, package->GetProperty(PackageProperty::Name), package->GetProperty(PackageProperty::Id)); } + void ReportInstalledPackageVersionIdentity(Execution::Context& context) + { + auto package = context.Get<Execution::Data::Package>(); + auto version = context.Get<Execution::Data::InstalledPackageVersion>(); + ReportIdentity(context, {}, Resource::String::ReportIdentityFound, version->GetProperty(PackageVersionProperty::Name), package ? package->GetProperty(PackageProperty::Id) : version->GetProperty(PackageVersionProperty::Id)); + } + void ReportManifestIdentity(Execution::Context& context) { const auto& manifest = context.Get<Execution::Data::Manifest>(); @@ -1349,7 +1368,38 @@ namespace AppInstaller::CLI::Workflow void GetInstalledPackageVersion(Execution::Context& context) { - context.Add<Execution::Data::InstalledPackageVersion>(GetInstalledVersion(context.Get<Execution::Data::Package>())); + std::shared_ptr<IPackage> installed = context.Get<Execution::Data::Package>()->GetInstalled(); + + if (ExperimentalFeature::IsEnabled(ExperimentalFeature::Feature::SideBySide)) + { + if (installed) + { + // TODO: This may need to be expanded dramatically to enable targeting across a variety of dimensions (architecture, etc.) + // Alternatively, if we make it easier to see the fully unique package identifiers, we may avoid that need. + if (context.Args.Contains(Execution::Args::Type::TargetVersion)) + { + Repository::PackageVersionKey versionKey{ "", context.Args.GetArg(Execution::Args::Type::TargetVersion) , "" }; + std::shared_ptr<IPackageVersion> installedVersion = installed->GetVersion(versionKey); + + if (!installedVersion) + { + context.Reporter.Error() << Resource::String::GetManifestResultVersionNotFound(Utility::LocIndView{ versionKey.Version }) << std::endl; + // This error maintains consistency with passing an available version to commands + AICLI_TERMINATE_CONTEXT(APPINSTALLER_CLI_ERROR_NO_MANIFEST_FOUND); + } + + context.Add<Execution::Data::InstalledPackageVersion>(std::move(installedVersion)); + } + else + { + context.Add<Execution::Data::InstalledPackageVersion>(installed->GetLatestVersion()); + } + } + } + else + { + context.Add<Execution::Data::InstalledPackageVersion>(GetInstalledVersion(context.Get<Execution::Data::Package>())); + } } void ReportExecutionStage::operator()(Execution::Context& context) const diff --git a/src/AppInstallerCLICore/Workflows/WorkflowBase.h b/src/AppInstallerCLICore/Workflows/WorkflowBase.h @@ -353,6 +353,12 @@ namespace AppInstaller::CLI::Workflow // Outputs: None void ReportPackageIdentity(Execution::Context& context); + // Reports the installed package version identity. + // Required Args: None + // Inputs: InstalledPackageVersion + // Outputs: None + void ReportInstalledPackageVersionIdentity(Execution::Context& context); + // Reports the manifest's identity. // Required Args: None // Inputs: Manifest diff --git a/src/AppInstallerCLIE2ETests/Helpers/TestCommon.cs b/src/AppInstallerCLIE2ETests/Helpers/TestCommon.cs @@ -1,4 +1,4 @@ -// ----------------------------------------------------------------------------- +// ----------------------------------------------------------------------------- // <copyright file="TestCommon.cs" company="Microsoft Corporation"> // Copyright (c) Microsoft Corporation. Licensed under the MIT License. // </copyright> diff --git a/src/AppInstallerCLIE2ETests/ListCommand.cs b/src/AppInstallerCLIE2ETests/ListCommand.cs @@ -1,4 +1,4 @@ -// ----------------------------------------------------------------------------- +// ----------------------------------------------------------------------------- // <copyright file="ListCommand.cs" company="Microsoft Corporation"> // Copyright (c) Microsoft Corporation. Licensed under the MIT License. // </copyright> @@ -105,20 +105,21 @@ namespace AppInstallerCLIE2ETests public void ListWithScopeExeInstalledAsMachine() { System.Guid guid = System.Guid.NewGuid(); + string identifier = "AppInstallerTest.TestExeInstaller"; string productCode = guid.ToString(); var installDir = TestCommon.GetRandomTestDir(); - var result = TestCommon.RunAICLICommand("install", $"AppInstallerTest.TestExeInstaller --override \"/InstallDir {installDir} /ProductID {productCode} /UseHKLM\""); + var result = TestCommon.RunAICLICommand("install", $"{identifier} --override \"/InstallDir {installDir} /ProductID {productCode} /UseHKLM\""); Assert.AreEqual(Constants.ErrorCode.S_OK, result.ExitCode); // List with user scope will not find the package result = TestCommon.RunAICLICommand("list", $"{productCode} --scope user"); Assert.AreEqual(Constants.ErrorCode.ERROR_NO_APPLICATIONS_FOUND, result.ExitCode); - Assert.False(result.StdOut.Contains(productCode)); + Assert.False(result.StdOut.Contains(identifier)); // List with machine scope will find the package result = TestCommon.RunAICLICommand("list", $"{productCode} --scope machine"); Assert.AreEqual(Constants.ErrorCode.S_OK, result.ExitCode); - Assert.True(result.StdOut.Contains(productCode)); + Assert.True(result.StdOut.Contains(identifier)); TestCommon.RunCommand(Path.Combine(installDir, Constants.TestExeUninstallerFileName)); } @@ -130,20 +131,21 @@ namespace AppInstallerCLIE2ETests public void ListWithScopeExeInstalledAsUser() { System.Guid guid = System.Guid.NewGuid(); + string identifier = "AppInstallerTest.TestExeInstaller"; string productCode = guid.ToString(); var installDir = TestCommon.GetRandomTestDir(); - var result = TestCommon.RunAICLICommand("install", $"AppInstallerTest.TestExeInstaller --override \"/InstallDir {installDir} /ProductID {productCode}\""); + var result = TestCommon.RunAICLICommand("install", $"{identifier} --override \"/InstallDir {installDir} /ProductID {productCode}\""); Assert.AreEqual(Constants.ErrorCode.S_OK, result.ExitCode); // List with user scope will find the package result = TestCommon.RunAICLICommand("list", $"{productCode} --scope user"); Assert.AreEqual(Constants.ErrorCode.S_OK, result.ExitCode); - Assert.True(result.StdOut.Contains(productCode)); + Assert.True(result.StdOut.Contains(identifier)); // List with machine scope will not find the package result = TestCommon.RunAICLICommand("list", $"{productCode} --scope machine"); Assert.AreEqual(Constants.ErrorCode.ERROR_NO_APPLICATIONS_FOUND, result.ExitCode); - Assert.False(result.StdOut.Contains(productCode)); + Assert.False(result.StdOut.Contains(identifier)); TestCommon.RunCommand(Path.Combine(installDir, Constants.TestExeUninstallerFileName)); } diff --git a/src/AppInstallerCLIPackage/Shared/Strings/en-us/winget.resw b/src/AppInstallerCLIPackage/Shared/Strings/en-us/winget.resw @@ -2817,6 +2817,16 @@ Please specify one of them using the --source option to proceed.</value> <data name="APPINSTALLER_CLI_ERROR_SQLITE_CONNECTION_TERMINATED" xml:space="preserve"> <value>The SQLite connection was terminated to prevent corruption.</value> </data> + <data name="UninstallAllVersionsArgumentDescription" xml:space="preserve"> + <value>Uninstall all versions</value> + </data> + <data name="TargetVersionArgumentDescription" xml:space="preserve"> + <value>The version to act upon</value> + </data> + <data name="UninstallFailedDueToMultipleVersions" xml:space="preserve"> + <value>Multiple versions of this package are installed. Either refine the search, pass the `--version` argument to select one, or pass the `--all-versions` flag to uninstall all of them.</value> + <comment>{Locked="--version,--all-versions"}</comment> + </data> <data name="PolicyEnableProxyCommandLineOptions" xml:space="preserve"> <value>Enable Windows Package Manager proxy command line options</value> <comment>Describes a Group Policy that can enable the use of the --proxy option to set a proxy</comment> diff --git a/src/AppInstallerCLITests/CompositeSource.cpp b/src/AppInstallerCLITests/CompositeSource.cpp @@ -32,9 +32,10 @@ struct ComponentTestSource : public TestSource { ComponentTestSource() = default; - ComponentTestSource(std::string_view identifier) + ComponentTestSource(std::string_view identifier, SourceOrigin origin = SourceOrigin::Default) { Details.Identifier = identifier; + Details.Origin = origin; } SearchResult Search(const SearchRequest& request) const override @@ -52,47 +53,6 @@ struct ComponentTestSource : public TestSource SearchResult Everything; }; -// A helper to create the sources used by the majority of tests in this file. -struct CompositeTestSetup -{ - CompositeTestSetup(CompositeSearchBehavior behavior = CompositeSearchBehavior::Installed) : Composite("*Tests") - { - Installed = std::make_shared<ComponentTestSource>("InstalledTestSource1"); - Available = std::make_shared<ComponentTestSource>("AvailableTestSource1"); - Composite.SetInstalledSource(Source{ Installed }, behavior); - Composite.AddAvailableSource(Source{ Available }); - } - - SearchResult Search() - { - SearchRequest request; - request.Query = RequestMatch(MatchType::Exact, s_Everything_Query); - return Composite.Search(request); - } - - std::shared_ptr<ComponentTestSource> Installed; - std::shared_ptr<ComponentTestSource> Available; - CompositeSource Composite; -}; - -// A helper to create the sources used by the majority of tests in this file. -struct CompositeWithTrackingTestSetup : public CompositeTestSetup -{ - CompositeWithTrackingTestSetup() : TrackingFactory([&](const SourceDetails&) { return Tracking; }) - { - Tracking = std::make_shared<SQLiteIndexSource>(SourceDetails{}, SQLiteIndex::CreateNew(SQLITE_MEMORY_DB_CONNECTION_TARGET)); - TestHook_SetSourceFactoryOverride(std::string{ PackageTrackingCatalogSourceFactory::Type() }, TrackingFactory); - } - - ~CompositeWithTrackingTestSetup() - { - TestHook_ClearSourceFactoryOverrides(); - } - - TestSourceFactory TrackingFactory; - std::shared_ptr<SQLiteIndexSource> Tracking; -}; - // A helper to make matches. struct Criteria : public PackageMatchFilter { @@ -160,7 +120,7 @@ struct TestPackageHelper return *this; } - operator std::shared_ptr<ICompositePackage>() + std::shared_ptr<TestCompositePackage> ToPackage() { if (!m_package) { @@ -177,6 +137,11 @@ struct TestPackageHelper return m_package; } + operator std::shared_ptr<ICompositePackage>() + { + return ToPackage(); + } + operator const Manifest::Manifest& () const { return m_manifest; @@ -190,15 +155,66 @@ private: bool m_hideSystemReferenceStrings = false; }; -TestPackageHelper MakeInstalled() +// A helper to create the sources used by the majority of tests in this file. +struct CompositeTestSetup { - return { /* isInstalled */ true}; -} + CompositeTestSetup(CompositeSearchBehavior behavior = CompositeSearchBehavior::Installed) : Composite("*Tests") + { + Installed = std::make_shared<ComponentTestSource>("InstalledTestSource1", SourceOrigin::Predefined); + Available = std::make_shared<ComponentTestSource>("AvailableTestSource1"); + Composite.SetInstalledSource(Source{ Installed }, behavior); + Composite.AddAvailableSource(Source{ Available }); + } -TestPackageHelper MakeAvailable(std::shared_ptr<ISource> source) + SearchResult Search() + { + SearchRequest request; + request.Query = RequestMatch(MatchType::Exact, s_Everything_Query); + return Composite.Search(request); + } + + TestPackageHelper MakeInstalled(std::shared_ptr<ISource> source) + { + return { /* isInstalled */ true, std::move(source)}; + } + + TestPackageHelper MakeInstalled() + { + return MakeInstalled(Installed); + } + + TestPackageHelper MakeAvailable(std::shared_ptr<ISource> source) + { + return { /* isInstalled */ false, std::move(source) }; + } + + TestPackageHelper MakeAvailable() + { + return MakeAvailable(Available); + } + + std::shared_ptr<ComponentTestSource> Installed; + std::shared_ptr<ComponentTestSource> Available; + CompositeSource Composite; +}; + +// A helper to create the sources used by the majority of tests in this file. +struct CompositeWithTrackingTestSetup : public CompositeTestSetup { - return { /* isInstalled */ false, source}; -} + CompositeWithTrackingTestSetup() : TrackingFactory([&](const SourceDetails&) { return Tracking; }) + { + Tracking = std::make_shared<SQLiteIndexSource>(SourceDetails{}, SQLiteIndex::CreateNew(SQLITE_MEMORY_DB_CONNECTION_TARGET)); + TestHook_SetSourceFactoryOverride(std::string{ PackageTrackingCatalogSourceFactory::Type() }, TrackingFactory); + } + + ~CompositeWithTrackingTestSetup() + { + TestHook_ClearSourceFactoryOverrides(); + } + + TestSourceFactory TrackingFactory; + std::shared_ptr<SQLiteIndexSource> Tracking; +}; bool SearchRequestIncludes(const std::vector<PackageMatchFilter>& filters, PackageMatchField field, MatchType type, std::optional<std::string> value = {}) { @@ -227,7 +243,7 @@ TEST_CASE("CompositeSource_PackageFamilyName_NotAvailable", "[CompositeSource]") std::string pfn = "sortof_apfn"; CompositeTestSetup setup; - setup.Installed->Everything.Matches.emplace_back(MakeInstalled().WithPFN(pfn), Criteria()); + setup.Installed->Everything.Matches.emplace_back(setup.MakeInstalled().WithPFN(pfn), Criteria()); SearchResult result = setup.Search(); @@ -241,13 +257,13 @@ TEST_CASE("CompositeSource_PackageFamilyName_Available", "[CompositeSource]") std::string pfn = "sortof_apfn"; CompositeTestSetup setup; - setup.Installed->Everything.Matches.emplace_back(MakeInstalled().WithPFN(pfn), Criteria()); + setup.Installed->Everything.Matches.emplace_back(setup.MakeInstalled().WithPFN(pfn), Criteria()); setup.Available->SearchFunction = [&](const SearchRequest& request) { RequireSearchRequestIncludes(request.Inclusions, PackageMatchField::PackageFamilyName, MatchType::Exact, pfn); SearchResult result; - result.Matches.emplace_back(MakeAvailable(setup.Available).WithPFN(pfn), Criteria()); + result.Matches.emplace_back(setup.MakeAvailable().WithPFN(pfn), Criteria()); return result; }; @@ -264,7 +280,7 @@ TEST_CASE("CompositeSource_ProductCode_NotAvailable", "[CompositeSource]") std::string pc = "thiscouldbeapc"; CompositeTestSetup setup; - setup.Installed->Everything.Matches.emplace_back(MakeInstalled().WithPC(pc), Criteria()); + setup.Installed->Everything.Matches.emplace_back(setup.MakeInstalled().WithPC(pc), Criteria()); SearchResult result = setup.Search(); @@ -278,13 +294,13 @@ TEST_CASE("CompositeSource_ProductCode_Available", "[CompositeSource]") std::string pc = "thiscouldbeapc"; CompositeTestSetup setup; - setup.Installed->Everything.Matches.emplace_back(MakeInstalled().WithPC(pc), Criteria()); + setup.Installed->Everything.Matches.emplace_back(setup.MakeInstalled().WithPC(pc), Criteria()); setup.Available->SearchFunction = [&](const SearchRequest& request) { RequireSearchRequestIncludes(request.Inclusions, PackageMatchField::ProductCode, MatchType::Exact, pc); SearchResult result; - result.Matches.emplace_back(MakeAvailable(setup.Available).WithPC(pc), Criteria()); + result.Matches.emplace_back(setup.MakeAvailable().WithPC(pc), Criteria()); return result; }; @@ -299,13 +315,13 @@ TEST_CASE("CompositeSource_ProductCode_Available", "[CompositeSource]") TEST_CASE("CompositeSource_NameAndPublisher_Match", "[CompositeSource]") { CompositeTestSetup setup; - setup.Installed->Everything.Matches.emplace_back(MakeInstalled(), Criteria()); + setup.Installed->Everything.Matches.emplace_back(setup.MakeInstalled(), Criteria()); setup.Available->SearchFunction = [&](const SearchRequest& request) { RequireSearchRequestIncludes(request.Inclusions, PackageMatchField::NormalizedNameAndPublisher, MatchType::Exact); SearchResult result; - result.Matches.emplace_back(MakeAvailable(setup.Available), Criteria()); + result.Matches.emplace_back(setup.MakeAvailable(), Criteria()); return result; }; @@ -322,12 +338,12 @@ TEST_CASE("CompositeSource_MultiMatch_FindsStrongMatch", "[CompositeSource]") std::string name = "MatchingName"; CompositeTestSetup setup; - setup.Installed->Everything.Matches.emplace_back(MakeInstalled().WithPFN("sortof_apfn"), Criteria()); + setup.Installed->Everything.Matches.emplace_back(setup.MakeInstalled().WithPFN("sortof_apfn"), Criteria()); setup.Available->SearchFunction = [&](const SearchRequest&) { SearchResult result; - result.Matches.emplace_back(MakeAvailable(setup.Available).WithId("A different ID"), Criteria(PackageMatchField::NormalizedNameAndPublisher)); - result.Matches.emplace_back(MakeAvailable(setup.Available).WithDefaultName(name), Criteria(PackageMatchField::PackageFamilyName)); + result.Matches.emplace_back(setup.MakeAvailable().WithId("A different ID"), Criteria(PackageMatchField::NormalizedNameAndPublisher)); + result.Matches.emplace_back(setup.MakeAvailable().WithDefaultName(name), Criteria(PackageMatchField::PackageFamilyName)); return result; }; @@ -345,12 +361,12 @@ TEST_CASE("CompositeSource_MultiMatch_FindsStrongMatch", "[CompositeSource]") TEST_CASE("CompositeSource_MultiMatch_DoesNotFindStrongMatch", "[CompositeSource]") { CompositeTestSetup setup; - setup.Installed->Everything.Matches.emplace_back(MakeInstalled().WithPFN("sortof_apfn"), Criteria()); + setup.Installed->Everything.Matches.emplace_back(setup.MakeInstalled().WithPFN("sortof_apfn"), Criteria()); setup.Available->SearchFunction = [&](const SearchRequest&) { SearchResult result; - result.Matches.emplace_back(MakeAvailable(setup.Available).WithId("A different ID"), Criteria(PackageMatchField::NormalizedNameAndPublisher)); - result.Matches.emplace_back(MakeAvailable(setup.Available).WithId("Another diff ID"), Criteria(PackageMatchField::NormalizedNameAndPublisher)); + result.Matches.emplace_back(setup.MakeAvailable().WithId("A different ID"), Criteria(PackageMatchField::NormalizedNameAndPublisher)); + result.Matches.emplace_back(setup.MakeAvailable().WithId("Another diff ID"), Criteria(PackageMatchField::NormalizedNameAndPublisher)); return result; }; @@ -366,8 +382,8 @@ TEST_CASE("CompositeSource_FoundByBothRootSearches", "[CompositeSource]") std::string pfn = "sortof_apfn"; CompositeTestSetup setup; - auto installedPackage = MakeInstalled().WithPFN(pfn); - auto availablePackage = MakeAvailable(setup.Available).WithPFN(pfn); + auto installedPackage = setup.MakeInstalled().WithPFN(pfn); + auto availablePackage = setup.MakeAvailable().WithPFN(pfn); setup.Installed->Everything.Matches.emplace_back(installedPackage, Criteria()); setup.Installed->SearchFunction = [&](const SearchRequest& request) @@ -407,17 +423,17 @@ TEST_CASE("CompositeSource_OnlyAvailableFoundByRootSearch", "[CompositeSource]") RequireSearchRequestIncludes(request.Inclusions, PackageMatchField::PackageFamilyName, MatchType::Exact, pfn); SearchResult result; - result.Matches.emplace_back(MakeInstalled().WithPFN(pfn), Criteria()); + result.Matches.emplace_back(setup.MakeInstalled().WithPFN(pfn), Criteria()); return result; }; - setup.Available->Everything.Matches.emplace_back(MakeAvailable(setup.Available).WithPFN(pfn), Criteria()); + setup.Available->Everything.Matches.emplace_back(setup.MakeAvailable().WithPFN(pfn), Criteria()); setup.Available->SearchFunction = [&](const SearchRequest& request) { RequireSearchRequestIncludes(request.Inclusions, PackageMatchField::PackageFamilyName, MatchType::Exact, pfn); SearchResult result; - result.Matches.emplace_back(MakeAvailable(setup.Available).WithPFN(pfn), Criteria()); + result.Matches.emplace_back(setup.MakeAvailable().WithPFN(pfn), Criteria()); return result; }; @@ -434,13 +450,13 @@ TEST_CASE("CompositeSource_FoundByAvailableRootSearch_NotInstalled", "[Composite std::string pfn = "sortof_apfn"; CompositeTestSetup setup; - setup.Available->Everything.Matches.emplace_back(MakeAvailable(setup.Available).WithPFN(pfn), Criteria()); + setup.Available->Everything.Matches.emplace_back(setup.MakeAvailable().WithPFN(pfn), Criteria()); setup.Available->SearchFunction = [&](const SearchRequest& request) { RequireSearchRequestIncludes(request.Inclusions, PackageMatchField::PackageFamilyName, MatchType::Exact, pfn); SearchResult result; - result.Matches.emplace_back(MakeAvailable(setup.Available).WithPFN(pfn), Criteria()); + result.Matches.emplace_back(setup.MakeAvailable().WithPFN(pfn), Criteria()); return result; }; @@ -456,8 +472,8 @@ TEST_CASE("CompositeSource_UpdateWithBetterMatchCriteria", "[CompositeSource]") MatchType type = MatchType::Exact; CompositeTestSetup setup; - auto installedPackage = MakeInstalled().WithPFN(pfn); - auto availablePackage = MakeAvailable(setup.Available).WithPFN(pfn); + auto installedPackage = setup.MakeInstalled().WithPFN(pfn); + auto availablePackage = setup.MakeAvailable().WithPFN(pfn); setup.Installed->Everything.Matches.emplace_back(installedPackage, Criteria()); @@ -504,7 +520,7 @@ TEST_CASE("CompositePackage_PropertyFromInstalled", "[CompositeSource]") std::string id = "Special test ID"; CompositeTestSetup setup; - setup.Installed->Everything.Matches.emplace_back(MakeInstalled().WithId(id), Criteria()); + setup.Installed->Everything.Matches.emplace_back(setup.MakeInstalled().WithId(id), Criteria()); SearchResult result = setup.Search(); @@ -518,11 +534,11 @@ TEST_CASE("CompositePackage_PropertyFromAvailable", "[CompositeSource]") std::string pfn = "sortof_apfn"; CompositeTestSetup setup; - setup.Installed->Everything.Matches.emplace_back(MakeInstalled().WithPFN(pfn), Criteria()); + setup.Installed->Everything.Matches.emplace_back(setup.MakeInstalled().WithPFN(pfn), Criteria()); setup.Available->SearchFunction = [&](const SearchRequest&) { SearchResult result; - result.Matches.emplace_back(MakeAvailable(setup.Available).WithId(id), Criteria()); + result.Matches.emplace_back(setup.MakeAvailable().WithId(id), Criteria()); return result; }; @@ -538,7 +554,7 @@ TEST_CASE("CompositePackage_AvailableVersions_ChannelFilteredOut", "[CompositeSo std::string channel = "Channel"; CompositeTestSetup setup; - setup.Installed->Everything.Matches.emplace_back(MakeInstalled().WithPFN(pfn), Criteria()); + setup.Installed->Everything.Matches.emplace_back(setup.MakeInstalled().WithPFN(pfn), Criteria()); setup.Available->SearchFunction = [&](const SearchRequest&) { Manifest::Manifest noChannel = MakeDefaultManifest(); @@ -580,7 +596,7 @@ TEST_CASE("CompositePackage_AvailableVersions_NoChannelFilteredOut", "[Composite std::string channel = "Channel"; CompositeTestSetup setup; - setup.Installed->Everything.Matches.emplace_back(MakeInstalled().WithPFN(pfn).WithChannel(channel), Criteria()); + setup.Installed->Everything.Matches.emplace_back(setup.MakeInstalled().WithPFN(pfn).WithChannel(channel), Criteria()); setup.Available->SearchFunction = [&](const SearchRequest&) { Manifest::Manifest noChannel = MakeDefaultManifest(); @@ -628,14 +644,14 @@ TEST_CASE("CompositeSource_MultipleAvailableSources_MatchAll", "[CompositeSource std::shared_ptr<ComponentTestSource> secondAvailable = std::make_shared<ComponentTestSource>(); setup.Composite.AddAvailableSource(Source{ secondAvailable }); - setup.Installed->Everything.Matches.emplace_back(MakeInstalled().WithPFN(pfn), Criteria()); + setup.Installed->Everything.Matches.emplace_back(setup.MakeInstalled().WithPFN(pfn), Criteria()); setup.Available->SearchFunction = [&](const SearchRequest& request) { RequireSearchRequestIncludes(request.Inclusions, PackageMatchField::PackageFamilyName, MatchType::Exact, pfn); SearchResult result; - result.Matches.emplace_back(MakeAvailable(setup.Available).WithDefaultName(firstName), Criteria()); + result.Matches.emplace_back(setup.MakeAvailable().WithDefaultName(firstName), Criteria()); return result; }; @@ -644,7 +660,7 @@ TEST_CASE("CompositeSource_MultipleAvailableSources_MatchAll", "[CompositeSource RequireSearchRequestIncludes(request.Inclusions, PackageMatchField::PackageFamilyName, MatchType::Exact, pfn); SearchResult result; - result.Matches.emplace_back(MakeAvailable(secondAvailable).WithDefaultName(secondName), Criteria()); + result.Matches.emplace_back(setup.MakeAvailable(secondAvailable).WithDefaultName(secondName), Criteria()); return result; }; @@ -667,14 +683,14 @@ TEST_CASE("CompositeSource_MultipleAvailableSources_MatchSecond", "[CompositeSou std::shared_ptr<ComponentTestSource> secondAvailable = std::make_shared<ComponentTestSource>(); setup.Composite.AddAvailableSource(Source{ secondAvailable }); - setup.Installed->Everything.Matches.emplace_back(MakeInstalled().WithPFN(pfn), Criteria()); + setup.Installed->Everything.Matches.emplace_back(setup.MakeInstalled().WithPFN(pfn), Criteria()); secondAvailable->SearchFunction = [&](const SearchRequest& request) { RequireSearchRequestIncludes(request.Inclusions, PackageMatchField::PackageFamilyName, MatchType::Exact, pfn); SearchResult result; - result.Matches.emplace_back(MakeAvailable(setup.Available).WithDefaultName(secondName), Criteria()); + result.Matches.emplace_back(setup.MakeAvailable().WithDefaultName(secondName), Criteria()); return result; }; @@ -690,9 +706,9 @@ TEST_CASE("CompositeSource_MultipleAvailableSources_ReverseMatchBoth", "[Composi { std::string pfn = "sortof_apfn"; - auto installedPackage = MakeInstalled().WithPFN(pfn); - CompositeTestSetup setup; + auto installedPackage = setup.MakeInstalled().WithPFN(pfn); + std::shared_ptr<ComponentTestSource> secondAvailable = std::make_shared<ComponentTestSource>(); setup.Composite.AddAvailableSource(Source{ secondAvailable }); @@ -705,8 +721,8 @@ TEST_CASE("CompositeSource_MultipleAvailableSources_ReverseMatchBoth", "[Composi return result; }; - setup.Available->Everything.Matches.emplace_back(MakeAvailable(setup.Available).WithPFN(pfn), Criteria()); - secondAvailable->Everything.Matches.emplace_back(MakeAvailable(setup.Available).WithPFN(pfn), Criteria()); + setup.Available->Everything.Matches.emplace_back(setup.MakeAvailable().WithPFN(pfn), Criteria()); + secondAvailable->Everything.Matches.emplace_back(setup.MakeAvailable().WithPFN(pfn), Criteria()); SearchResult result = setup.Search(); @@ -719,7 +735,7 @@ TEST_CASE("CompositeSource_MultipleAvailableSources_ReverseMatchBoth", "[Composi TEST_CASE("CompositeSource_IsSame", "[CompositeSource]") { CompositeTestSetup setup; - setup.Installed->Everything.Matches.emplace_back(MakeInstalled().WithPFN("sortof_apfn"), Criteria()); + setup.Installed->Everything.Matches.emplace_back(setup.MakeInstalled().WithPFN("sortof_apfn"), Criteria()); SearchResult result1 = setup.Search(); REQUIRE(result1.Matches.size() == 1); @@ -740,7 +756,7 @@ TEST_CASE("CompositeSource_AvailableSearchFailure", "[CompositeSource]") AvailableSucceeds->SearchFunction = [&](const SearchRequest&) { SearchResult result; - result.Matches.emplace_back(MakeAvailable({}).WithPFN(pfn), Criteria()); + result.Matches.emplace_back(TestPackageHelper{ /* isInstalled */ false }.WithPFN(pfn), Criteria()); return result; }; @@ -784,8 +800,8 @@ TEST_CASE("CompositeSource_InstalledToAvailableCorrelationSearchFailure", "[Comp std::string pfn = "sortof_apfn"; CompositeTestSetup setup; - setup.Installed->Everything.Matches.emplace_back(MakeInstalled().WithPFN(pfn), Criteria()); - setup.Available->Everything.Matches.emplace_back(MakeAvailable(setup.Available).WithPFN(pfn), Criteria()); + setup.Installed->Everything.Matches.emplace_back(setup.MakeInstalled().WithPFN(pfn), Criteria()); + setup.Available->Everything.Matches.emplace_back(setup.MakeAvailable().WithPFN(pfn), Criteria()); std::shared_ptr<ComponentTestSource> AvailableFails = std::make_shared<ComponentTestSource>(); AvailableFails->SearchFunction = [&](const SearchRequest&) -> SearchResult { THROW_HR(expectedHR); }; @@ -823,7 +839,7 @@ TEST_CASE("CompositeSource_InstalledAvailableSearchFailure", "[CompositeSource]" setup.Available->SearchFunction = [&](const SearchRequest&) { SearchResult result; - result.Matches.emplace_back(MakeAvailable(setup.Available).WithPFN(pfn), Criteria()); + result.Matches.emplace_back(setup.MakeAvailable().WithPFN(pfn), Criteria()); return result; }; @@ -864,8 +880,8 @@ TEST_CASE("CompositeSource_TrackingPackageFound", "[CompositeSource]") std::string pfn = "sortof_apfn"; CompositeWithTrackingTestSetup setup; - auto installedPackage = MakeInstalled().WithPFN(pfn); - auto availablePackage = MakeAvailable(setup.Available).WithPFN(pfn).WithId(availableID).WithDefaultName(s_Everything_Query); + auto installedPackage = setup.MakeInstalled().WithPFN(pfn); + auto availablePackage = setup.MakeAvailable().WithPFN(pfn).WithId(availableID).WithDefaultName(s_Everything_Query); setup.Installed->Everything.Matches.emplace_back(installedPackage, Criteria()); setup.Installed->SearchFunction = [&](const SearchRequest& request) @@ -914,8 +930,8 @@ TEST_CASE("CompositeSource_TrackingPackageFound_MetadataPopulatedFromTracking", std::string pfn = "sortof_apfn"; CompositeWithTrackingTestSetup setup; - auto installedPackage = MakeInstalled().WithPFN(pfn); - auto availablePackage = MakeAvailable(setup.Available).WithPFN(pfn).WithId(availableID).WithDefaultName(s_Everything_Query); + auto installedPackage = setup.MakeInstalled().WithPFN(pfn); + auto availablePackage = setup.MakeAvailable().WithPFN(pfn).WithId(availableID).WithDefaultName(s_Everything_Query); setup.Installed->Everything.Matches.emplace_back(installedPackage, Criteria()); setup.Installed->SearchFunction = [&](const SearchRequest& request) @@ -975,8 +991,8 @@ TEST_CASE("CompositeSource_TrackingFound_AvailableNot", "[CompositeSource]") std::string pfn = "sortof_apfn"; CompositeWithTrackingTestSetup setup; - auto installedPackage = MakeInstalled().WithPFN(pfn); - auto availablePackage = MakeAvailable(setup.Available).WithPFN(pfn).WithId(availableID).WithDefaultName(s_Everything_Query); + auto installedPackage = setup.MakeInstalled().WithPFN(pfn); + auto availablePackage = setup.MakeAvailable().WithPFN(pfn).WithId(availableID).WithDefaultName(s_Everything_Query); setup.Installed->Everything.Matches.emplace_back(installedPackage, Criteria()); setup.Installed->SearchFunction = [&](const SearchRequest& request) @@ -1007,8 +1023,8 @@ TEST_CASE("CompositeSource_TrackingFound_AvailablePath", "[CompositeSource]") std::string availableID = "Available.ID"; std::string pfn = "sortof_apfn"; - auto installedPackage = MakeInstalled().WithPFN(pfn); - auto availablePackage = MakeAvailable(setup.Available).WithPFN(pfn).WithId(availableID).WithDefaultName(s_Everything_Query); + auto installedPackage = setup.MakeInstalled().WithPFN(pfn); + auto availablePackage = setup.MakeAvailable().WithPFN(pfn).WithId(availableID).WithDefaultName(s_Everything_Query); setup.Installed->SearchFunction = [&](const SearchRequest& request) { @@ -1049,8 +1065,8 @@ TEST_CASE("CompositeSource_TrackingFound_NotInstalled", "[CompositeSource]") std::string pfn = "sortof_apfn"; CompositeWithTrackingTestSetup setup; - auto installedPackage = MakeInstalled().WithPFN(pfn); - auto availablePackage = MakeAvailable(setup.Available).WithPFN(pfn).WithId(availableID).WithDefaultName(s_Everything_Query); + auto installedPackage = setup.MakeInstalled().WithPFN(pfn); + auto availablePackage = setup.MakeAvailable().WithPFN(pfn).WithId(availableID).WithDefaultName(s_Everything_Query); setup.Available->Everything.Matches.emplace_back(availablePackage, Criteria()); @@ -1064,7 +1080,7 @@ TEST_CASE("CompositeSource_TrackingFound_NotInstalled", "[CompositeSource]") TEST_CASE("CompositeSource_NullInstalledVersion", "[CompositeSource]") { CompositeTestSetup setup; - setup.Installed->Everything.Matches.emplace_back(MakeAvailable(setup.Available), Criteria()); + setup.Installed->Everything.Matches.emplace_back(setup.MakeAvailable(), Criteria()); // We are mostly testing to see if a null installed version causes an AV or not SearchResult result = setup.Search(); @@ -1074,7 +1090,7 @@ TEST_CASE("CompositeSource_NullInstalledVersion", "[CompositeSource]") TEST_CASE("CompositeSource_NullAvailableVersion", "[CompositeSource]") { CompositeTestSetup setup{ CompositeSearchBehavior::AvailablePackages }; - setup.Available->Everything.Matches.emplace_back(MakeInstalled(), Criteria()); + setup.Available->Everything.Matches.emplace_back(setup.MakeInstalled(), Criteria()); // We are mostly testing to see if a null available version causes an AV or not REQUIRE_THROWS_HR(setup.Search(), E_UNEXPECTED); @@ -1156,7 +1172,7 @@ TEST_CASE("CompositeSource_Pinning_AvailableVersionPinned", "[CompositeSource][P TestUserSettings userSettings; CompositeTestSetup setup; - auto installedPackage = TestCompositePackage::Make(MakeDefaultManifest("1.0.1"sv), TestCompositePackage::MetadataMap{}); + auto installedPackage = setup.MakeInstalled().WithVersion("1.0.1"sv); setup.Installed->Everything.Matches.emplace_back(installedPackage, Criteria()); setup.Available->SearchFunction = [&](const SearchRequest&) @@ -1267,7 +1283,7 @@ TEST_CASE("CompositeSource_Pinning_OneSourcePinned", "[CompositeSource][PinFlow] TestUserSettings userSettings; CompositeTestSetup setup; - auto installedPackage = TestCompositePackage::Make(MakeDefaultManifest("1.0"sv), TestCompositePackage::MetadataMap{}); + auto installedPackage = setup.MakeInstalled().WithVersion("1.0"sv); setup.Installed->Everything.Matches.emplace_back(installedPackage, Criteria()); setup.Available->SearchFunction = [&](const SearchRequest&) @@ -1323,7 +1339,7 @@ TEST_CASE("CompositeSource_Pinning_OneSourceGated", "[CompositeSource][PinFlow]" TestUserSettings userSettings; CompositeTestSetup setup; - auto installedPackage = TestCompositePackage::Make(MakeDefaultManifest("1.0"sv), TestCompositePackage::MetadataMap{}); + auto installedPackage = setup.MakeInstalled().WithVersion("1.0.1"sv); setup.Installed->Everything.Matches.emplace_back(installedPackage, Criteria()); setup.Available->SearchFunction = [&](const SearchRequest&) @@ -1389,11 +1405,12 @@ TEST_CASE("CompositeSource_Pinning_MultipleInstalled", "[CompositeSource][PinFlo std::string productCode1 = "product-code1"; std::string productCode2 = "product-code2"; + CompositeTestSetup setup; + // Installed packages differ in product code and version - auto installedPackage1 = MakeInstalled().WithId(productCode1).WithPC(productCode1).WithVersion("1.1"sv); - auto installedPackage2 = MakeInstalled().WithId(productCode2).WithPC(productCode2).WithVersion("1.2"sv); + auto installedPackage1 = setup.MakeInstalled().WithId(productCode1).WithPC(productCode1).WithVersion("1.1"sv); + auto installedPackage2 = setup.MakeInstalled().WithId(productCode2).WithPC(productCode2).WithVersion("1.2"sv); - CompositeTestSetup setup; setup.Installed->SearchFunction = [&](const SearchRequest& request) { bool isSearchById = SearchRequestIncludes(request.Inclusions, PackageMatchField::Id, MatchType::Exact, packageId); @@ -1416,7 +1433,7 @@ TEST_CASE("CompositeSource_Pinning_MultipleInstalled", "[CompositeSource][PinFlo setup.Available->SearchFunction = [&](const SearchRequest&) { SearchResult result; - result.Matches.emplace_back(MakeAvailable(setup.Available).WithId(packageId).WithVersion("2.0"sv), Criteria()); + result.Matches.emplace_back(setup.MakeAvailable().WithId(packageId).WithVersion("2.0"sv), Criteria()); return result; }; @@ -1520,7 +1537,7 @@ TEST_CASE("CompositeSource_CorrelateToInstalledContainsManifestData", "[Composit setup.Available->SearchFunction = [&](const SearchRequest&) { SearchResult result; - result.Matches.emplace_back(MakeAvailable(setup.Available).WithPC("hello"), Criteria()); + result.Matches.emplace_back(setup.MakeAvailable().WithPC("hello"), Criteria()); return result; }; @@ -1551,7 +1568,7 @@ TEST_CASE("CompositeSource_Respects_FeatureFlag_ManifestMayContainAdditionalSyst setup.Available->SearchFunction = [&](const SearchRequest&) { SearchResult result; - result.Matches.emplace_back(MakeAvailable(setup.Available).WithId(id).WithPC(productCode1).HideSRS(), Criteria()); + result.Matches.emplace_back(setup.MakeAvailable().WithId(id).WithPC(productCode1).HideSRS(), Criteria()); return result; }; @@ -1577,3 +1594,224 @@ TEST_CASE("CompositeSource_Respects_FeatureFlag_ManifestMayContainAdditionalSyst REQUIRE(productCodeSearched); } } + +TEST_CASE("CompositeSource_SxS_TwoVersions_NoAvailable", "[CompositeSource][SideBySide]") +{ + auto enableFeature = TestUserSettings::EnableExperimentalFeature(Settings::ExperimentalFeature::Feature::SideBySide); + + std::string productCode1 = "PC1"; + std::string productCode2 = "PC2"; + + CompositeTestSetup setup; + auto availablePackage = setup.MakeAvailable(); + + setup.Installed->Everything.Matches.emplace_back(setup.MakeInstalled().WithVersion("1.0").WithPC(productCode1), Criteria()); + setup.Installed->Everything.Matches.emplace_back(setup.MakeInstalled().WithVersion("2.0").WithPC(productCode2), Criteria()); + + SearchResult result = setup.Search(); + + REQUIRE(result.Matches.size() == 2); +} + +TEST_CASE("CompositeSource_SxS_TwoVersions_DifferentAvailable", "[CompositeSource][SideBySide]") +{ + auto enableFeature = TestUserSettings::EnableExperimentalFeature(Settings::ExperimentalFeature::Feature::SideBySide); + + std::string productCode1 = "PC1"; + std::string productCode2 = "PC2"; + + CompositeTestSetup setup; + auto availablePackage1 = setup.MakeAvailable().ToPackage(); + auto availablePackage2 = setup.MakeAvailable().ToPackage(); + + setup.Installed->Everything.Matches.emplace_back(setup.MakeInstalled().WithVersion("1.0").WithPC(productCode1), Criteria()); + setup.Installed->Everything.Matches.emplace_back(setup.MakeInstalled().WithVersion("2.0").WithPC(productCode2), Criteria()); + + setup.Available->SearchFunction = [&](const SearchRequest& request) + { + SearchResult result; + + std::string productCode; + for (const auto& item : request.Inclusions) + { + if (item.Field == PackageMatchField::ProductCode) + { + productCode = item.Value; + break; + } + } + + if (productCode == productCode1) + { + result.Matches.emplace_back(availablePackage1, Criteria()); + } + else if (productCode == productCode1) + { + result.Matches.emplace_back(availablePackage2, Criteria()); + } + + return result; + }; + + SearchResult result = setup.Search(); + + REQUIRE(result.Matches.size() == 2); +} + +TEST_CASE("CompositeSource_SxS_TwoVersions_SameAvailable", "[CompositeSource][SideBySide]") +{ + auto enableFeature = TestUserSettings::EnableExperimentalFeature(Settings::ExperimentalFeature::Feature::SideBySide); + + std::string version1 = "1.0"; + std::string version2 = "2.0"; + std::string productCode1 = "PC1"; + std::string productCode2 = "PC2"; + + CompositeTestSetup setup; + auto availablePackage = setup.MakeAvailable().ToPackage(); + + setup.Installed->Everything.Matches.emplace_back(setup.MakeInstalled().WithVersion(version1).WithPC(productCode1), Criteria()); + setup.Installed->Everything.Matches.emplace_back(setup.MakeInstalled().WithVersion(version2).WithPC(productCode2), Criteria()); + + setup.Available->SearchFunction = [&](const SearchRequest&) + { + SearchResult result; + result.Matches.emplace_back(availablePackage, Criteria()); + return result; + }; + + SearchResult result = setup.Search(); + + REQUIRE(result.Matches.size() == 1); + auto package = result.Matches[0].Package; + REQUIRE(package); + auto installedPackage = package->GetInstalled(); + REQUIRE(installedPackage); + auto installedVersions = installedPackage->GetVersionKeys(); + REQUIRE(installedVersions.size() == 2); + REQUIRE(std::any_of(installedVersions.begin(), installedVersions.end(), [&](const PackageVersionKey& key) { return key.Version == version1; })); + REQUIRE(std::any_of(installedVersions.begin(), installedVersions.end(), [&](const PackageVersionKey& key) { return key.Version == version2; })); + auto availablePackages = package->GetAvailable(); + REQUIRE(availablePackages.size() == 1); + REQUIRE(availablePackages[0]->IsSame(availablePackage->Available[0].get())); +} + +TEST_CASE("CompositeSource_SxS_ThreeVersions_SameAvailable", "[CompositeSource][SideBySide]") +{ + auto enableFeature = TestUserSettings::EnableExperimentalFeature(Settings::ExperimentalFeature::Feature::SideBySide); + + std::string version1 = "1.0"; + std::string version2 = "2.0"; + std::string version3 = "3.0"; + std::string productCode1 = "PC1"; + std::string productCode2 = "PC2"; + std::string productCode3 = "PC3"; + + CompositeTestSetup setup; + auto availablePackage = setup.MakeAvailable().ToPackage(); + + setup.Installed->Everything.Matches.emplace_back(setup.MakeInstalled().WithVersion(version1).WithPC(productCode1), Criteria()); + setup.Installed->Everything.Matches.emplace_back(setup.MakeInstalled().WithVersion(version2).WithPC(productCode2), Criteria()); + setup.Installed->Everything.Matches.emplace_back(setup.MakeInstalled().WithVersion(version3).WithPC(productCode3), Criteria()); + + setup.Available->SearchFunction = [&](const SearchRequest&) + { + SearchResult result; + result.Matches.emplace_back(availablePackage, Criteria()); + return result; + }; + + SearchResult result = setup.Search(); + + REQUIRE(result.Matches.size() == 1); + auto package = result.Matches[0].Package; + REQUIRE(package); + auto installedPackage = package->GetInstalled(); + REQUIRE(installedPackage); + auto installedVersions = installedPackage->GetVersionKeys(); + REQUIRE(installedVersions.size() == 3); + REQUIRE(std::any_of(installedVersions.begin(), installedVersions.end(), [&](const PackageVersionKey& key) { return key.Version == version1; })); + REQUIRE(std::any_of(installedVersions.begin(), installedVersions.end(), [&](const PackageVersionKey& key) { return key.Version == version2; })); + REQUIRE(std::any_of(installedVersions.begin(), installedVersions.end(), [&](const PackageVersionKey& key) { return key.Version == version3; })); + auto availablePackages = package->GetAvailable(); + REQUIRE(availablePackages.size() == 1); + REQUIRE(availablePackages[0]->IsSame(availablePackage->Available[0].get())); +} + +TEST_CASE("CompositeSource_SxS_TwoVersions_SameAvailable_Tracking", "[CompositeSource][SideBySide]") +{ + auto enableFeature = TestUserSettings::EnableExperimentalFeature(Settings::ExperimentalFeature::Feature::SideBySide); + + std::string version1 = "1.0"; + std::string version2 = "2.0"; + std::string productCode1 = "PC1"; + std::string productCode2 = "PC2"; + + CompositeWithTrackingTestSetup setup; + auto installedPackage1 = setup.MakeInstalled().WithVersion(version1).WithPC(productCode1); + auto availablePackage = setup.MakeAvailable().ToPackage(); + + setup.Installed->Everything.Matches.emplace_back(installedPackage1, Criteria()); + setup.Installed->Everything.Matches.emplace_back(setup.MakeInstalled().WithVersion(version2).WithPC(productCode2), Criteria()); + setup.Tracking->GetIndex().AddManifest(installedPackage1); + + setup.Available->SearchFunction = [&](const SearchRequest&) + { + SearchResult result; + result.Matches.emplace_back(availablePackage, Criteria()); + return result; + }; + + SearchResult result = setup.Search(); + + REQUIRE(result.Matches.size() == 1); + auto package = result.Matches[0].Package; + REQUIRE(package); + auto installedPackage = package->GetInstalled(); + REQUIRE(installedPackage); + auto installedVersions = installedPackage->GetVersionKeys(); + REQUIRE(installedVersions.size() == 2); + REQUIRE(std::any_of(installedVersions.begin(), installedVersions.end(), [&](const PackageVersionKey& key) { return key.Version == version1; })); + REQUIRE(std::any_of(installedVersions.begin(), installedVersions.end(), [&](const PackageVersionKey& key) { return key.Version == version2; })); + auto availablePackages = package->GetAvailable(); + REQUIRE(availablePackages.size() == 1); + REQUIRE(availablePackages[0]->IsSame(availablePackage->Available[0].get())); +} + +TEST_CASE("CompositeSource_SxS_Available_TwoVersions_SameAvailable", "[CompositeSource][SideBySide]") +{ + auto enableFeature = TestUserSettings::EnableExperimentalFeature(Settings::ExperimentalFeature::Feature::SideBySide); + + std::string version1 = "1.0"; + std::string version2 = "2.0"; + std::string productCode1 = "PC1"; + std::string productCode2 = "PC2"; + + CompositeTestSetup setup; + auto availablePackage = setup.MakeAvailable().ToPackage(); + + setup.Installed->SearchFunction = [&](const SearchRequest&) + { + SearchResult result; + result.Matches.emplace_back(setup.MakeInstalled().WithVersion(version1).WithPC(productCode1), Criteria()); + result.Matches.emplace_back(setup.MakeInstalled().WithVersion(version2).WithPC(productCode2), Criteria()); + return result; + }; + + setup.Available->Everything.Matches.emplace_back(availablePackage, Criteria()); + + SearchResult result = setup.Search(); + + REQUIRE(result.Matches.size() == 1); + auto package = result.Matches[0].Package; + REQUIRE(package); + auto installedPackage = package->GetInstalled(); + REQUIRE(installedPackage); + auto installedVersions = installedPackage->GetVersionKeys(); + REQUIRE(installedVersions.size() == 2); + REQUIRE(std::any_of(installedVersions.begin(), installedVersions.end(), [&](const PackageVersionKey& key) { return key.Version == version1; })); + REQUIRE(std::any_of(installedVersions.begin(), installedVersions.end(), [&](const PackageVersionKey& key) { return key.Version == version2; })); + auto availablePackages = package->GetAvailable(); + REQUIRE(availablePackages.size() == 1); + REQUIRE(availablePackages[0]->IsSame(availablePackage->Available[0].get())); +} diff --git a/src/AppInstallerCLITests/PredefinedInstalledSource.cpp b/src/AppInstallerCLITests/PredefinedInstalledSource.cpp @@ -147,6 +147,11 @@ std::shared_ptr<ISource> CreatePredefinedInstalledSource(Factory::Filter filter return factory->Create(details)->Open(progress); } +SQLiteIndex CreateMemoryIndex() +{ + return SQLiteIndex::CreateNew(SQLITE_MEMORY_DB_CONNECTION_TARGET, SQLite::Version::Latest(), SQLiteIndex::CreateOptions::SupportPathless); +} + TEST_CASE("ARPHelper_GetARPForArchitecture", "[arphelper][list]") { auto systemArch = GetSystemArchitecture(); @@ -296,7 +301,7 @@ TEST_CASE("ARPHelper_PopulateIndexFromKey_Single", "[arphelper][list]") AddARPEntryToKey(root.get(), helper, entry); - auto index = SQLiteIndex::CreateNew(SQLITE_MEMORY_DB_CONNECTION_TARGET); + auto index = CreateMemoryIndex(); helper.PopulateIndexFromKey(index, key, s_TestScope, "TestArchitecture"); auto result = index.Search({}); @@ -332,7 +337,7 @@ TEST_CASE("ARPHelper_PopulateIndexFromKey_SingleValid", "[arphelper][list]") { "Nothing" }, }); - auto index = SQLiteIndex::CreateNew(SQLITE_MEMORY_DB_CONNECTION_TARGET); + auto index = CreateMemoryIndex(); helper.PopulateIndexFromKey(index, key, s_TestScope, "TestArchitecture"); auto result = index.Search({}); @@ -368,7 +373,7 @@ TEST_CASE("ARPHelper_PopulateIndexFromKey_Two", "[arphelper][list]") AddARPEntryToKey(root.get(), helper, entry1); AddARPEntryToKey(root.get(), helper, entry2); - auto index = SQLiteIndex::CreateNew(SQLITE_MEMORY_DB_CONNECTION_TARGET); + auto index = CreateMemoryIndex(); helper.PopulateIndexFromKey(index, key, s_TestScope, "TestArchitecture"); REQUIRE(index.Search({}).Matches.size() == 2); diff --git a/src/AppInstallerCLITests/TestCommon.cpp b/src/AppInstallerCLITests/TestCommon.cpp @@ -249,6 +249,23 @@ namespace TestCommon AppInstaller::Settings::SetUserSettingsOverride(nullptr); } + std::unique_ptr<TestUserSettings> TestUserSettings::EnableExperimentalFeature(Settings::ExperimentalFeature::Feature feature, bool keepFileSettings) + { + std::unique_ptr<TestUserSettings> result = std::make_unique<TestUserSettings>(keepFileSettings); + + // Due to the template usage, this needs to be updated for any features that want to use it. + switch (feature) + { + case Settings::ExperimentalFeature::Feature::SideBySide: + result->Set<Settings::Setting::EFSideBySide>(true); + break; + default: + THROW_HR(E_NOTIMPL); + } + + return result; + } + bool InstallCertFromSignedPackage(const std::filesystem::path& package) { auto [certContext, certStore] = AppInstaller::Msix::GetCertContextFromMsix(package); diff --git a/src/AppInstallerCLITests/TestCommon.h b/src/AppInstallerCLITests/TestCommon.h @@ -5,10 +5,12 @@ #include <AppInstallerProgress.h> #include <AppxPackaging.h> #include <winget/UserSettings.h> +#include <winget/ExperimentalFeature.h> #include <wil/result.h> #include <filesystem> #include <functional> +#include <memory> #include <string> #define REQUIRE_THROWS_HR(_expr_, _hr_) REQUIRE_THROWS_MATCHES(_expr_, wil::ResultException, ::TestCommon::ResultExceptionHRMatcher(_hr_)) @@ -141,6 +143,8 @@ namespace TestCommon { m_settings[S].emplace<AppInstaller::Settings::details::SettingIndex(S)>(std::move(value)); } + + static std::unique_ptr<TestUserSettings> EnableExperimentalFeature(AppInstaller::Settings::ExperimentalFeature::Feature feature, bool keepFileSettings = false); }; // Below cert installation/uninstallation methods require admin privilege, diff --git a/src/AppInstallerCLITests/TestSource.cpp b/src/AppInstallerCLITests/TestSource.cpp @@ -9,6 +9,15 @@ using namespace AppInstaller::Repository; namespace TestCommon { + namespace + { + size_t GetNextTestPackageId() + { + static std::atomic_size_t packageId(0); + return ++packageId; + } + } + TestPackageVersion::TestPackageVersion(const Manifest& manifest, MetadataMap installationMetadata, std::weak_ptr<const ISource> source) : VersionManifest(manifest), Metadata(std::move(installationMetadata)), Source(source) {} @@ -120,6 +129,7 @@ namespace TestCommon TestPackage::TestPackage(const std::vector<Manifest>& available, std::weak_ptr<const ISource> source, bool hideSystemReferenceStringsOnVersion) : Source(source) { + DefaultIsSameIdentity = GetNextTestPackageId(); for (const auto& manifest : available) { Versions.emplace_back(TestPackageVersion::Make(manifest, source, hideSystemReferenceStringsOnVersion)); @@ -129,6 +139,7 @@ namespace TestCommon TestPackage::TestPackage(const Manifest& installed, MetadataMap installationMetadata, std::weak_ptr<const ISource> source) : Source(source) { + DefaultIsSameIdentity = GetNextTestPackageId(); Versions.emplace_back(TestPackageVersion::Make(installed, std::move(installationMetadata), source)); } @@ -203,23 +214,14 @@ namespace TestCommon return IsSameOverride(this, other); } - const TestPackage* otherAvailable = PackageCast<const TestPackage*>(other); + const TestPackage* otherPackage = PackageCast<const TestPackage*>(other); - if (!otherAvailable || - Versions.size() != otherAvailable->Versions.size()) + if (otherPackage && DefaultIsSameIdentity == otherPackage->DefaultIsSameIdentity) { - return false; + return true; } - for (size_t i = 0; i < Versions.size(); ++i) - { - if (Versions[i].get() != otherAvailable->Versions[i].get()) - { - return false; - } - } - - return true; + return false; } const void* TestPackage::CastTo(IPackageType type) const diff --git a/src/AppInstallerCLITests/TestSource.h b/src/AppInstallerCLITests/TestSource.h @@ -74,6 +74,7 @@ namespace TestCommon std::vector<std::shared_ptr<AppInstaller::Repository::IPackageVersion>> Versions; std::weak_ptr<const ISource> Source; + size_t DefaultIsSameIdentity = 0; std::function<bool(const IPackage*, const IPackage*)> IsSameOverride; }; diff --git a/src/AppInstallerCommonCore/Architecture.cpp b/src/AppInstallerCommonCore/Architecture.cpp @@ -187,6 +187,25 @@ namespace AppInstaller::Utility return Architecture::Unknown; } + std::optional<::AppInstaller::Utility::Architecture> ConvertToArchitectureEnum(winrt::Windows::System::ProcessorArchitecture architecture) + { + switch (architecture) + { + case winrt::Windows::System::ProcessorArchitecture::X86: + return ::AppInstaller::Utility::Architecture::X86; + case winrt::Windows::System::ProcessorArchitecture::Arm: + return ::AppInstaller::Utility::Architecture::Arm; + case winrt::Windows::System::ProcessorArchitecture::X64: + return ::AppInstaller::Utility::Architecture::X64; + case winrt::Windows::System::ProcessorArchitecture::Neutral: + return ::AppInstaller::Utility::Architecture::Neutral; + case winrt::Windows::System::ProcessorArchitecture::Arm64: + return ::AppInstaller::Utility::Architecture::Arm64; + } + + return {}; + } + LocIndView ToString(Architecture architecture) { switch (architecture) diff --git a/src/AppInstallerCommonCore/ExperimentalFeature.cpp b/src/AppInstallerCommonCore/ExperimentalFeature.cpp @@ -44,6 +44,8 @@ namespace AppInstaller::Settings return userSettings.Get<Setting::EFResume>(); case ExperimentalFeature::Feature::Configuration03: return userSettings.Get<Setting::EFConfiguration03>(); + case ExperimentalFeature::Feature::SideBySide: + return userSettings.Get<Setting::EFSideBySide>(); case ExperimentalFeature::Feature::Proxy: return userSettings.Get<Setting::EFProxy>(); default: @@ -79,6 +81,8 @@ namespace AppInstaller::Settings return ExperimentalFeature{ "Resume", "resume", "https://aka.ms/winget-settings", Feature::Resume }; case Feature::Configuration03: return ExperimentalFeature{ "Configuration Schema 0.3", "configuration03", "https://aka.ms/winget-settings", Feature::Configuration03 }; + case Feature::SideBySide: + return ExperimentalFeature{ "Side-by-side improvements", "sideBySide", "https://aka.ms/winget-settings", Feature::SideBySide }; case Feature::Proxy: return ExperimentalFeature{ "Proxy", "proxy", "https://aka.ms/winget-settings", Feature::Proxy }; default: diff --git a/src/AppInstallerCommonCore/Public/AppInstallerArchitecture.h b/src/AppInstallerCommonCore/Public/AppInstallerArchitecture.h @@ -1,9 +1,10 @@ // Copyright (c) Microsoft Corporation. // Licensed under the MIT License. #pragma once - +#include <optional> #include <vector> #include <winget/LocIndependent.h> +#include <winrt/Windows.System.h> namespace AppInstaller::Utility { @@ -22,6 +23,9 @@ namespace AppInstaller::Utility // Converts a string to corresponding enum Architecture ConvertToArchitectureEnum(std::string_view archStr); + // Converts an ProcessorArchitecture to an Architecture + std::optional<Architecture> ConvertToArchitectureEnum(winrt::Windows::System::ProcessorArchitecture architecture); + // Converts an Architecture to a string_view LocIndView ToString(Architecture architecture); diff --git a/src/AppInstallerCommonCore/Public/winget/ExperimentalFeature.h b/src/AppInstallerCommonCore/Public/winget/ExperimentalFeature.h @@ -26,6 +26,7 @@ namespace AppInstaller::Settings Resume = 0x2, Configuration03 = 0x4, Proxy = 0x8, + SideBySide = 0x10, Max, // This MUST always be after all experimental features // Features listed after Max will not be shown with the features command diff --git a/src/AppInstallerCommonCore/Public/winget/Pin.h b/src/AppInstallerCommonCore/Public/winget/Pin.h @@ -43,7 +43,7 @@ namespace AppInstaller::Pinning // but they break when the package is updated outside of winget. struct PinKey { - PinKey() {} + PinKey() = default; PinKey(const Manifest::Manifest::string_t& packageId, std::string_view sourceId) : PackageId(packageId), SourceId(sourceId) {} @@ -80,6 +80,12 @@ namespace AppInstaller::Pinning struct Pin { + Pin(const Pin&) = default; + Pin& operator=(const Pin& other) = default; + + Pin(Pin&&) = default; + Pin& operator=(Pin&&) = default; + static Pin CreateBlockingPin(PinKey&& pinKey); static Pin CreatePinningPin(PinKey&& pinKey); static Pin CreateGatingPin(PinKey&& pinKey, Utility::GatedVersion&& gatedVersion); diff --git a/src/AppInstallerCommonCore/Public/winget/UserSettings.h b/src/AppInstallerCommonCore/Public/winget/UserSettings.h @@ -72,6 +72,7 @@ namespace AppInstaller::Settings EFDirectMSI, EFResume, EFConfiguration03, + EFSideBySide, EFProxy, // Telemetry TelemetryDisable, @@ -152,6 +153,7 @@ namespace AppInstaller::Settings SETTINGMAPPING_SPECIALIZATION(Setting::EFDirectMSI, bool, bool, false, ".experimentalFeatures.directMSI"sv); SETTINGMAPPING_SPECIALIZATION(Setting::EFResume, bool, bool, false, ".experimentalFeatures.resume"sv); SETTINGMAPPING_SPECIALIZATION(Setting::EFConfiguration03, bool, bool, false, ".experimentalFeatures.configuration03"sv); + SETTINGMAPPING_SPECIALIZATION(Setting::EFSideBySide, bool, bool, false, ".experimentalFeatures.sideBySide"sv); SETTINGMAPPING_SPECIALIZATION(Setting::EFProxy, bool, bool, false, ".experimentalFeatures.proxy"sv); // Telemetry SETTINGMAPPING_SPECIALIZATION(Setting::TelemetryDisable, bool, bool, false, ".telemetry.disable"sv); diff --git a/src/AppInstallerCommonCore/UserSettings.cpp b/src/AppInstallerCommonCore/UserSettings.cpp @@ -261,6 +261,7 @@ namespace AppInstaller::Settings WINGET_VALIDATE_PASS_THROUGH(EFDirectMSI) WINGET_VALIDATE_PASS_THROUGH(EFResume) WINGET_VALIDATE_PASS_THROUGH(EFConfiguration03) + WINGET_VALIDATE_PASS_THROUGH(EFSideBySide) WINGET_VALIDATE_PASS_THROUGH(EFProxy) WINGET_VALIDATE_PASS_THROUGH(AnonymizePathForDisplay) WINGET_VALIDATE_PASS_THROUGH(TelemetryDisable) diff --git a/src/AppInstallerRepositoryCore/CompositeSource.cpp b/src/AppInstallerRepositoryCore/CompositeSource.cpp @@ -100,14 +100,12 @@ namespace AppInstaller::Repository } } - // For a given package from a tracking catalog, get the latest write time and its corresponding package version. + // For a given package from a tracking catalog, get the latest write time. // Look at all versions rather than just the latest to account for the potential of downgrading. - std::pair<std::chrono::system_clock::time_point, std::shared_ptr<IPackageVersion>> GetLatestTrackingWriteTimeAndPackageVersion( - const std::shared_ptr<ICompositePackage>& trackingCompositePackage) + std::chrono::system_clock::time_point GetLatestTrackingWriteTime( + const std::shared_ptr<IPackage>& trackingPackage) { - std::shared_ptr<IPackage> trackingPackage = OnlyAvailable(trackingCompositePackage); - std::chrono::system_clock::time_point resultTime{}; - std::shared_ptr<IPackageVersion> resultVersion; + std::chrono::system_clock::time_point result{}; for (const auto& key : trackingPackage->GetVersionKeys()) { @@ -127,16 +125,15 @@ namespace AppInstaller::Repository std::chrono::system_clock::time_point versionTime = Utility::ConvertUnixEpochToSystemClock(unixEpoch); - if (versionTime > resultTime) + if (versionTime > result) { - resultTime = versionTime; - resultVersion = version; + result = versionTime; } } } } - return { resultTime, std::move(resultVersion) }; + return result; } // An installed package's version reported in ARP does not necessarily match the versions used for the manifest. @@ -341,50 +338,78 @@ namespace AppInstaller::Repository }; // An IPackage for the installed package of a CompositePackage. - // Supports only a single version of a single package at this time. struct CompositeInstalledPackage : public IPackage { static constexpr IPackageType PackageType = IPackageType::CompositeInstalledPackage; - CompositeInstalledPackage(std::shared_ptr<IPackage> package, Source trackingSource, std::shared_ptr<IPackageVersion> trackingPackageVersion, std::string overrideVersion = {}) : - m_package(std::move(package)), m_trackingSource(std::move(trackingSource)), m_trackingPackageVersion(std::move(trackingPackageVersion)), m_overrideVersion(std::move(overrideVersion)) - {} + CompositeInstalledPackage(std::shared_ptr<IPackage> package) + { + AddPackageAndVersionKeyData(std::move(package)); + } Utility::LocIndString GetProperty(PackageProperty property) const override { - return m_package->GetProperty(property); + THROW_HR_IF(E_UNEXPECTED, m_packages.empty() || m_versionKeyData.empty()); + + // Use the highest version for package properties + return m_packages[m_versionKeyData[0].PackageIndex]->GetProperty(property); } std::vector<PackageVersionKey> GetVersionKeys() const override { - auto result = m_package->GetVersionKeys(); - THROW_HR_IF(E_UNEXPECTED, result.size() != 1); - if (!m_overrideVersion.empty()) - { - result.front().Version = m_overrideVersion; - } - return result; + return { m_versionKeyData.begin(), m_versionKeyData.end() }; } - std::shared_ptr<IPackageVersion> GetVersion(const PackageVersionKey& versionKey) const + std::shared_ptr<IPackageVersion> GetVersion(const PackageVersionKey& versionKey) const override { - return (GetVersionKeys().front().IsMatch(versionKey)) ? GetLatestVersion() : std::shared_ptr<IPackageVersion>{}; + std::shared_ptr<IPackageVersion> installedVersion; + std::string overrideVersion; + + for (const VersionKeyData& key : m_versionKeyData) + { + if (key.IsMatch(versionKey)) + { + installedVersion = key.InstalledVersion; + overrideVersion = key.Version; + break; + } + } + + if (installedVersion) + { + // Get the appropriate tracking version or latest if it is not found. + // The tracking package uses the mapped version. + std::shared_ptr<IPackageVersion> trackingPackageVersion; + if (m_trackingPackage) + { + // Remove our use of the package id as source + PackageVersionKey versionKey_NoSource = versionKey; + versionKey_NoSource.SourceId.clear(); + + trackingPackageVersion = m_trackingPackage->GetVersion(versionKey_NoSource); + + if (!trackingPackageVersion) + { + trackingPackageVersion = m_trackingPackage->GetLatestVersion(); + } + } + + return std::make_shared<CompositeInstalledVersion>(std::move(installedVersion), m_trackingSource, std::move(trackingPackageVersion), std::move(overrideVersion)); + } + + return nullptr; } std::shared_ptr<IPackageVersion> GetLatestVersion() const override { - return std::make_shared<CompositeInstalledVersion>(m_package->GetLatestVersion(), m_trackingSource, m_trackingPackageVersion, m_overrideVersion); + return GetVersion({}); } Source GetSource() const override { // If there is a tracking source, use it instead to indicate that it came from there. - if (m_trackingSource) - { - return m_trackingSource; - } - - return m_package->GetSource(); + // Otherwise, all of the installed packages should be from the same source. + return m_trackingSource ? m_trackingSource : m_packages[0]->GetSource(); } bool IsSame(const IPackage* other) const override @@ -393,7 +418,31 @@ namespace AppInstaller::Repository if (otherPackage) { - return m_package->IsSame(otherPackage->m_package.get()); + if (m_packages.size() != otherPackage->m_packages.size()) + { + return false; + } + + for (const auto& subPackage : m_packages) + { + bool foundSame = false; + + for (const auto& otherSubPackage : otherPackage->m_packages) + { + if (subPackage->IsSame(otherSubPackage.get())) + { + foundSame = true; + break; + } + } + + if (!foundSame) + { + return false; + } + } + + return true; } return false; @@ -409,54 +458,194 @@ namespace AppInstaller::Repository return nullptr; } + void SetTracking( + Source trackingSource, + std::shared_ptr<IPackage> trackingPackage, + std::chrono::system_clock::time_point trackingWriteTime) + { + m_trackingSource = std::move(trackingSource); + m_trackingPackage = std::move(trackingPackage); + m_trackingWriteTime = trackingWriteTime; + } + + Source GetTrackingSource() const + { + return m_trackingSource; + } + + const std::shared_ptr<IPackage>& GetTrackingPackage() const + { + return m_trackingPackage; + } + + std::chrono::system_clock::time_point GetTrackingPackageWriteTime() const + { + return m_trackingWriteTime; + } + + bool ContainsInstalledPackage(const IPackage* installedPackage) const + { + for (const auto& package : m_packages) + { + if (package->IsSame(installedPackage)) + { + return true; + } + } + + return false; + } + + void FoldInstalledIn(const std::shared_ptr<CompositeInstalledPackage>& other) + { + for (const auto& package : other->m_packages) + { + AddPackageAndVersionKeyData(package); + } + } + + // Set a version that will override the version string from the installed package + void SetOverrideInstalledVersion(const std::shared_ptr<IPackage>& availablePackage) + { + if (availablePackage) + { + m_availablePackageVersionOverride = availablePackage; + + for (auto& key : m_versionKeyData) + { + if (Manifest::DoesInstallerTypeSupportArpVersionRange(key.InstalledType)) + { + key.Version = GetMappedInstalledVersion(key.InstalledVersion->GetProperty(PackageVersionProperty::Version), availablePackage); + } + } + } + } + + bool IsEmpty() const + { + return m_versionKeyData.empty(); + } + private: - std::shared_ptr<IPackage> m_package; + // Contains information about all of the version keys. + // We use the `SourceId` field to store the installed package identifier so that we can disambiguate keys is they have the same version. + struct VersionKeyData : public PackageVersionKey + { + size_t PackageIndex; + std::shared_ptr<IPackageVersion> InstalledVersion; + Manifest::InstallerTypeEnum InstalledType; + Utility::VersionAndChannel VersionAndChannel; + + bool operator<(const VersionKeyData& other) const + { + return VersionAndChannel < other.VersionAndChannel; + } + }; + + // Adds the package and version key data to the composite. + // The version keys are then sorted so that the first (index 0) in the vector has the highest version. + // Note that it may tied for highest version if, for instance, the same version is installed for different architectures. + void AddPackageAndVersionKeyData(std::shared_ptr<IPackage> package) + { + // We don't want this to happen, but it could. Rather than a crash, we will log it and move on. + if (!package) + { + AICLI_LOG(Repo, Verbose, << "AddPackageAndVersionKeyData called with an empty package"); + return; + } + + size_t packageIndex = m_packages.size(); + std::string packageIdentifier = package->GetProperty(PackageProperty::Id); + bool versionAdded = false; + + for (const auto& versionKey : package->GetVersionKeys()) + { + VersionKeyData keyData{ versionKey }; + + keyData.PackageIndex = packageIndex; + keyData.InstalledVersion = package->GetVersion(versionKey); + + if (!keyData.InstalledVersion) + { + AICLI_LOG(Repo, Verbose, << "AddPackageAndVersionKeyData: Package [" << packageIdentifier << "] did not return a version for [" << versionKey.Version << "]"); + continue; + } + + // We use the `SourceId` field to store the installed package identifier so that we can disambiguate keys if they have the same version. + keyData.SourceId = packageIdentifier; + + keyData.InstalledType = Manifest::ConvertToInstallerTypeEnum(keyData.InstalledVersion->GetMetadata()[PackageVersionMetadata::InstalledType]); + if (m_availablePackageVersionOverride && Manifest::DoesInstallerTypeSupportArpVersionRange(keyData.InstalledType)) + { + keyData.Version = GetMappedInstalledVersion(keyData.InstalledVersion->GetProperty(PackageVersionProperty::Version), m_availablePackageVersionOverride); + } + + keyData.VersionAndChannel = Utility::VersionAndChannel{ keyData.Version, keyData.Channel }; + + m_versionKeyData.emplace_back(std::move(keyData)); + versionAdded = true; + } + + if (versionAdded) + { + m_packages.emplace_back(std::move(package)); + + std::sort(m_versionKeyData.begin(), m_versionKeyData.end()); + } + } + + std::vector<std::shared_ptr<IPackage>> m_packages; + std::vector<VersionKeyData> m_versionKeyData; Source m_trackingSource; - std::shared_ptr<IPackageVersion> m_trackingPackageVersion; - std::string m_overrideVersion; + std::shared_ptr<IPackage> m_trackingPackage; + std::chrono::system_clock::time_point m_trackingWriteTime = std::chrono::system_clock::time_point::min(); + std::shared_ptr<IPackage> m_availablePackageVersionOverride; }; // An ICompositePackage for the CompositeSource. struct CompositePackage : public ICompositePackage { // The availablePackage may only contain one available package within it, as it is expected to be the output of a search on a single source. - CompositePackage(const std::shared_ptr<ICompositePackage>& installedPackage, const std::shared_ptr<ICompositePackage>& availablePackage = {}) + CompositePackage(const std::shared_ptr<ICompositePackage>& installedPackage, const std::shared_ptr<ICompositePackage>& availablePackage = {}, bool setPrimary = false) { if (installedPackage) { - m_installedPackage = installedPackage->GetInstalled(); + m_installedPackage = std::make_shared<CompositeInstalledPackage>(installedPackage->GetInstalled()); + + // If the installed package result existed, but didn't actually create any installed versions, drop it. + if (m_installedPackage->IsEmpty()) + { + m_installedPackage.reset(); + } } - AddAvailablePackage(availablePackage); + AddAvailablePackage(availablePackage, setPrimary); } Utility::LocIndString GetProperty(PackageProperty property) const override { - std::shared_ptr<IPackage> truth; - if (m_defaultAvailablePackage) + IPackage* truth = nullptr; + if (m_primaryAvailablePackage) { - truth = m_defaultAvailablePackage; + truth = m_primaryAvailablePackage.get(); } - if (!truth) + if (!truth && !m_availablePackages.empty()) { - truth = m_trackingPackage; + truth = m_availablePackages[0].get(); } if (!truth) { - truth = m_installedPackage; + truth = m_installedPackage.get(); } + THROW_HR_IF(E_UNEXPECTED, !truth); + return truth->GetProperty(property); } std::shared_ptr<IPackage> GetInstalled() override { - if (m_installedPackage) - { - return std::make_shared<CompositeInstalledPackage>(m_installedPackage, m_trackingSource, m_trackingPackageVersion, m_overrideInstalledVersion); - } - - return {}; + return m_installedPackage; } std::vector<std::shared_ptr<IPackage>> GetAvailable() override @@ -464,6 +653,11 @@ namespace AppInstaller::Repository return m_availablePackages; } + const std::vector<std::shared_ptr<IPackage>>& GetAvailablePackages() + { + return m_availablePackages; + } + bool IsSameAsAnyAvailable(const IPackage* other) const { if (other) @@ -480,69 +674,96 @@ namespace AppInstaller::Repository return false; } - std::shared_ptr<IPackage> GetInstalledPackage() const + const std::shared_ptr<CompositeInstalledPackage>& GetInstalledPackage() const { return m_installedPackage; } - const std::shared_ptr<IPackage>& GetTrackingPackage() const + bool ContainsInstalledPackage(const IPackage* installedPackage) const { - return m_trackingPackage; + return m_installedPackage ? m_installedPackage->ContainsInstalledPackage(installedPackage) : false; } - void AddAvailablePackage(const std::shared_ptr<ICompositePackage>& availablePackage) + void AddAvailablePackage(const std::shared_ptr<ICompositePackage>& availablePackage, bool setPrimary = false) { if (availablePackage) { - std::shared_ptr<IPackage> singlePackage = OnlyAvailable(availablePackage); + // Disable primary if feature not enabled + setPrimary = setPrimary && ExperimentalFeature::IsEnabled(ExperimentalFeature::Feature::SideBySide); + + m_availablePackages.emplace_back(OnlyAvailable(availablePackage)); - if (!m_defaultAvailablePackage) + if (setPrimary) { - // Set override only with the first available version found - m_defaultAvailablePackage = singlePackage; - TrySetOverrideInstalledVersion(m_defaultAvailablePackage); + m_primaryAvailablePackage = m_availablePackages.back(); } - m_availablePackages.emplace_back(std::move(singlePackage)); + // Set override for primary or with the first available version found + if (setPrimary || m_availablePackages.size() == 1) + { + TrySetOverrideInstalledVersion(m_availablePackages.back()); + } } } - void SetTracking(Source trackingSource, std::shared_ptr<IPackage> trackingPackage, std::shared_ptr<IPackageVersion> trackingPackageVersion) + std::shared_ptr<IPackage>& GetPrimaryAvailablePackage() { - m_trackingSource = std::move(trackingSource); - m_trackingPackage = std::move(trackingPackage); - m_trackingPackageVersion = std::move(trackingPackageVersion); + return m_primaryAvailablePackage; } - const Source& GetTrackingSource() const + Source GetTrackingSource() const { - return m_trackingSource; + return m_installedPackage ? m_installedPackage->GetTrackingSource() : Source{}; + } + + std::shared_ptr<IPackage> GetTrackingPackage() const + { + return m_installedPackage ? m_installedPackage->GetTrackingPackage() : std::shared_ptr<IPackage>{}; + } + + std::chrono::system_clock::time_point GetTrackingPackageWriteTime() const + { + return m_installedPackage ? m_installedPackage->GetTrackingPackageWriteTime() : std::chrono::system_clock::time_point::min(); + } + + void SetTracking( + Source trackingSource, + std::shared_ptr<IPackage> trackingPackage, + std::chrono::system_clock::time_point trackingWriteTime) + { + if (m_installedPackage) + { + m_installedPackage->SetTracking(std::move(trackingSource), std::move(trackingPackage), trackingWriteTime); + } + } + + void FoldInstalledIn(const std::shared_ptr<CompositePackage>& other) + { + if (other->m_installedPackage) + { + if (m_installedPackage) + { + m_installedPackage->FoldInstalledIn(other->m_installedPackage); + } + else + { + m_installedPackage = other->m_installedPackage; + } + } } private: // Try to set a version that will override the version string from the installed package - void TrySetOverrideInstalledVersion(std::shared_ptr<IPackage> availablePackage) + void TrySetOverrideInstalledVersion(const std::shared_ptr<IPackage>& availablePackage) { if (m_installedPackage && availablePackage) { - auto installedVersion = m_installedPackage->GetLatestVersion(); - if (installedVersion) - { - auto installedType = Manifest::ConvertToInstallerTypeEnum(installedVersion->GetMetadata()[PackageVersionMetadata::InstalledType]); - if (Manifest::DoesInstallerTypeSupportArpVersionRange(installedType)) - { - m_overrideInstalledVersion = GetMappedInstalledVersion(installedVersion->GetProperty(PackageVersionProperty::Version), availablePackage); - } - } + m_installedPackage->SetOverrideInstalledVersion(availablePackage); } } - std::shared_ptr<IPackage> m_installedPackage; - Source m_trackingSource; - std::shared_ptr<IPackage> m_trackingPackage; - std::shared_ptr<IPackageVersion> m_trackingPackageVersion; - std::string m_overrideInstalledVersion; - std::shared_ptr<IPackage> m_defaultAvailablePackage; + std::shared_ptr<CompositeInstalledPackage> m_installedPackage; + std::shared_ptr<IPackage> m_primaryAvailablePackage; std::vector<std::shared_ptr<IPackage>> m_availablePackages; }; @@ -658,13 +879,122 @@ namespace AppInstaller::Repository result.Purpose = searchPurpose; return result; } + + std::shared_ptr<IPackage> AddSystemReferenceStringsFromTrackingPackage(const PackageTrackingCatalog& trackingCatalog, const Utility::LocIndString& identifier, std::string_view sourceIdentifier) + { + SearchRequest trackingRequest; + trackingRequest.Filters.emplace_back(PackageMatchField::Id, MatchType::CaseInsensitive, identifier.get()); + + SearchResult trackingResult = trackingCatalog.Search(trackingRequest); + + if (trackingResult.Matches.size() == 1) + { + std::shared_ptr<IPackage> result = OnlyAvailable(trackingResult.Matches[0].Package); + AddSystemReferenceStrings(result.get()); + return result; + } + else + { + AICLI_LOG(Repo, Warning, << "Found multiple results for Id [" << identifier << "] in tracking catalog for: " << sourceIdentifier); + return {}; + } + } + + void AddSystemReferenceStrings(IPackage* package) + { + for (auto const& versionKey : package->GetVersionKeys()) + { + auto version = package->GetVersion(versionKey); + AddSystemReferenceStrings(version.get()); + } + } + + void AddSystemReferenceStrings(IPackageVersion* version) + { + GetSystemReferenceStrings( + version, + PackageVersionMultiProperty::PackageFamilyName, + PackageMatchField::PackageFamilyName); + + GetSystemReferenceStrings( + version, + PackageVersionMultiProperty::ProductCode, + PackageMatchField::ProductCode); + + GetSystemReferenceStrings( + version, + PackageVersionMultiProperty::UpgradeCode, + PackageMatchField::UpgradeCode); + + GetNameAndPublisher( + version); + } + + void AddSystemReferenceStringsFromManifest(const Manifest::Manifest& manifest) + { + for (const auto& pfn : manifest.GetPackageFamilyNames()) + { + AddIfNotPresent(SystemReferenceString{ PackageMatchField::PackageFamilyName, Utility::LocIndString{ pfn } }); + } + for (const auto& productCode : manifest.GetProductCodes()) + { + AddIfNotPresent(SystemReferenceString{ PackageMatchField::ProductCode, Utility::LocIndString{ productCode } }); + } + for (const auto& upgradeCode : manifest.GetUpgradeCodes()) + { + AddIfNotPresent(SystemReferenceString{ PackageMatchField::UpgradeCode, Utility::LocIndString{ upgradeCode } }); + } + for (const auto& name : manifest.GetPackageNames()) + { + for (const auto& publisher : manifest.GetPublishers()) + { + AddIfNotPresent(SystemReferenceString{ + PackageMatchField::NormalizedNameAndPublisher, + Utility::LocIndString{ name }, + Utility::LocIndString{ publisher } }); + } + } + } + + private: + void GetSystemReferenceStrings( + IPackageVersion* installedVersion, + PackageVersionMultiProperty prop, + PackageMatchField field) + { + for (auto&& string : installedVersion->GetMultiProperty(prop)) + { + AddIfNotPresent(SystemReferenceString{ field, std::move(string) }); + } + } + + void GetNameAndPublisher( + IPackageVersion* installedVersion) + { + // Unfortunately the names and publishers are unique and not tied to each other strictly, so we need + // to go broad on the matches. Future work can hopefully make name and publisher operate more as a unit, + // but for now we have to search for the cartesian of these... + auto names = installedVersion->GetMultiProperty(PackageVersionMultiProperty::Name); + auto publishers = installedVersion->GetMultiProperty(PackageVersionMultiProperty::Publisher); + + for (size_t i = 0; i < names.size(); ++i) + { + for (size_t j = 0; j < publishers.size(); ++j) + { + AddIfNotPresent(SystemReferenceString{ + PackageMatchField::NormalizedNameAndPublisher, + names[i], + publishers[j] }); + } + } + } }; - // For a given package version, prepares the results for it. - PackageData GetSystemReferenceStrings(IPackageVersion* version) + // For a given package, prepares the results for it. + PackageData GetSystemReferenceStrings(IPackage* package) { PackageData result; - AddSystemReferenceStrings(version, result); + result.AddSystemReferenceStrings(package); return result; } @@ -695,66 +1025,51 @@ namespace AppInstaller::Repository for (auto const& versionKey : availablePackage->GetVersionKeys()) { auto packageVersion = availablePackage->GetVersion(versionKey); - AddSystemReferenceStrings(packageVersion.get(), result); + result.AddSystemReferenceStrings(packageVersion.get()); if (downloadManifests && manifestsDownloaded < c_downloadManifestsLimit) { auto manifest = packageVersion->GetManifest(); - AddSystemReferenceStringsFromManifest(manifest, result); + result.AddSystemReferenceStringsFromManifest(manifest); manifestsDownloaded++; } } return result; } - // Check for a package already in the result that should have been correlated already. - // If we find one, see if we should upgrade it's match criteria. - // If we don't, return package data for further use. - std::optional<PackageData> CheckForExistingResultFromTrackingPackageMatch(const ResultMatch& trackingMatch) + // Determines if the results contain the given installed package. + bool ContainsInstalledPackage(const IPackage* installedPackage) const { - std::shared_ptr<IPackage> trackingMatchPackage = OnlyAvailable(trackingMatch.Package); - for (auto& match : Matches) { - const std::shared_ptr<IPackage>& trackingPackage = match.Package->GetTrackingPackage(); - if (trackingPackage && trackingPackage->IsSame(trackingMatchPackage.get())) + if (match.Package->ContainsInstalledPackage(installedPackage)) { - if (ResultMatchComparator{}(trackingMatch, match)) - { - match.MatchCriteria = trackingMatch.MatchCriteria; - } - - return {}; + return true; } } - PackageData result; - for (auto const& versionKey : trackingMatchPackage->GetVersionKeys()) - { - auto packageVersion = trackingMatchPackage->GetVersion(versionKey); - AddSystemReferenceStrings(packageVersion.get(), result); - } - return result; + return false; } // Determines if the results contain the given installed package. - bool ContainsInstalledPackage(const IPackage* installedPackage) + std::shared_ptr<CompositePackage> FindInstalledPackage(const IPackage* installedPackage) const { for (auto& match : Matches) { - const std::shared_ptr<IPackage>& matchPackage = match.Package->GetInstalledPackage(); - if (matchPackage && matchPackage->IsSame(installedPackage)) + if (match.Package->ContainsInstalledPackage(installedPackage)) { - return true; + return match.Package; } } - return false; + return {}; } - // Destructively converts the result to the standard variant. - operator SearchResult() && + // *Destructively* converts the result to the standard variant. + SearchResult ConvertToSearchResult() { + FoldResults(); + SearchResult result; result.Matches.reserve(Matches.size()); @@ -812,95 +1127,218 @@ namespace AppInstaller::Repository return result; } - std::vector<CompositeResultMatch> Matches; - bool Truncated = false; - std::vector<SearchResult::Failure> Failures; - - private: - void AddSystemReferenceStrings(IPackageVersion* version, PackageData& data) + // Group results in an attempt to have a single result that covers all installed versions. + // This is expected to be called immediately after the installed search portion, + // when each result will contain a single installed version and some number of available packages. + // + // The folds that happen are: + // 1. When results have the same primary available package (the primary available package is set due to tracking data) + // 2. When a result has no primary available package, but another result does have a primary that matches one of the available + // a. Choose the latest primary if there are multiple + // 3. When multiple results have no primary available package and share the same available package set + // a. There are many potential additional rules that could be made here, but we will start with the simplest version. + // + // Potential improvements: + // 1. Attempting correlation of non-primary available packages to allow folding in more complex cases + // a. For example, if installed A has {source1:package1, source2:package2} and installed B has {source1:package1}, can we + // make sure that source1:package1 and source2:package2 are in fact "the same" to confidently say that installed A and B + // are side by side versions. + // 2. Attempt correlation by installed data only + // a. We can potentially detect multiple instances of the same installed item with the same correlation logic turned back on + // the installed source. This would allow for folding even when the package is not in any available source. + void FoldResults() { - GetSystemReferenceStrings( - version, - PackageVersionMultiProperty::PackageFamilyName, - PackageMatchField::PackageFamilyName, - data); + if (!ExperimentalFeature::IsEnabled(ExperimentalFeature::Feature::SideBySide)) + { + return; + } - GetSystemReferenceStrings( - version, - PackageVersionMultiProperty::ProductCode, - PackageMatchField::ProductCode, - data); + // The key to uniquely identify the package in the map + struct InstalledResultFoldKey + { + InstalledResultFoldKey() = default; - GetSystemReferenceStrings( - version, - PackageVersionMultiProperty::UpgradeCode, - PackageMatchField::UpgradeCode, - data); + InstalledResultFoldKey(const std::shared_ptr<IPackage>& package) + { + std::shared_ptr<IPackageVersion> latestAvailable = package->GetLatestVersion(); + if (latestAvailable) + { + SourceIdentifier = latestAvailable->GetSource().GetIdentifier(); + PackageIdentifier = latestAvailable->GetProperty(PackageVersionProperty::Id); + } + } - GetNameAndPublisher( - version, - data); - } + // Hash operation + size_t operator()(const InstalledResultFoldKey& value) const noexcept + { + std::hash<std::string> hashString; + return hashString(value.SourceIdentifier) ^ (hashString(value.PackageIdentifier) << 1); + } - void AddSystemReferenceStringsFromManifest(const Manifest::Manifest& manifest, PackageData& data) - { - for (const auto& pfn : manifest.GetPackageFamilyNames()) - { - data.AddIfNotPresent(SystemReferenceString{ PackageMatchField::PackageFamilyName, Utility::LocIndString{ pfn } }); - } - for (const auto& productCode : manifest.GetProductCodes()) - { - data.AddIfNotPresent(SystemReferenceString{ PackageMatchField::ProductCode, Utility::LocIndString{ productCode } }); - } - for (const auto& upgradeCode : manifest.GetUpgradeCodes()) + bool operator==(const InstalledResultFoldKey& other) const noexcept + { + // Treat both empty as invalid and never equal + if (SourceIdentifier.empty() && PackageIdentifier.empty()) + { + return false; + } + + return SourceIdentifier == other.SourceIdentifier && PackageIdentifier == other.PackageIdentifier; + } + + std::string SourceIdentifier; + std::string PackageIdentifier; + }; + + // The data for a package in the map + struct InstalledResultFoldData { - data.AddIfNotPresent(SystemReferenceString{ PackageMatchField::UpgradeCode, Utility::LocIndString{ upgradeCode } }); - } - for (const auto& name : manifest.GetPackageNames()) + InstalledResultFoldData() = default; + explicit InstalledResultFoldData(size_t primaryPackageIndex) : PrimaryPackageIndex(primaryPackageIndex) {} + + std::optional<size_t> PrimaryPackageIndex; + std::vector<size_t> NonPrimaryPackageIndices; + }; + + std::unordered_map<InstalledResultFoldKey, InstalledResultFoldData, InstalledResultFoldKey> foldData; + + // Attempt to fold all primary package matches first. + // Packages without primaries will still be indexed into the hash table. + for (size_t i = 0; i < Matches.size(); ++i) { - for (const auto& publisher : manifest.GetPublishers()) + CompositeResultMatch& currentMatch = Matches[i]; + + // Check current match for fold target + if (currentMatch.Package->GetPrimaryAvailablePackage()) { - data.AddIfNotPresent(SystemReferenceString{ - PackageMatchField::NormalizedNameAndPublisher, - Utility::LocIndString{ name }, - Utility::LocIndString{ publisher } }); + InstalledResultFoldKey key{ currentMatch.Package->GetPrimaryAvailablePackage() }; + + auto itr = foldData.find(key); + if (itr != foldData.end()) + { + if (itr->second.PrimaryPackageIndex) + { + Matches[itr->second.PrimaryPackageIndex.value()].Package->FoldInstalledIn(currentMatch.Package); + currentMatch.Package.reset(); + } + else + { + itr->second.PrimaryPackageIndex = i; + } + } + else + { + foldData[key] = InstalledResultFoldData{ i }; + } + } + else + { + for (const auto& availablePackage : currentMatch.Package->GetAvailablePackages()) + { + InstalledResultFoldKey key{ availablePackage }; + + auto itr = foldData.find(key); + if (itr == foldData.end()) + { + itr = foldData.insert({ key, {} }).first; + } + + itr->second.NonPrimaryPackageIndices.emplace_back(i); + } } } - } - void GetSystemReferenceStrings( - IPackageVersion* installedVersion, - PackageVersionMultiProperty prop, - PackageMatchField field, - PackageData& data) - { - for (auto&& string : installedVersion->GetMultiProperty(prop)) + // After primary matches are folded, attempt to fold results without primary matches. + // The latest primary match will be preferred. + for (size_t i = 0; i < Matches.size(); ++i) { - data.AddIfNotPresent(SystemReferenceString{ field, std::move(string) }); - } - } + CompositeResultMatch& currentMatch = Matches[i]; - void GetNameAndPublisher( - IPackageVersion* installedVersion, - PackageData& data) - { - // Unfortunately the names and publishers are unique and not tied to each other strictly, so we need - // to go broad on the matches. Future work can hopefully make name and publisher operate more as a unit, - // but for now we have to search for the cartesian of these... - auto names = installedVersion->GetMultiProperty(PackageVersionMultiProperty::Name); - auto publishers = installedVersion->GetMultiProperty(PackageVersionMultiProperty::Publisher); + // Skip any matches that we have already folded + if (!currentMatch.Package) + { + continue; + } - for (size_t i = 0; i < names.size(); ++i) - { - for (size_t j = 0; j < publishers.size(); ++j) + if (!currentMatch.Package->GetPrimaryAvailablePackage()) { - data.AddIfNotPresent(SystemReferenceString{ - PackageMatchField::NormalizedNameAndPublisher, - names[i], - publishers[j] }); + InstalledResultFoldData* latestPrimaryAvailable = nullptr; + std::vector<InstalledResultFoldData*> availableFoldData; + + for (const auto& availablePackage : currentMatch.Package->GetAvailablePackages()) + { + auto& packageFoldData = foldData.at(availablePackage); + + if (packageFoldData.PrimaryPackageIndex) + { + if (!latestPrimaryAvailable || + Matches[latestPrimaryAvailable->PrimaryPackageIndex.value()].Package->GetTrackingPackageWriteTime() < Matches[packageFoldData.PrimaryPackageIndex.value()].Package->GetTrackingPackageWriteTime()) + { + latestPrimaryAvailable = &packageFoldData; + } + } + else + { + availableFoldData.emplace_back(&packageFoldData); + } + } + + if (latestPrimaryAvailable) + { + Matches[latestPrimaryAvailable->PrimaryPackageIndex.value()].Package->FoldInstalledIn(currentMatch.Package); + currentMatch.Package.reset(); + + // If the result with the primary is later, move it forward + if (latestPrimaryAvailable->PrimaryPackageIndex.value() > i) + { + currentMatch.Package = std::move(Matches[latestPrimaryAvailable->PrimaryPackageIndex.value()].Package); + Matches[latestPrimaryAvailable->PrimaryPackageIndex.value()].Package.reset(); + } + continue; + } + + // First, find the intersection of all results that contain all of the packages from this result. + std::vector<size_t> candidateMatches; + for (size_t j = 0; j < availableFoldData.size(); ++j) + { + InstalledResultFoldData* packageFoldData = availableFoldData[j]; + + if (j == 0) + { + candidateMatches = packageFoldData->NonPrimaryPackageIndices; + } + else + { + std::vector<size_t> temp; + std::set_intersection( + candidateMatches.begin(), candidateMatches.end(), + packageFoldData->NonPrimaryPackageIndices.begin(), packageFoldData->NonPrimaryPackageIndices.end(), + std::back_inserter(temp)); + candidateMatches = std::move(temp); + } + } + + // Now exclude both our own result and any that have a different (larger) number of available packages + candidateMatches.erase(std::remove_if(candidateMatches.begin(), candidateMatches.end(), + [&](size_t index) { return index == i || Matches[index].Package->GetAvailablePackages().size() != currentMatch.Package->GetAvailablePackages().size(); }), + candidateMatches.end()); + + // All of these remaining values should be folded in to our result + for (size_t foldTarget : candidateMatches) + { + currentMatch.Package->FoldInstalledIn(Matches[foldTarget].Package); + Matches[foldTarget].Package.reset(); + } } } + + // Get rid of the folded results; we reset the Package to indicate that it is no longer valid + Matches.erase(std::remove_if(Matches.begin(), Matches.end(), [&](const CompositeResultMatch& match) { return !match.Package; }), Matches.end()); } + + std::vector<CompositeResultMatch> Matches; + bool Truncated = false; + std::vector<SearchResult::Failure> Failures; }; std::shared_ptr<ICompositePackage> GetTrackedPackageFromAvailableSource(CompositeResult& result, const Source& source, const Utility::LocIndString& identifier) @@ -1030,17 +1468,11 @@ namespace AppInstaller::Repository } std::shared_ptr<CompositePackage> compositePackage = std::make_shared<CompositePackage>(match.Package); - std::shared_ptr<IPackageVersion> installedVersion; - auto installedPackage = compositePackage->GetInstalled(); - if (installedPackage) - { - installedVersion = installedPackage->GetLatestVersion(); - } - if (!installedVersion) + if (!installedPackage) { - // One would think that the installed version coming directly from our own installed source + // One would think that the installed package coming directly from our own installed source // would never be null, but it is sometimes. Rather than making users suffer through crashes // that break their entire experience, lets log a few things and then ignore this match. AICLI_LOG(Repo, Warning, << "CompositeSource: The installed version of the package '" << @@ -1048,98 +1480,86 @@ namespace AppInstaller::Repository continue; } - auto installedPackageData = result.GetSystemReferenceStrings(installedVersion.get()); + auto installedPackageData = result.GetSystemReferenceStrings(installedPackage.get()); // Create a search request to run against all available sources if (!installedPackageData.SystemReferenceStrings.empty()) { SearchRequest systemReferenceSearch = installedPackageData.CreateInclusionsSearchRequest(SearchPurpose::CorrelationToAvailable); - AICLI_LOG(Repo, Info, << "Finding available package from installed package using system reference search: " << systemReferenceSearch.ToString()); - - Source trackedSource; - std::shared_ptr<IPackage> trackingPackage; - std::shared_ptr<IPackageVersion> trackingPackageVersion; - std::chrono::system_clock::time_point trackingPackageTime; - bool foundAvailablePackageFromTracking = false; + AICLI_LOG(Repo, Verbose, << "Finding available package from installed package using system reference search: " << systemReferenceSearch.ToString()); - // Check the tracking catalog first to see if there is a correlation there. - // TODO: When the issue with support for multiple available packages is fixed, this should move into - // the below available sources loop as we will check all sources at that point. + // Search sources and add to result for (const auto& source : m_availableSources) { + AICLI_LOG(Repo, Verbose, << " ... searching source: " << source.GetDetails().Name << " [" << source.GetIdentifier() << ']'); + + // Find the tracking result with the latest timestamp. auto trackingCatalog = source.GetTrackingCatalog(); SearchResult trackingResult = trackingCatalog.Search(systemReferenceSearch); - std::shared_ptr<ICompositePackage> candidatePackage = GetMatchingPackage(trackingResult.Matches, - [&]() { - AICLI_LOG(Repo, Info, - << "Found multiple matches for installed package [" << installedVersion->GetProperty(PackageVersionProperty::Id) << - "] in tracking catalog for source [" << source.GetIdentifier() << "] when searching for [" << systemReferenceSearch.ToString() << "]"); - }, [&] { - AICLI_LOG(Repo, Warning, << " Appropriate tracking package could not be determined"); - }); + std::shared_ptr<IPackage> trackingPackage; + std::chrono::system_clock::time_point trackingPackageTime; + bool trackingSet = false; - // Determine the candidate package with the latest install time - if (candidatePackage) + for (const auto& trackingMatch : trackingResult.Matches) { - auto [candidateTime, candidateVersion] = GetLatestTrackingWriteTimeAndPackageVersion(candidatePackage); + auto candidateTime = GetLatestTrackingWriteTime(OnlyAvailable(trackingMatch.Package)); if (!trackingPackage || candidateTime > trackingPackageTime) { - trackedSource = source; - trackingPackage = OnlyAvailable(candidatePackage); - trackingPackageVersion = std::move(candidateVersion); + trackingPackage = OnlyAvailable(trackingMatch.Package); trackingPackageTime = candidateTime; } } - } - - // Directly search for the available package from tracking information. - if (trackingPackage) - { - auto availablePackage = GetTrackedPackageFromAvailableSource(result, trackedSource, trackingPackage->GetProperty(PackageProperty::Id)); - if (availablePackage) - { - compositePackage->AddAvailablePackage(std::move(availablePackage)); - foundAvailablePackageFromTracking = true; - } - compositePackage->SetTracking(std::move(trackedSource), std::move(trackingPackage), std::move(trackingPackageVersion)); - } - - // Search sources and add to result - for (const auto& source : m_availableSources) - { - // Do not attempt to correlate local packages against this source - if (!source.GetDetails().SupportInstalledSearchCorrelation) - { - continue; - } - // Skip the source that the tracking correlation result came from if we found one - if (foundAvailablePackageFromTracking && compositePackage->GetTrackingSource() == source) + if (trackingPackage && trackingPackageTime > compositePackage->GetTrackingPackageWriteTime()) { - continue; + AICLI_LOG(Repo, Verbose, << " ... setting latest tracking package to: " << trackingPackage->GetProperty(PackageProperty::Id)); + compositePackage->SetTracking(source, trackingPackage, trackingPackageTime); + trackingSet = true; } - SearchResult availableResult = result.SearchAndHandleFailures(source, systemReferenceSearch); - - if (availableResult.Matches.empty()) + // Attempt to correlate local packages against this source if supported. + SearchResult availableResult; + if (source.GetDetails().SupportInstalledSearchCorrelation) { - continue; + availableResult = result.SearchAndHandleFailures(source, systemReferenceSearch); } - // We will keep matching packages found from all sources, but generally we will use only the first one. auto availablePackage = GetMatchingPackage(availableResult.Matches, [&]() { AICLI_LOG(Repo, Info, - << "Found multiple matches for installed package [" << installedVersion->GetProperty(PackageVersionProperty::Id) << + << "Found multiple matches for installed package [" << installedPackage->GetProperty(PackageProperty::Id) << "] in source [" << source.GetIdentifier() << "] when searching for [" << systemReferenceSearch.ToString() << "]"); }, [&] { AICLI_LOG(Repo, Warning, << " Appropriate available package could not be determined"); }); - // For non pinning cases. We found some matching packages here, don't keep going. - compositePackage->AddAvailablePackage(availablePackage); + if (trackingPackage) + { + auto trackingIdentifier = trackingPackage->GetProperty(PackageProperty::Id); + + // We always want to take the available search result if it exists as the package may have been updated. + if (availablePackage) + { + auto availableIdentifier = availablePackage->GetProperty(PackageProperty::Id); + if (!Utility::ICUCaseInsensitiveEquals(availableIdentifier, trackingIdentifier)) + { + AICLI_LOG(Repo, Verbose, << " ... overriding tracking package (" << trackingIdentifier << ") with available package (" << availableIdentifier << ")"); + } + } + else + { + AICLI_LOG(Repo, Verbose, << " ... using tracking package: " << trackingIdentifier); + availablePackage = GetTrackedPackageFromAvailableSource(result, source, trackingIdentifier); + } + } + + if (availablePackage) + { + AICLI_LOG(Repo, Verbose, << " ... adding available package: " << availablePackage->GetProperty(PackageProperty::Id)); + compositePackage->AddAvailablePackage(availablePackage, trackingSet); + } } } @@ -1150,68 +1570,21 @@ namespace AppInstaller::Repository // Optimization for the "everything installed" case, no need to allow for reverse correlations if (request.IsForEverything() && m_searchBehavior == CompositeSearchBehavior::Installed) { - return std::move(result); + return result.ConvertToSearchResult(); } } // Search available sources for (const auto& source : m_availableSources) { - // Search the tracking catalog as it can potentially get better correlations auto trackingCatalog = source.GetTrackingCatalog(); - SearchResult trackingResult = trackingCatalog.Search(request); - - for (auto&& match : trackingResult.Matches) - { - // Check for a package already in the result that should have been correlated already. - auto packageData = result.CheckForExistingResultFromTrackingPackageMatch(match); - - // If found existing package in the result, continue - if (!packageData) - { - continue; - } - - // If no package was found that was already in the results, do a correlation lookup with the installed - // source to create a new composite package entry if we find any packages there. - if (packageData && !packageData->SystemReferenceStrings.empty()) - { - SearchRequest systemReferenceSearch = packageData->CreateInclusionsSearchRequest(SearchPurpose::CorrelationToInstalled); - - AICLI_LOG(Repo, Info, << "Finding installed package from tracking package using system reference search: " << systemReferenceSearch.ToString()); - // Correlate against installed (allow exceptions out as we own the installed source) - SearchResult installedCrossRef = m_installedSource.Search(systemReferenceSearch); - - auto installedPackage = GetMatchingPackage(installedCrossRef.Matches, - [&]() { - AICLI_LOG(Repo, Info, - << "Found multiple matches for tracking package [" << match.Package->GetProperty(PackageProperty::Id) << - "] in source [" << source.GetIdentifier() << "] when searching for [" << systemReferenceSearch.ToString() << "]"); - }, [&] { - AICLI_LOG(Repo, Warning, << " Appropriate installed package could not be determined"); - }); - - if (installedPackage && !result.ContainsInstalledPackage(installedPackage->GetInstalled().get())) - { - auto compositePackage = std::make_shared<CompositePackage>( - installedPackage, - GetTrackedPackageFromAvailableSource(result, source, match.Package->GetProperty(PackageProperty::Id))); - - auto [writeTime, trackingPackageVersion] = GetLatestTrackingWriteTimeAndPackageVersion(match.Package); - - compositePackage->SetTracking(source, OnlyAvailable(match.Package), std::move(trackingPackageVersion)); - - result.Matches.emplace_back(std::move(compositePackage), match.MatchCriteria); - } - } - } SearchResult availableResult = result.SearchAndHandleFailures(source, request); bool downloadManifests = source.QueryFeatureFlag(SourceFeatureFlag::ManifestMayContainAdditionalSystemReferenceStrings); for (auto&& match : availableResult.Matches) { - // Check for a package already in the result that should have been correlated already. + // Check for the package already in the result. // In cases that PackageData will be created, also download manifests for system reference strings // when search result is small (currently limiting to 1). auto packageData = result.CheckForExistingResultFromAvailablePackageMatch(match, downloadManifests && availableResult.Matches.size() == 1); @@ -1222,32 +1595,127 @@ namespace AppInstaller::Repository continue; } + // Use data from the tracking catalog as it can potentially get better correlations + auto trackingPackage = packageData->AddSystemReferenceStringsFromTrackingPackage(trackingCatalog, match.Package->GetProperty(PackageProperty::Id), source.GetDetails().Name); + // If no package was found that was already in the results, do a correlation lookup with the installed // source to create a new composite package entry if we find any packages there. bool foundInstalledMatch = false; - if (packageData && !packageData->SystemReferenceStrings.empty()) + if (!packageData->SystemReferenceStrings.empty()) { // Create a search request to run against the installed source SearchRequest systemReferenceSearch = packageData->CreateInclusionsSearchRequest(SearchPurpose::CorrelationToInstalled); - AICLI_LOG(Repo, Info, << "Finding installed package from available package using system reference search: " << systemReferenceSearch.ToString()); + AICLI_LOG(Repo, Verbose, << "Finding installed package from available package using system reference search: " << systemReferenceSearch.ToString()); // Correlate against installed (allow exceptions out as we own the installed source) SearchResult installedCrossRef = m_installedSource.Search(systemReferenceSearch); - auto installedPackage = GetMatchingPackage(installedCrossRef.Matches, - [&]() { - AICLI_LOG(Repo, Info, + if (ExperimentalFeature::IsEnabled(ExperimentalFeature::Feature::SideBySide)) + { + for (const auto& installedMatch : installedCrossRef.Matches) + { + if (!IsStrongMatchField(installedMatch.MatchCriteria.Field)) + { + // For weak correlations, do an installed -> available check to ensure that there are no other strong correlations. + SearchResult correlationConfirmation; + if (source.GetDetails().SupportInstalledSearchCorrelation) + { + correlationConfirmation = result.SearchAndHandleFailures(source, result.GetSystemReferenceStrings(installedMatch.Package->GetInstalled().get()).CreateInclusionsSearchRequest(SearchPurpose::CorrelationToAvailable)); + } + + if (correlationConfirmation.Matches.empty()) + { + // We probably made the correlation due to tracking data, keep it. + } + else if (correlationConfirmation.Matches.size() > 1) + { + // There is contention for the correlation. + AICLI_LOG(Repo, Verbose, << " ... installed package [" << installedMatch.Package->GetProperty(PackageProperty::Id) << + "] had multiple correlations and is being ignored as a match for [" << match.Package->GetProperty(PackageProperty::Id) << "]"); + continue; + } + else if (!OnlyAvailable(correlationConfirmation.Matches[0].Package)->IsSame(OnlyAvailable(match.Package).get())) + { + // The only correlation is not to the current package. + AICLI_LOG(Repo, Verbose, << " ... installed package [" << installedMatch.Package->GetProperty(PackageProperty::Id) << + "] was found through available package [" << match.Package->GetProperty(PackageProperty::Id) << "], but only correlated to [" << + correlationConfirmation.Matches[0].Package->GetProperty(PackageProperty::Id) << "] and is being ignored"); + continue; + } + } + + // Now that we know we need to add this available package, determine how exactly + std::shared_ptr<CompositePackage> resultPackage = result.FindInstalledPackage(installedMatch.Package->GetInstalled().get()); + + if (resultPackage) + { + // Check for a package from the same source already present on the result package. + bool foundSameSource = false; + + for (const auto& availablePackage : resultPackage->GetAvailablePackages()) + { + if (availablePackage->GetSource() == source) + { + // TODO: May need to add more data so that we can choose the proper correlation, but it may also be very difficult to get through + // the gauntlet of other checks and arrive in this situation. + AICLI_LOG(Repo, Verbose, << " ... found [" << availablePackage->GetProperty(PackageProperty::Id) << + "] already correlated to [" << installedMatch.Package->GetProperty(PackageProperty::Id) << "] from the same source [" << + source.GetDetails().Name << "] as [" << match.Package->GetProperty(PackageProperty::Id) << "]; ignoring the second correlation"); + foundSameSource = true; + } + } + + if (foundSameSource) + { + continue; + } + } + else + { + result.Matches.emplace_back(std::make_shared<CompositePackage>(installedMatch.Package), match.MatchCriteria); + resultPackage = result.Matches.back().Package; + } + + bool setPrimary = false; + if (trackingPackage) + { + auto trackingPackageTime = GetLatestTrackingWriteTime(trackingPackage); + + if (trackingPackageTime > resultPackage->GetTrackingPackageWriteTime()) + { + resultPackage->SetTracking(source, std::move(trackingPackage), trackingPackageTime); + setPrimary = true; + } + } + + resultPackage->AddAvailablePackage(std::move(match.Package), setPrimary); + + foundInstalledMatch = true; + } + } + else + { + auto installedPackage = GetMatchingPackage(installedCrossRef.Matches, + [&]() { + AICLI_LOG(Repo, Info, << "Found multiple matches for available package [" << match.Package->GetProperty(PackageProperty::Id) << "] in source [" << source.GetIdentifier() << "] when searching for [" << systemReferenceSearch.ToString() << "]"); - }, [&] { - AICLI_LOG(Repo, Warning, << " Appropriate installed package could not be determined"); - }); + }, [&] { + AICLI_LOG(Repo, Warning, << " Appropriate installed package could not be determined"); + }); - if (installedPackage && !result.ContainsInstalledPackage(installedPackage->GetInstalled().get())) - { - // TODO: Needs a whole separate change to fix the fact that we don't support multiple available packages and what the different search behaviors mean - foundInstalledMatch = true; - result.Matches.emplace_back(std::make_shared<CompositePackage>(installedPackage, std::move(match.Package)), match.MatchCriteria); + if (installedPackage && !result.ContainsInstalledPackage(installedPackage->GetInstalled().get())) + { + // TODO: Needs a whole separate change to fix the fact that we don't support multiple available packages and what the different search behaviors mean + foundInstalledMatch = true; + auto compositePackage = std::make_shared<CompositePackage>(installedPackage, std::move(match.Package)); + if (trackingPackage) + { + auto trackingPackageTime = GetLatestTrackingWriteTime(trackingPackage); + compositePackage->SetTracking(source, std::move(trackingPackage), trackingPackageTime); + } + result.Matches.emplace_back(std::move(compositePackage), match.MatchCriteria); + } } } @@ -1267,7 +1735,7 @@ namespace AppInstaller::Repository result.Matches.erase(result.Matches.begin() + request.MaximumResults, result.Matches.end()); } - return std::move(result); + return result.ConvertToSearchResult(); } // An available search goes through each source, searching individually and then sorting the full result set. diff --git a/src/AppInstallerRepositoryCore/Microsoft/ARPHelper.cpp b/src/AppInstallerRepositoryCore/Microsoft/ARPHelper.cpp @@ -403,7 +403,19 @@ namespace AppInstaller::Repository::Microsoft // `Publisher.DisplayName`. We would need to ensure that there are no matches // against the rest of the data however (might happen if same package is // installed for multiple architectures/languages). - manifest.Id = productCode; + if (Settings::ExperimentalFeature::IsEnabled(Settings::ExperimentalFeature::Feature::SideBySide)) + { + char separator = '\\'; + + std::ostringstream stream; + stream << "ARP" << separator << scope << separator << architecture << separator << productCode; + + manifest.Id = stream.str(); + } + else + { + manifest.Id = productCode; + } manifest.Installers.emplace_back(); // TODO: This likely needs some cleanup applied, as it looks like INNO tends to append an "_is#" @@ -491,7 +503,7 @@ namespace AppInstaller::Repository::Microsoft try { // Use the ProductCode as a unique key for the path - manifestIdOpt = index.AddManifest(manifest, Utility::ConvertToUTF16(manifest.Installers[0].ProductCode)); + manifestIdOpt = index.AddManifest(manifest); } catch (...) { diff --git a/src/AppInstallerRepositoryCore/Microsoft/PinningIndex.h b/src/AppInstallerRepositoryCore/Microsoft/PinningIndex.h @@ -29,8 +29,7 @@ namespace AppInstaller::Repository::Microsoft return { filePath, disposition, std::move(indexFile) }; } - // Opens or creates a PinningIndex database on the default path. - // openDisposition is only used when opening an existing database. + // Opens the PinningIndex database on the default path if it exists. // Returns nullptr in case of error. static std::shared_ptr<PinningIndex> OpenIfExists(OpenDisposition openDisposition = OpenDisposition::Read); diff --git a/src/AppInstallerRepositoryCore/Microsoft/PredefinedInstalledSourceFactory.cpp b/src/AppInstallerRepositoryCore/Microsoft/PredefinedInstalledSourceFactory.cpp @@ -9,6 +9,7 @@ #include <winget/Registry.h> #include <AppInstallerArchitecture.h> +#include <winget/ExperimentalFeature.h> using namespace std::string_literals; using namespace std::string_view_literals; @@ -100,9 +101,17 @@ namespace AppInstaller::Repository::Microsoft } auto packageId = package.Id(); + Utility::NormalizedString fullName = Utility::ConvertToUTF8(packageId.FullName()); Utility::NormalizedString familyName = Utility::ConvertToUTF8(packageId.FamilyName()); - manifest.Id = familyName; + if (Settings::ExperimentalFeature::IsEnabled(Settings::ExperimentalFeature::Feature::SideBySide)) + { + manifest.Id = "MSIX\\" + fullName; + } + else + { + manifest.Id = familyName; + } // Get version std::ostringstream strstr; @@ -142,11 +151,11 @@ namespace AppInstaller::Repository::Microsoft catch (const winrt::hresult_error& hre) { AICLI_LOG(Repo, Warning, << "winrt::hresult_error[0x" << Logging::SetHRFormat << hre.code() << ": " << - Utility::ConvertToUTF8(hre.message()) << "] exception thrown when getting DisplayName for " << familyName); + Utility::ConvertToUTF8(hre.message()) << "] exception thrown when getting DisplayName for " << fullName); } catch (...) { - AICLI_LOG(Repo, Warning, << "Unknown exception thrown when getting DisplayName for " << familyName); + AICLI_LOG(Repo, Warning, << "Unknown exception thrown when getting DisplayName for " << fullName); } } @@ -160,17 +169,28 @@ namespace AppInstaller::Repository::Microsoft try { // Use the full name as a unique key for the path - auto manifestId = index.AddManifest(manifest, std::filesystem::path{ packageId.FullName().c_str() }); + auto manifestId = index.AddManifest(manifest); index.SetMetadataByManifestId(manifestId, PackageVersionMetadata::InstalledType, Manifest::InstallerTypeToString(Manifest::InstallerTypeEnum::Msix)); + + auto architecture = Utility::ConvertToArchitectureEnum(packageId.Architecture()); + if (architecture) + { + index.SetMetadataByManifestId(manifestId, PackageVersionMetadata::InstalledArchitecture, + ToString(architecture.value())); + } } catch (const wil::ResultException& resultException) { if (HRESULT_FROM_WIN32(ERROR_ALREADY_EXISTS) == resultException.GetErrorCode() && package.IsFramework()) { - // There may be multiple packages with same package family name for framework packages. - continue; + // This should not be needed with the SxS changes + if (!Settings::ExperimentalFeature::IsEnabled(Settings::ExperimentalFeature::Feature::SideBySide)) + { + // There may be multiple packages with same package family name for framework packages. + continue; + } } throw; @@ -183,7 +203,7 @@ namespace AppInstaller::Repository::Microsoft AICLI_LOG(Repo, Verbose, << "Creating PredefinedInstalledSource with filter [" << PredefinedInstalledSourceFactory::FilterToString(filter) << ']'); // Create an in memory index - SQLiteIndex index = SQLiteIndex::CreateNew(SQLITE_MEMORY_DB_CONNECTION_TARGET, SQLite::Version::Latest()); + SQLiteIndex index = SQLiteIndex::CreateNew(SQLITE_MEMORY_DB_CONNECTION_TARGET, SQLite::Version::Latest(), SQLiteIndex::CreateOptions::SupportPathless); // Put installed packages into the index if (filter == PredefinedInstalledSourceFactory::Filter::None || filter == PredefinedInstalledSourceFactory::Filter::ARP || diff --git a/src/AppInstallerRepositoryCore/Microsoft/Schema/1_0/SearchResultsTable_1_0.cpp b/src/AppInstallerRepositoryCore/Microsoft/Schema/1_0/SearchResultsTable_1_0.cpp @@ -105,7 +105,7 @@ namespace AppInstaller::Repository::Microsoft::Schema::V1_0 SQLite::Statement statement = builder.Prepare(m_connection); BindStatementForMatchType(statement, filter, bindIndex); statement.Execute(); - AICLI_LOG(Repo, Verbose, << "Search found " << m_connection.GetChanges() << " rows"); + AICLI_LOG(SQL, Verbose, << "Search found " << m_connection.GetChanges() << " rows"); } void SearchResultsTable::RemoveDuplicateManifestRows() @@ -128,7 +128,7 @@ namespace AppInstaller::Repository::Microsoft::Schema::V1_0 EndParenthetical(); builder.Execute(m_connection); - AICLI_LOG(Repo, Verbose, << "Removed " << m_connection.GetChanges() << " duplicate rows"); + AICLI_LOG(SQL, Verbose, << "Removed " << m_connection.GetChanges() << " duplicate rows"); } void SearchResultsTable::PrepareToFilter() @@ -170,7 +170,7 @@ namespace AppInstaller::Repository::Microsoft::Schema::V1_0 SQLite::Statement statement = builder.Prepare(m_connection); BindStatementForMatchType(statement, filter, bindIndex); statement.Execute(); - AICLI_LOG(Repo, Verbose, << "Filter kept " << m_connection.GetChanges() << " rows"); + AICLI_LOG(SQL, Verbose, << "Filter kept " << m_connection.GetChanges() << " rows"); } void SearchResultsTable::CompleteFilter() @@ -180,7 +180,7 @@ namespace AppInstaller::Repository::Microsoft::Schema::V1_0 builder.DeleteFrom(GetQualifiedName()).Where(s_SearchResultsTable_Filter).Equals(false); builder.Execute(m_connection); - AICLI_LOG(Repo, Verbose, << "Filter deleted " << m_connection.GetChanges() << " rows"); + AICLI_LOG(SQL, Verbose, << "Filter deleted " << m_connection.GetChanges() << " rows"); } ISQLiteIndex::SearchResult SearchResultsTable::GetSearchResults(size_t limit) diff --git a/src/AppInstallerRepositoryCore/Public/winget/RepositorySearch.h b/src/AppInstallerRepositoryCore/Public/winget/RepositorySearch.h @@ -242,7 +242,7 @@ namespace AppInstaller::Repository { PackageVersionKey() = default; - PackageVersionKey(Utility::NormalizedString sourceId, Utility::NormalizedString version, Utility::NormalizedString channel) : + PackageVersionKey(std::string sourceId, Utility::NormalizedString version, Utility::NormalizedString channel) : SourceId(std::move(sourceId)), Version(std::move(version)), Channel(std::move(channel)) {} // The source id that this version came from. diff --git a/src/AppInstallerRepositoryCore/pch.h b/src/AppInstallerRepositoryCore/pch.h @@ -50,6 +50,7 @@ #include <thread> #include <tuple> #include <type_traits> +#include <unordered_map> #include <unordered_set> #include <utility> diff --git a/src/Microsoft.Management.Deployment/Converters.cpp b/src/Microsoft.Management.Deployment/Converters.cpp @@ -219,21 +219,7 @@ namespace winrt::Microsoft::Management::Deployment::implementation std::optional<::AppInstaller::Utility::Architecture> GetUtilityArchitecture(winrt::Windows::System::ProcessorArchitecture architecture) { - switch (architecture) - { - case winrt::Windows::System::ProcessorArchitecture::X86: - return ::AppInstaller::Utility::Architecture::X86; - case winrt::Windows::System::ProcessorArchitecture::Arm: - return ::AppInstaller::Utility::Architecture::Arm; - case winrt::Windows::System::ProcessorArchitecture::X64: - return ::AppInstaller::Utility::Architecture::X64; - case winrt::Windows::System::ProcessorArchitecture::Neutral: - return ::AppInstaller::Utility::Architecture::Neutral; - case winrt::Windows::System::ProcessorArchitecture::Arm64: - return ::AppInstaller::Utility::Architecture::Arm64; - } - - return {}; + return ::AppInstaller::Utility::ConvertToArchitectureEnum(architecture); } std::optional<winrt::Windows::System::ProcessorArchitecture> GetWindowsSystemProcessorArchitecture(::AppInstaller::Utility::Architecture architecture) diff --git a/tools/DevInSandbox/InSandboxScript.ps1 b/tools/DevInSandbox/InSandboxScript.ps1 @@ -0,0 +1,31 @@ +Param( + [String[]] $DesktopAppInstallerDependencyPath +) + +$ProgressPreference = 'SilentlyContinue' + +$desktopPath = "C:\Users\WDAGUtilityAccount\Desktop" + +Write-Host @" +--> Installing WinGet + +"@ + +foreach($dependency in $DesktopAppInstallerDependencyPath) +{ + Write-Host @" + ----> Installing $dependency +"@ + Add-AppxPackage -Path $dependency +} + +Write-Host @" + ----> Enabling dev mode +"@ +reg add "HKEY_LOCAL_MACHINE\SOFTWARE\Microsoft\Windows\CurrentVersion\AppModelUnlock" /t REG_DWORD /f /v "AllowDevelopmentWithoutDevLicense" /d "1" + +$devPackageManifestPath = Join-Path $desktopPath "DevPackage\AppxManifest.xml" +Write-Host @" + ----> Installing $devPackageManifestPath +"@ +Add-AppxPackage -Path $devPackageManifestPath -Register diff --git a/tools/DevInSandbox/Launch-DevPackageInSandbox.ps1 b/tools/DevInSandbox/Launch-DevPackageInSandbox.ps1 @@ -0,0 +1,216 @@ +Param( + [Parameter(HelpMessage = "The directory where local dev build is located; only the release build works.")] + [String] $DevPackagePath +) + +$ErrorActionPreference = "Stop" + +# Validate that the local dev manifest exists + +if (-not $DevPackagePath) +{ + $DevPackagePath = Join-Path $PSScriptRoot "..\..\src\AppInstallerCLIPackage\bin\x64\Release\AppX" +} + +$DevPackagePath = [System.IO.Path]::GetFullPath($DevPackagePath) + +if ($DevPackagePath.ToLower().Contains("debug")) +{ + Write-Error -Category InvalidArgument -Message @" +The Debug dev package does not work for unknown reasons. +Use the Release build or figure out how to make debug work and fix the scripts. +"@ +} + +if (-not (Test-Path (Join-Path $DevPackagePath "AppxManifest.xml"))) +{ + Write-Error -Category InvalidArgument -Message @" +AppxManifest.xml does not exist in the path $DevPackagePath +Either build the local dev package, or provide the location using -DevPackagePath +"@ +} + +# Check if Windows Sandbox is enabled + +if (-Not (Get-Command 'WindowsSandbox' -ErrorAction SilentlyContinue)) +{ + Write-Error -Category NotInstalled -Message @' +Windows Sandbox does not seem to be available. Check the following URL for prerequisites and further details: +https://docs.microsoft.com/windows/security/threat-protection/windows-sandbox/windows-sandbox-overview + +You can run the following command in an elevated PowerShell for enabling Windows Sandbox: +$ Enable-WindowsOptionalFeature -Online -FeatureName 'Containers-DisposableClientVM' +'@ +} + +# Close Windows Sandbox + +function Close-WindowsSandbox { + $sandbox = Get-Process 'WindowsSandboxClient' -ErrorAction SilentlyContinue + if ($sandbox) + { + Write-Host '--> Closing Windows Sandbox' + + $sandboxServer = Get-Process 'WindowsSandbox' -ErrorAction SilentlyContinue + + $sandbox | Stop-Process + $sandbox | Wait-Process -Timeout 120 + + # Also wait for the server to close + if ($sandboxServer) + { + try + { + $sandboxServer | Wait-Process -Timeout 120 + } + catch [System.TimeoutException] + { + # Force stop the server if it does not automatically stop + $sandboxServer | Stop-Process + $sandboxServer | Wait-Process -Timeout 120 + } + + } + + Write-Host + } + Remove-Variable sandbox +} + +Close-WindowsSandbox + +# Initialize Temp Folder + +$tempFolderName = 'DevInSandboxStaging' +$tempFolder = Join-Path -Path ([System.IO.Path]::GetTempPath()) -ChildPath $tempFolderName + +New-Item $tempFolder -ItemType Directory -ErrorAction SilentlyContinue | Out-Null + +# Set dependencies + +$desktopInSandbox = 'C:\Users\WDAGUtilityAccount\Desktop' + +[Net.ServicePointManager]::SecurityProtocol = [Net.SecurityProtocolType]::Tls12 +$WebClient = New-Object System.Net.WebClient + +# Hide the progress bar of Invoke-WebRequest +$oldProgressPreference = $ProgressPreference +$ProgressPreference = 'SilentlyContinue' + +$ProgressPreference = $oldProgressPreference + +$vcLibsUwp = @{ + fileName = 'Microsoft.VCLibs.x64.14.00.Desktop.appx' + url = 'https://aka.ms/Microsoft.VCLibs.x64.14.00.Desktop.appx' + hash = '9BFDE6CFCC530EF073AB4BC9C4817575F63BE1251DD75AAA58CB89299697A569' + folderInLocal = Join-Path ${env:ProgramFiles(x86)} "Microsoft SDKs\Windows Kits\10\ExtensionSDKs\Microsoft.VCLibs.Desktop\14.0\Appx\Retail\x64" +} + +$dependencies = @($vcLibsUwp) + +# Clean temp directory + +Get-ChildItem $tempFolder -Recurse -Exclude $dependencies.fileName | Remove-Item -Force -Recurse + +# Download dependencies + +Write-Host '--> Checking dependencies' + +foreach ($dependency in $dependencies) +{ + $dependency.pathInSandbox = Join-Path -Path $desktopInSandbox -ChildPath (Join-Path -Path $tempFolderName -ChildPath $dependency.fileName) + + # First see if the file exists locally to copy instead of downloading + if ($dependency.folderInLocal -ne $null) + { + $dependencyFilePath = Join-Path -Path $dependency.folderInLocal -ChildPath $dependency.fileName + if (Test-Path -Path $dependencyFilePath -PathType Leaf) + { + $dependencyFilePath + $tempFolder + Copy-Item -Path $dependencyFilePath -Destination $tempFolder -Force + continue + } + } + + # File does not exist locally, we need to download + + $dependency.file = Join-Path -Path $tempFolder -ChildPath $dependency.fileName + + # Only download if the file does not exist, or its hash does not match. + if (-Not ((Test-Path -Path $dependency.file -PathType Leaf) -And $dependency.hash -eq $(Get-FileHash $dependency.file).Hash)) + { + Write-Host @" + - Downloading: + $($dependency.url) +"@ + + try + { + $WebClient.DownloadFile($dependency.url, $dependency.file) + } + catch + { + #Pass the exception as an inner exception + throw [System.Net.WebException]::new("Error downloading $($dependency.url).",$_.Exception) + } + if (-not ($dependency.hash -eq $(Get-FileHash $dependency.file).Hash)) + { + throw [System.Activities.VersionMismatchException]::new('Dependency hash does not match the downloaded file') + } + } +} + +# Copy main script + +$mainPs1FileName = 'InSandboxScript.ps1' +Copy-Item (Join-Path $PSScriptRoot $mainPs1FileName) (Join-Path $tempFolder $mainPs1FileName) + +$dependenciesPathsInSandbox = "@('$($vcLibsUwp.pathInSandbox)')" +$bootstrapPs1Content = ".\$mainPs1FileName -DesktopAppInstallerDependencyPath @($dependenciesPathsInSandbox)" + +$bootstrapPs1FileName = 'Bootstrap.ps1' +$bootstrapPs1Content | Out-File (Join-Path $tempFolder $bootstrapPs1FileName) -Force + +# Create Windows Sandbox configuration file + +$bootstrapPs1InSandbox = Join-Path -Path $desktopInSandbox -ChildPath (Join-Path -Path $tempFolderName -ChildPath $bootstrapPs1FileName) +$tempFolderInSandbox = Join-Path -Path $desktopInSandbox -ChildPath $tempFolderName + +$devPackageInSandbox = Join-Path -Path $desktopInSandbox -ChildPath "DevPackage" +$devPackageXMLFragment = "" + +$devPackageXMLFragment = @" + <MappedFolder> + <HostFolder>$DevPackagePath</HostFolder> + <SandboxFolder>$devPackageInSandbox</SandboxFolder> + </MappedFolder> +"@ + +$sandboxTestWsbContent = @" +<Configuration> + <MappedFolders> + <MappedFolder> + <HostFolder>$tempFolder</HostFolder> + <ReadOnly>true</ReadOnly> + </MappedFolder> + $devPackageXMLFragment + </MappedFolders> + <LogonCommand> + <Command>PowerShell Start-Process PowerShell -WindowStyle Maximized -WorkingDirectory '$tempFolderInSandbox' -ArgumentList '-ExecutionPolicy Bypass -NoExit -NoLogo -File $bootstrapPs1InSandbox'</Command> + </LogonCommand> +</Configuration> +"@ + +$sandboxTestWsbFileName = 'SandboxTest.wsb' +$sandboxTestWsbFile = Join-Path -Path $tempFolder -ChildPath $sandboxTestWsbFileName +$sandboxTestWsbContent | Out-File $sandboxTestWsbFile -Force + +Write-Host @" +--> Starting Windows Sandbox + $sandboxTestWsbFile +"@ + +Write-Host + +WindowsSandbox $SandboxTestWsbFile