winget-cli

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

commit bb67e380f53e8150945f9043e560db2f44cdf4e4
parent 3a07a697fbc4f7af4298eeee7bf8565bafe42643
Author: fzanollo <fzanollo@users.noreply.github.com>
Date:   Tue, 26 Oct 2021 16:31:08 -0300

Package type Dependency graph (#1341)

* first tests for show depenencies feature

* show dependencies feature for show, install, need to change message

* showflow output changed

* install commands shows dependencies as in specs

* tests for informing dependencies on commands: show, install, upgrade, import

* refactor install flow

* change import command from install flow (install multiple)

* import shows all dependencies together

* show dependencies for validate and uninstall

* tests for validate and uninstall

* create show dependencies exp feature

* test for validate command

* put functionality under experimental feature check

* enable show dep experimental feature on unit test cases

* change experimental feature name

* remove info stream characters on testcase

* move check for exp feature inside report function, create new DependenciesFlow

* DependenciesFlow header and cpp inside Workflow

* –change representation of Dependency, create DependencyType and DependencyList; add Dependency to context data

* dependencies context data is of type DependencyList (not optional)

* fix spelling errors

* ApplyTo function, can receibe a lambda function to apply on specific DpeendencyType

* localize user strings, code style changes, validate report dependencies task divided, creates ValidateFlow

* change uninstall flow (gets dependencies from package version), make DependencyList.dependencies private, code style

* add missing report dep on upgrade

* Update src/AppInstallerCLIPackage/Shared/Strings/en-us/winget.resw

Co-authored-by: JohnMcPMS <johnmcp@microsoft.com>

* compare id with ICU case insensitive

* move dependencies related functions to DependenciesFlow, ReportDependencies is now a WorkflowTask receiving resource string id

* TODO to get dependencies on uninstall context, remove testcase for now, move dependency related function to DependenciesFlow

* wfIds not temporary

* DependencyList Add function checks for existence and updates min version if needed

* root dependencies are used when installer are not present, otherwise installer are preferred

* merge commits from master

* style and details

* fix merge conflicts

* spellcheck exception

* detail when adding dep

* proto

* divide identity report and installation disclaimer from actual installation

* proto tests

* testing

* questions

* test for dependency graph and fixes

* check loop logic change

* const & dep graph

* with installation order

* two more tests

* change map key to Dependency (missing min version check)

* Dependency implements operand<

* Dependency.MinVersion type is AppInstaller::Utility::Version

* choose installer using ManifestComparator

* getting composite source

* Drop const from the returned type of GetSource

* add check for min version

* spelling

* add dependencies source

* new parameter dependency source, open source can set up both: source and dependency source

* spelling

* DependencyGraph struct, manages only node/adjacent addition, loop check

* OpenSource() missing

* add adjacents to spellchecker

* graph logic is ok, installer selection and test source settings needs to be fixed

* dependency-source argument

* typo

* DependencyGraph receives function to search for dependencies, auto-builds the graph

* dependency table, create with id

* dependency graph logic moved to manifest common

* refactor

* break InstallMultiple workflow into two

* SelectInstallerFromMetadata does not receive context

* leaving workflow to work as in report dependencies, with the addition of managing package dep

* move dependencies to context data

* dependency graph + installation, all tests passed

* information detail, delete temp folder

* more testing for dependencies on install command

* valid min version test

* typo

* temp stash

* temp stash

* Fixed merge errors

* Merge branch 'master' of https://github.com/hackean-msft/winget-cli into user/akalagbe/floppy-dependency-graph

* More cleanup

* Removed prompt for failed dependencies

* Removed declarations for dependencies table

* Spell check fix

* Addressed pr comments.

* Addressed PR comments 2

* Code cleanup

* Fixed spell check error

* Refactored DependencyGraph

* Fixed typos

* Code refactor and typo fix

Co-authored-by: JohnMcPMS <johnmcp@microsoft.com>
Co-authored-by: Akinwale Alagbe <akalagbe@microsoft.com>
Co-authored-by: Akinwale Alagbe <24412729+hackean-msft@users.noreply.github.com>
Diffstat:
M.github/actions/spelling/expect.txt | 2++
Msrc/AppInstallerCLICore/AppInstallerCLICore.vcxproj | 2++
Msrc/AppInstallerCLICore/AppInstallerCLICore.vcxproj.filters | 8+++++++-
Msrc/AppInstallerCLICore/Argument.cpp | 2++
Msrc/AppInstallerCLICore/Commands/ExportCommand.cpp | 2+-
Msrc/AppInstallerCLICore/Commands/InstallCommand.cpp | 1+
Msrc/AppInstallerCLICore/Commands/ListCommand.cpp | 4++--
Msrc/AppInstallerCLICore/Commands/SearchCommand.cpp | 4++--
Msrc/AppInstallerCLICore/Commands/ShowCommand.cpp | 2+-
Msrc/AppInstallerCLICore/Commands/UninstallCommand.cpp | 4++--
Msrc/AppInstallerCLICore/Commands/UpgradeCommand.cpp | 8++++----
Msrc/AppInstallerCLICore/ExecutionArgs.h | 1+
Msrc/AppInstallerCLICore/ExecutionContextData.h | 7+++++++
Msrc/AppInstallerCLICore/Resources.h | 12++++++++++++
Msrc/AppInstallerCLICore/Workflows/CompletionFlow.cpp | 2+-
Msrc/AppInstallerCLICore/Workflows/DependenciesFlow.cpp | 214+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++------------------
Msrc/AppInstallerCLICore/Workflows/DependenciesFlow.h | 21+++++++++++++++++++++
Asrc/AppInstallerCLICore/Workflows/DependencyNodeProcessor.cpp | 98+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Asrc/AppInstallerCLICore/Workflows/DependencyNodeProcessor.h | 46++++++++++++++++++++++++++++++++++++++++++++++
Msrc/AppInstallerCLICore/Workflows/ImportExportFlow.cpp | 3++-
Msrc/AppInstallerCLICore/Workflows/InstallFlow.cpp | 21++++++++++++++-------
Msrc/AppInstallerCLICore/Workflows/InstallFlow.h | 18+++++++++++++++---
Msrc/AppInstallerCLICore/Workflows/ShowFlow.cpp | 5++++-
Msrc/AppInstallerCLICore/Workflows/WorkflowBase.cpp | 71+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++----------
Msrc/AppInstallerCLICore/Workflows/WorkflowBase.h | 16+++++++++++++---
Msrc/AppInstallerCLIPackage/Shared/Strings/en-us/winget.resw | 43+++++++++++++++++++++++++++++++++++++++++++
Msrc/AppInstallerCLITests/AppInstallerCLITests.vcxproj | 2++
Msrc/AppInstallerCLITests/AppInstallerCLITests.vcxproj.filters | 6++++++
Asrc/AppInstallerCLITests/Dependencies.cpp | 210+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Asrc/AppInstallerCLITests/DependenciesTestSource.h | 203+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Msrc/AppInstallerCLITests/TestData/Installer_Exe_Dependencies.yaml | 2+-
Msrc/AppInstallerCLITests/WorkFlow.cpp | 393+++++++++++++++++++++++++++++++++++++++++++++++++++++++++----------------------
Msrc/AppInstallerCommonCore/AppInstallerCommonCore.vcxproj | 2++
Msrc/AppInstallerCommonCore/AppInstallerCommonCore.vcxproj.filters | 6++++++
Asrc/AppInstallerCommonCore/DependenciesGraph.cpp | 135+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Msrc/AppInstallerCommonCore/Manifest/ManifestCommon.cpp | 106+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Msrc/AppInstallerCommonCore/Manifest/ManifestYamlPopulator.cpp | 2+-
Msrc/AppInstallerCommonCore/Public/AppInstallerErrors.h | 1+
Asrc/AppInstallerCommonCore/Public/winget/DependenciesGraph.h | 42++++++++++++++++++++++++++++++++++++++++++
Msrc/AppInstallerCommonCore/Public/winget/ManifestCommon.h | 142+++++++++++++++++++++----------------------------------------------------------
Msrc/AppInstallerRepositoryCore/AppInstallerRepositoryCore.vcxproj | 1+
Msrc/AppInstallerRepositoryCore/AppInstallerRepositoryCore.vcxproj.filters | 3+++
Msrc/AppInstallerRepositoryCore/Microsoft/SQLiteIndexSource.cpp | 22+++++++++++-----------
Msrc/AppInstallerRepositoryCore/Rest/RestSource.cpp | 16++++++++--------
44 files changed, 1591 insertions(+), 320 deletions(-)

diff --git a/.github/actions/spelling/expect.txt b/.github/actions/spelling/expect.txt @@ -1,4 +1,5 @@ abcd +adjacents activatable adml admx @@ -38,6 +39,7 @@ Baz bcp Beigi bfd +BFirst bght bitmask bkup diff --git a/src/AppInstallerCLICore/AppInstallerCLICore.vcxproj b/src/AppInstallerCLICore/AppInstallerCLICore.vcxproj @@ -275,6 +275,7 @@ <ClInclude Include="VTSupport.h" /> <ClInclude Include="PackageCollection.h" /> <ClInclude Include="Workflows\CompletionFlow.h" /> + <ClInclude Include="Workflows\DependencyNodeProcessor.h" /> <ClInclude Include="Workflows\DownloadFlow.h" /> <ClInclude Include="Workflows\ImportExportFlow.h" /> <ClInclude Include="Workflows\MsiInstallFlow.h" /> @@ -325,6 +326,7 @@ <ClCompile Include="Resources.cpp" /> <ClCompile Include="VTSupport.cpp" /> <ClCompile Include="Workflows\CompletionFlow.cpp" /> + <ClCompile Include="Workflows\DependencyNodeProcessor.cpp" /> <ClCompile Include="Workflows\DownloadFlow.cpp" /> <ClCompile Include="Workflows\ImportExportFlow.cpp" /> <ClCompile Include="Workflows\MsiInstallFlow.cpp" /> diff --git a/src/AppInstallerCLICore/AppInstallerCLICore.vcxproj.filters b/src/AppInstallerCLICore/AppInstallerCLICore.vcxproj.filters @@ -173,6 +173,9 @@ <ClInclude Include="Workflows\DownloadFlow.h"> <Filter>Workflows</Filter> </ClInclude> + <ClInclude Include="Workflows\DependencyNodeProcessor.h"> + <Filter>Workflows</Filter> + </ClInclude> </ItemGroup> <ItemGroup> <ClCompile Include="pch.cpp"> @@ -310,7 +313,10 @@ <ClCompile Include="Workflows\SettingsFlow.cpp"> <Filter>Workflows</Filter> </ClCompile> - <ClCompile Include="Workflows\DlownloadFlow.cpp"> + <ClCompile Include="Workflows\DownloadFlow.cpp"> + <Filter>Source Files</Filter> + </ClCompile> + <ClCompile Include="Workflows\DependencyNodeProcessor.cpp"> <Filter>Workflows</Filter> </ClCompile> </ItemGroup> diff --git a/src/AppInstallerCLICore/Argument.cpp b/src/AppInstallerCLICore/Argument.cpp @@ -30,6 +30,8 @@ namespace AppInstaller::CLI return Argument{ "command", NoAlias, Args::Type::Command, Resource::String::CommandArgumentDescription, ArgumentType::Standard, Argument::Visibility::Help }; case Args::Type::Source: return Argument{ "source", 's', Args::Type::Source, Resource::String::SourceArgumentDescription, ArgumentType::Standard }; + case Args::Type::DependencySource: + return Argument{ "dependency-source", NoAlias, Args::Type::DependencySource, Resource::String::DependencySourceArgumentDescription, ArgumentType::Standard }; case Args::Type::Count: return Argument{ "count", 'n', Args::Type::Count, Resource::String::CountArgumentDescription, ArgumentType::Standard }; case Args::Type::Exact: diff --git a/src/AppInstallerCLICore/Commands/ExportCommand.cpp b/src/AppInstallerCLICore/Commands/ExportCommand.cpp @@ -57,7 +57,7 @@ namespace AppInstaller::CLI context << Workflow::ReportExecutionStage(Workflow::ExecutionStage::Discovery) << - Workflow::OpenSource << + Workflow::OpenSource() << Workflow::OpenCompositeSource(Repository::PredefinedSource::Installed) << Workflow::SearchSourceForMany << Workflow::HandleSearchResultFailures << diff --git a/src/AppInstallerCLICore/Commands/InstallCommand.cpp b/src/AppInstallerCLICore/Commands/InstallCommand.cpp @@ -39,6 +39,7 @@ namespace AppInstaller::CLI Argument::ForType(Args::Type::Override), Argument::ForType(Args::Type::InstallLocation), Argument::ForType(Args::Type::HashOverride), + Argument::ForType(Args::Type::DependencySource), Argument::ForType(Args::Type::AcceptPackageAgreements), Argument::ForType(Args::Type::CustomHeader), Argument::ForType(Args::Type::AcceptSourceAgreements), diff --git a/src/AppInstallerCLICore/Commands/ListCommand.cpp b/src/AppInstallerCLICore/Commands/ListCommand.cpp @@ -40,7 +40,7 @@ namespace AppInstaller::CLI void ListCommand::Complete(Execution::Context& context, Execution::Args::Type valueType) const { context << - Workflow::OpenSource << + Workflow::OpenSource() << Workflow::OpenCompositeSource(Repository::PredefinedSource::Installed); switch (valueType) @@ -73,7 +73,7 @@ namespace AppInstaller::CLI context.SetFlags(Execution::ContextFlag::TreatSourceFailuresAsWarning); context << - Workflow::OpenSource << + Workflow::OpenSource() << Workflow::OpenCompositeSource(Repository::PredefinedSource::Installed) << Workflow::SearchSourceForMany << Workflow::HandleSearchResultFailures << diff --git a/src/AppInstallerCLICore/Commands/SearchCommand.cpp b/src/AppInstallerCLICore/Commands/SearchCommand.cpp @@ -44,7 +44,7 @@ namespace AppInstaller::CLI { case Execution::Args::Type::Query: context << - Workflow::OpenSource << + Workflow::OpenSource() << Workflow::RequireCompletionWordNonEmpty << Workflow::SearchSourceForManyCompletion << Workflow::CompleteWithMatchedField; @@ -71,7 +71,7 @@ namespace AppInstaller::CLI context.SetFlags(Execution::ContextFlag::TreatSourceFailuresAsWarning); context << - Workflow::OpenSource << + Workflow::OpenSource() << Workflow::SearchSourceForMany << Workflow::HandleSearchResultFailures << Workflow::EnsureMatchesFromSearchResult(false) << diff --git a/src/AppInstallerCLICore/Commands/ShowCommand.cpp b/src/AppInstallerCLICore/Commands/ShowCommand.cpp @@ -65,7 +65,7 @@ namespace AppInstaller::CLI else { context << - Workflow::OpenSource << + Workflow::OpenSource() << Workflow::SearchSourceForSingle << Workflow::HandleSearchResultFailures << Workflow::EnsureOneMatchFromSearchResult(false) << diff --git a/src/AppInstallerCLICore/Commands/UninstallCommand.cpp b/src/AppInstallerCLICore/Commands/UninstallCommand.cpp @@ -55,7 +55,7 @@ namespace AppInstaller::CLI } context << - Workflow::OpenSource << + Workflow::OpenSource() << Workflow::OpenCompositeSource(Repository::PredefinedSource::Installed); switch (valueType) @@ -106,7 +106,7 @@ namespace AppInstaller::CLI // open the sources where to search for the package context << Workflow::ReportExecutionStage(ExecutionStage::Discovery) << - Workflow::OpenSource << + Workflow::OpenSource() << Workflow::OpenCompositeSource(Repository::PredefinedSource::Installed); // find the uninstaller diff --git a/src/AppInstallerCLICore/Commands/UpgradeCommand.cpp b/src/AppInstallerCLICore/Commands/UpgradeCommand.cpp @@ -71,7 +71,7 @@ namespace AppInstaller::CLI } context << - OpenSource << + OpenSource() << OpenCompositeSource(Repository::PredefinedSource::Installed); switch (valueType) @@ -128,9 +128,9 @@ namespace AppInstaller::CLI } context << - ReportExecutionStage(ExecutionStage::Discovery) << - OpenSource << - OpenCompositeSource(Repository::PredefinedSource::Installed); + Workflow::ReportExecutionStage(ExecutionStage::Discovery) << + Workflow::OpenSource() << + Workflow::OpenCompositeSource(Repository::PredefinedSource::Installed); if (ShouldListUpgrade(context)) { diff --git a/src/AppInstallerCLICore/ExecutionArgs.h b/src/AppInstallerCLICore/ExecutionArgs.h @@ -81,6 +81,7 @@ namespace AppInstaller::CLI::Execution Help, // Show command usage Info, // Show general info about WinGet VerboseLogs, // Increases winget logging level to verbose + DependencySource, // Index source to be queried against for finding dependencies CustomHeader, // Optional Rest source header AcceptSourceAgreements, // Accept all source agreements diff --git a/src/AppInstallerCLICore/ExecutionContextData.h b/src/AppInstallerCLICore/ExecutionContextData.h @@ -49,6 +49,7 @@ namespace AppInstaller::CLI::Execution Sources, ARPSnapshot, Dependencies, + DependencySource, AllowedArchitectures, Max }; @@ -226,6 +227,12 @@ namespace AppInstaller::CLI::Execution }; template <> + struct DataMapping<Data::DependencySource> + { + using value_t = std::shared_ptr<Repository::ISource>; + }; + + template <> struct DataMapping<Data::AllowedArchitectures> { using value_t = std::vector<Utility::Architecture>; diff --git a/src/AppInstallerCLICore/Resources.h b/src/AppInstallerCLICore/Resources.h @@ -43,6 +43,17 @@ namespace AppInstaller::CLI::Resource WINGET_DEFINE_RESOURCE_STRINGID(CompleteCommandLongDescription); WINGET_DEFINE_RESOURCE_STRINGID(CompleteCommandShortDescription); WINGET_DEFINE_RESOURCE_STRINGID(CountArgumentDescription); + WINGET_DEFINE_RESOURCE_STRINGID(DependenciesFlowInstall); + WINGET_DEFINE_RESOURCE_STRINGID(DependenciesFlowSourceNotFound); + WINGET_DEFINE_RESOURCE_STRINGID(DependenciesFlowSourceTooManyMatches); + WINGET_DEFINE_RESOURCE_STRINGID(DependenciesFlowPackageVersionNotFound); + WINGET_DEFINE_RESOURCE_STRINGID(DependenciesFlowNoInstallerFound); + WINGET_DEFINE_RESOURCE_STRINGID(DependenciesFlowNoMinVersion); + WINGET_DEFINE_RESOURCE_STRINGID(DependenciesFlowNoSuitableInstallerFound); + WINGET_DEFINE_RESOURCE_STRINGID(DependenciesFlowNoMatches); + WINGET_DEFINE_RESOURCE_STRINGID(DependenciesFlowContainsLoop); + WINGET_DEFINE_RESOURCE_STRINGID(DependenciesManagementError); + WINGET_DEFINE_RESOURCE_STRINGID(DependenciesManagementExitMessage); WINGET_DEFINE_RESOURCE_STRINGID(CountOutOfBoundsError); WINGET_DEFINE_RESOURCE_STRINGID(DisabledByGroupPolicy); WINGET_DEFINE_RESOURCE_STRINGID(Done); @@ -265,6 +276,7 @@ namespace AppInstaller::CLI::Resource WINGET_DEFINE_RESOURCE_STRINGID(SourceAgreementsTitle); WINGET_DEFINE_RESOURCE_STRINGID(SourceArgArgumentDescription); WINGET_DEFINE_RESOURCE_STRINGID(SourceArgumentDescription); + WINGET_DEFINE_RESOURCE_STRINGID(DependencySourceArgumentDescription); WINGET_DEFINE_RESOURCE_STRINGID(SourceCommandLongDescription); WINGET_DEFINE_RESOURCE_STRINGID(SourceCommandShortDescription); WINGET_DEFINE_RESOURCE_STRINGID(SourceExportCommandLongDescription); diff --git a/src/AppInstallerCLICore/Workflows/CompletionFlow.cpp b/src/AppInstallerCLICore/Workflows/CompletionFlow.cpp @@ -114,7 +114,7 @@ namespace AppInstaller::CLI::Workflow case Execution::Args::Type::Version: case Execution::Args::Type::Channel: context << - Workflow::OpenSource; + Workflow::OpenSource(); break; } diff --git a/src/AppInstallerCLICore/Workflows/DependenciesFlow.cpp b/src/AppInstallerCLICore/Workflows/DependenciesFlow.cpp @@ -1,52 +1,62 @@ -// Copyright (c) Microsoft Corporation. -// Licensed under the MIT License. - -#include "pch.h" -#include "DependenciesFlow.h" - -namespace AppInstaller::CLI::Workflow +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +#include "pch.h" +#include "DependenciesFlow.h" +#include "ManifestComparator.h" +#include "InstallFlow.h" +#include "winget\DependenciesGraph.h" +#include "DependencyNodeProcessor.h" + +using namespace AppInstaller::Repository; +using namespace AppInstaller::Manifest; + +namespace AppInstaller::CLI::Workflow { void ReportDependencies::operator()(Execution::Context& context) const { - if (!Settings::ExperimentalFeature::IsEnabled(Settings::ExperimentalFeature::Feature::Dependencies)) - { - return; - } - auto info = context.Reporter.Info(); - - const auto& dependencies = context.Get<Execution::Data::Dependencies>(); - if (dependencies.HasAny()) - { - info << Resource::StringId(m_messageId) << std::endl; - - if (dependencies.HasAnyOf(Manifest::DependencyType::WindowsFeature)) - { - info << " - " << Resource::String::WindowsFeaturesDependencies << std::endl; - dependencies.ApplyToType(Manifest::DependencyType::WindowsFeature, [&info](Manifest::Dependency dependency) {info << " " << dependency.Id << std::endl; }); - } - - if (dependencies.HasAnyOf(Manifest::DependencyType::WindowsLibrary)) - { - info << " - " << Resource::String::WindowsLibrariesDependencies << std::endl; - dependencies.ApplyToType(Manifest::DependencyType::WindowsLibrary, [&info](Manifest::Dependency dependency) {info << " " << dependency.Id << std::endl; }); - } - - if (dependencies.HasAnyOf(Manifest::DependencyType::Package)) - { - info << " - " << Resource::String::PackageDependencies << std::endl; - dependencies.ApplyToType(Manifest::DependencyType::Package, [&info](Manifest::Dependency dependency) - { - info << " " << dependency.Id; - if (dependency.MinVersion) info << " [>= " << dependency.MinVersion.value() << "]"; - info << std::endl; - }); - } - - if (dependencies.HasAnyOf(Manifest::DependencyType::External)) - { - info << " - " << Resource::String::ExternalDependencies << std::endl; - dependencies.ApplyToType(Manifest::DependencyType::External, [&info](Manifest::Dependency dependency) {info << " " << dependency.Id << std::endl; }); - } + if (!Settings::ExperimentalFeature::IsEnabled(Settings::ExperimentalFeature::Feature::Dependencies)) + { + return; + } + auto info = context.Reporter.Info(); + + const auto& dependencies = context.Get<Execution::Data::Dependencies>(); + if (dependencies.HasAny()) + { + info << m_messageId << std::endl; + + if (dependencies.HasAnyOf(DependencyType::WindowsFeature)) + { + info << " - " << Resource::String::WindowsFeaturesDependencies << std::endl; + dependencies.ApplyToType(DependencyType::WindowsFeature, [&info](Dependency dependency) {info << " " << dependency.Id << std::endl; }); + } + + if (dependencies.HasAnyOf(DependencyType::WindowsLibrary)) + { + info << " - " << Resource::String::WindowsLibrariesDependencies << std::endl; + dependencies.ApplyToType(DependencyType::WindowsLibrary, [&info](Dependency dependency) {info << " " << dependency.Id << std::endl; }); + } + + if (dependencies.HasAnyOf(DependencyType::Package)) + { + info << " - " << Resource::String::PackageDependencies << std::endl; + dependencies.ApplyToType(DependencyType::Package, [&info](Dependency dependency) + { + info << " " << dependency.Id; + if (dependency.MinVersion) + { + info << " [>= " << dependency.MinVersion.value().ToString() << "]"; + } + info << std::endl; + }); + } + + if (dependencies.HasAnyOf(DependencyType::External)) + { + context.Reporter.Warn() << " - " << Resource::String::ExternalDependencies << std::endl; + dependencies.ApplyToType(DependencyType::External, [&info](Dependency dependency) {info << " " << dependency.Id << std::endl; }); + } } } @@ -54,14 +64,14 @@ namespace AppInstaller::CLI::Workflow if (Settings::ExperimentalFeature::IsEnabled(Settings::ExperimentalFeature::Feature::Dependencies)) { const auto& manifest = context.Get<Execution::Data::Manifest>(); - Manifest::DependencyList allDependencies; + DependencyList allDependencies; for (const auto& installer : manifest.Installers) { allDependencies.Add(installer.Dependencies); } - context.Add<Execution::Data::Dependencies>(allDependencies); + context.Add<Execution::Data::Dependencies>(std::move(allDependencies)); } } @@ -82,7 +92,113 @@ namespace AppInstaller::CLI::Workflow if (Settings::ExperimentalFeature::IsEnabled(Settings::ExperimentalFeature::Feature::Dependencies)) { // TODO make best effort to get the correct installer information, it may be better to have a record of installations and save the correct installers - context.Add<Execution::Data::Dependencies>(Manifest::DependencyList()); // sending empty list of dependencies for now + context.Add<Execution::Data::Dependencies>(DependencyList()); // sending empty list of dependencies for now + } + } + + void OpenDependencySource(Execution::Context& context) + { + if (context.Contains(Execution::Data::PackageVersion)) + { + const auto& packageVersion = context.Get<Execution::Data::PackageVersion>(); + context.Add<Execution::Data::DependencySource>(std::const_pointer_cast<Repository::ISource>(packageVersion->GetSource())); + context << + Workflow::OpenCompositeSource(Repository::PredefinedSource::Installed, true); + } + else + { // install from manifest requires --dependency-source to be set + context << + Workflow::OpenSource(true) << + Workflow::OpenCompositeSource(Repository::PredefinedSource::Installed, true); + } + } + + + void ManagePackageDependencies::operator()(Execution::Context& context) const + { + if (!Settings::ExperimentalFeature::IsEnabled(Settings::ExperimentalFeature::Feature::Dependencies)) + { + return; + } + + auto info = context.Reporter.Info(); + auto error = context.Reporter.Error(); + const auto& rootManifest = context.Get<Execution::Data::Manifest>(); + + Dependency rootAsDependency = Dependency(DependencyType::Package, rootManifest.Id, rootManifest.Version); + + const auto& rootInstaller = context.Get<Execution::Data::Installer>(); + const auto& rootDependencies = rootInstaller->Dependencies; + + if (rootDependencies.Empty()) + { + // If there's no dependencies there's nothing to do aside of logging the outcome + return; + } + + info << Resource::String::DependenciesFlowInstall << std::endl; + + context << OpenDependencySource; + if (context.IsTerminated()) + { + info << Resource::String::DependenciesFlowSourceNotFound << std::endl; + AICLI_TERMINATE_CONTEXT(APPINSTALLER_CLI_ERROR_INTERNAL_ERROR); + } + + + std::map<string_t, Execution::PackageToInstall> idToPackageMap; + bool foundError = false; + DependencyGraph dependencyGraph(rootAsDependency, rootDependencies, + [&](Dependency node) { + DependencyNodeProcessor nodeProcessor(context); + + auto result = nodeProcessor.EvaluateDependencies(node); + DependencyList list = nodeProcessor.GetDependencyList(); + foundError = foundError || (result == DependencyNodeProcessorResult::Error); + + if (result == DependencyNodeProcessorResult::Success) + { + Execution::PackageToInstall packageToInstall{ + std::move(nodeProcessor.GetPackageLatestVersion()), + std::move(nodeProcessor.GetPackageInstalledVersion()), + std::move(nodeProcessor.GetManifest()), + std::move(nodeProcessor.GetPreferredInstaller()) }; + idToPackageMap.emplace(node.Id, std::move(packageToInstall)); + }; + + return list; + }); + + dependencyGraph.BuildGraph(); + + if (foundError) + { + error << Resource::String::DependenciesManagementExitMessage << std::endl; + AICLI_TERMINATE_CONTEXT(APPINSTALLER_CLI_ERROR_INSTALL_MISSING_DEPENDENCY); + } + + if (dependencyGraph.HasLoop()) + { + context.Reporter.Warn() << Resource::String::DependenciesFlowContainsLoop; + } + + const auto& installationOrder = dependencyGraph.GetInstallationOrder(); + + std::vector<Execution::PackageToInstall> installers; + + for (auto const& node : installationOrder) + { + auto itr = idToPackageMap.find(node.Id); + // if the package was already installed (with a useful version) or is the root + // then there will be no installer for it on the map. + if (itr != idToPackageMap.end()) + { + installers.push_back(std::move(itr->second)); + } } + + // Install dependencies in the correct order + context.Add<Execution::Data::PackagesToInstall>(installers); + context << Workflow::InstallMultiplePackages(m_dependencyReportMessage, APPINSTALLER_CLI_ERROR_INSTALL_DEPENDENCIES, {}, false, true); } } \ No newline at end of file diff --git a/src/AppInstallerCLICore/Workflows/DependenciesFlow.h b/src/AppInstallerCLICore/Workflows/DependenciesFlow.h @@ -38,4 +38,25 @@ namespace AppInstaller::CLI::Workflow // Inputs: None // Outputs: Dependencies void GetDependenciesInfoForUninstall(Execution::Context& context); + + // Builds the dependency graph. + // Required Args: None + // Inputs: Manifest, Installer and DependencySource + // Outputs: Dependencies + struct ManagePackageDependencies : public WorkflowTask + { + ManagePackageDependencies(AppInstaller::StringResource::StringId dependencyReportMessage) : + WorkflowTask("ReportDependencies"), m_dependencyReportMessage(dependencyReportMessage) {} + + void operator()(Execution::Context& context) const override; + + private: + AppInstaller::StringResource::StringId m_dependencyReportMessage; + }; + + // Sets up the source used to get the dependencies. + // Required Args: None + // Inputs: PackageVersion, Manifest + // Outputs: DependencySource + void OpenDependencySource(Execution::Context& context); } \ No newline at end of file diff --git a/src/AppInstallerCLICore/Workflows/DependencyNodeProcessor.cpp b/src/AppInstallerCLICore/Workflows/DependencyNodeProcessor.cpp @@ -0,0 +1,97 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. +#include "pch.h" +#include "DependencyNodeProcessor.h" +#include "ManifestComparator.h" + +using namespace AppInstaller::Manifest; +using namespace AppInstaller::Repository; + +namespace AppInstaller::CLI::Workflow +{ + DependencyNodeProcessor::DependencyNodeProcessor(Execution::Context& context) + : m_context(context) {} + + DependencyNodeProcessorResult DependencyNodeProcessor::EvaluateDependencies(Dependency& dependencyNode) + { + SearchRequest searchRequest; + const auto& source = m_context.Get<Execution::Data::DependencySource>(); + auto error = m_context.Reporter.Error(); + auto info = m_context.Reporter.Info(); + + searchRequest.Filters.emplace_back(PackageMatchFilter(PackageMatchField::Id, MatchType::CaseInsensitive, dependencyNode.Id)); + + const auto& matches = source->Search(searchRequest).Matches; + + if (matches.empty()) + { + error << Resource::String::DependenciesFlowNoMatches; + return DependencyNodeProcessorResult::Error; + } + + if (matches.size() > 1) + { + error << Resource::String::DependenciesFlowSourceTooManyMatches << " " << Utility::Normalize(dependencyNode.Id); + AICLI_LOG(CLI, Error, << "Too many matches for package " << dependencyNode.Id); + return DependencyNodeProcessorResult::Error; + } + + const auto& match = matches.at(0); + const auto& package = match.Package; + auto packageId = package->GetProperty(PackageProperty::Id); + m_nodePackageInstalledVersion = package->GetInstalledVersion(); + m_nodePackageLatestVersion = package->GetLatestAvailableVersion(); + + if (m_nodePackageInstalledVersion && dependencyNode.IsVersionOk(Utility::Version(m_nodePackageInstalledVersion->GetProperty(PackageVersionProperty::Version)))) + { + // return empty dependency list, + // as we won't keep searching for dependencies for installed packages + return DependencyNodeProcessorResult::Skipped; + } + + + if (!m_nodePackageLatestVersion) + { + error << Resource::String::DependenciesFlowPackageVersionNotFound << " " << Utility::Normalize(packageId); + AICLI_LOG(CLI, Error, << "Latest available version not found for package " << packageId); + return DependencyNodeProcessorResult::Error; + } + + if (!dependencyNode.IsVersionOk(Utility::Version(m_nodePackageLatestVersion->GetProperty(PackageVersionProperty::Version)))) + { + error << Resource::String::DependenciesFlowNoMinVersion << " " << Utility::Normalize(packageId); + AICLI_LOG(CLI, Error, << "No suitable min version found for package " << packageId); + return DependencyNodeProcessorResult::Error; + } + + m_nodeManifest = m_nodePackageLatestVersion->GetManifest(); + if (m_nodeManifest.Installers.empty()) + { + error << Resource::String::DependenciesFlowNoInstallerFound << " " << Utility::Normalize(m_nodeManifest.Id); + AICLI_LOG(CLI, Error, << "Installer not found for manifest " << m_nodeManifest.Id << " with version" << m_nodeManifest.Version); + return DependencyNodeProcessorResult::Error; + } + + std::optional<AppInstaller::Manifest::ManifestInstaller> installer; + + IPackageVersion::Metadata installationMetadata; + if (m_nodePackageInstalledVersion) + { + installationMetadata = m_nodePackageInstalledVersion->GetMetadata(); + } + + ManifestComparator manifestComparator(m_context, installationMetadata); + installer = manifestComparator.GetPreferredInstaller(m_nodeManifest); + + if (!installer.has_value()) + { + error << Resource::String::DependenciesFlowNoSuitableInstallerFound << " " << Utility::Normalize(m_nodeManifest.Id) << m_nodeManifest.Version; + AICLI_LOG(CLI, Error, << "No suitable installer found for manifest " << m_nodeManifest.Id << " with version " << m_nodeManifest.Version); + return DependencyNodeProcessorResult::Error; + } + + m_installer = installer.value(); + m_dependenciesList = m_installer.Dependencies; + return DependencyNodeProcessorResult::Success; + } +}+ \ No newline at end of file diff --git a/src/AppInstallerCLICore/Workflows/DependencyNodeProcessor.h b/src/AppInstallerCLICore/Workflows/DependencyNodeProcessor.h @@ -0,0 +1,45 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. +#pragma once +#include "pch.h" +#include <AppInstallerRepositorySearch.h> +#include "ExecutionContext.h" +#include "winget/ManifestCommon.h" + +using namespace AppInstaller::Manifest; +using namespace AppInstaller::Repository; + +namespace AppInstaller::CLI::Workflow +{ + enum DependencyNodeProcessorResult + { + Error, + Success, + Skipped, + }; + + struct DependencyNodeProcessor + { + DependencyNodeProcessor(Execution::Context& context); + + DependencyNodeProcessorResult EvaluateDependencies(Dependency& dependencyNode); + + DependencyList GetDependencyList() { return m_dependenciesList; } + + std::shared_ptr<IPackageVersion> GetPackageLatestVersion() { return m_nodePackageLatestVersion; } + + std::shared_ptr<IPackageVersion> GetPackageInstalledVersion() { return m_nodePackageInstalledVersion; } + + Manifest::Manifest GetManifest() { return m_nodeManifest; } + + Manifest::ManifestInstaller GetPreferredInstaller() { return m_installer; } + + private: + Execution::Context& m_context; + DependencyList m_dependenciesList; + std::shared_ptr<IPackageVersion> m_nodePackageLatestVersion; + std::shared_ptr<IPackageVersion> m_nodePackageInstalledVersion; + Manifest::ManifestInstaller m_installer; + Manifest::Manifest m_nodeManifest; + }; +}+ \ No newline at end of file diff --git a/src/AppInstallerCLICore/Workflows/ImportExportFlow.cpp b/src/AppInstallerCLICore/Workflows/ImportExportFlow.cpp @@ -348,7 +348,8 @@ namespace AppInstaller::CLI::Workflow void InstallImportedPackages(Execution::Context& context) { - context << Workflow::InstallMultiplePackages(Resource::String::ImportCommandReportDependencies, APPINSTALLER_CLI_ERROR_IMPORT_INSTALL_FAILED); + context << Workflow::InstallMultiplePackages( + Resource::String::ImportCommandReportDependencies, APPINSTALLER_CLI_ERROR_IMPORT_INSTALL_FAILED, {}, true, true); if (context.GetTerminationHR() == APPINSTALLER_CLI_ERROR_IMPORT_INSTALL_FAILED) { diff --git a/src/AppInstallerCLICore/Workflows/InstallFlow.cpp b/src/AppInstallerCLICore/Workflows/InstallFlow.cpp @@ -12,7 +12,6 @@ #include "WorkflowBase.h" #include "Workflows/DependenciesFlow.h" #include <winget/PackageTrackingCatalog.h> - #include <AppInstallerDeployment.h> using namespace winrt::Windows::ApplicationModel::Store::Preview::InstallControl; @@ -385,6 +384,7 @@ namespace AppInstaller::CLI::Workflow Workflow::ShowPackageAgreements(/* ensureAcceptance */ true) << Workflow::GetDependenciesFromInstaller << Workflow::ReportDependencies(Resource::String::InstallAndUpgradeCommandsReportDependencies) << + Workflow::ManagePackageDependencies(Resource::String::InstallAndUpgradeCommandsReportDependencies) << Workflow::DownloadInstaller; } @@ -397,8 +397,12 @@ namespace AppInstaller::CLI::Workflow void InstallMultiplePackages::operator()(Execution::Context& context) const { - // Show all license agreements before installing anything - context << Workflow::EnsurePackageAgreementsAcceptanceForMultipleInstallers; + if (m_ensurePackageAgreements) + { + // Show all license agreements before installing anything + context << Workflow::EnsurePackageAgreementsAcceptanceForMultipleInstallers; + } + if (context.IsTerminated()) { return; @@ -438,10 +442,13 @@ namespace AppInstaller::CLI::Workflow installContext.Add<Execution::Data::InstalledPackageVersion>(package.InstalledPackageVersion); installContext.Add<Execution::Data::Installer>(package.Installer); - installContext << - Workflow::ReportIdentityAndInstallationDisclaimer << - Workflow::DownloadInstaller << - Workflow::InstallPackageInstaller; + installContext << Workflow::ReportIdentityAndInstallationDisclaimer; + if (!m_ignorePackageDependencies) + { + installContext << Workflow::ManagePackageDependencies(m_dependenciesReportMessage); + } + installContext << Workflow::DownloadInstaller; + installContext << Workflow::InstallPackageInstaller; installContext.Reporter.Info() << std::endl; diff --git a/src/AppInstallerCLICore/Workflows/InstallFlow.h b/src/AppInstallerCLICore/Workflows/InstallFlow.h @@ -91,7 +91,10 @@ namespace AppInstaller::CLI::Workflow struct ReportInstallerResult : public WorkflowTask { ReportInstallerResult(std::string_view installerType, HRESULT hr, bool isHResult = false) : - WorkflowTask("ReportInstallerResult"), m_installerType(installerType), m_hr(hr), m_isHResult(isHResult) {} + WorkflowTask("ReportInstallerResult"), + m_installerType(installerType), + m_hr(hr), + m_isHResult(isHResult) {} void operator()(Execution::Context& context) const override; @@ -135,11 +138,18 @@ namespace AppInstaller::CLI::Workflow // Outputs: None struct InstallMultiplePackages : public WorkflowTask { - InstallMultiplePackages(StringResource::StringId dependenciesReportMessage, HRESULT resultOnFailure, std::vector<HRESULT>&& ignorableInstallResults = {}) : + InstallMultiplePackages( + StringResource::StringId dependenciesReportMessage, + HRESULT resultOnFailure, + std::vector<HRESULT>&& ignorableInstallResults = {}, + bool ensurePackageAgreements = true, + bool ignoreDependencies = false) : WorkflowTask("InstallMultiplePackages"), m_dependenciesReportMessage(dependenciesReportMessage), m_resultOnFailure(resultOnFailure), - m_ignorableInstallResults(std::move(ignorableInstallResults)) {} + m_ignorableInstallResults(std::move(ignorableInstallResults)), + m_ignorePackageDependencies(ignoreDependencies), + m_ensurePackageAgreements(ensurePackageAgreements) {} void operator()(Execution::Context& context) const override; @@ -147,6 +157,8 @@ namespace AppInstaller::CLI::Workflow HRESULT m_resultOnFailure; std::vector<HRESULT> m_ignorableInstallResults; StringResource::StringId m_dependenciesReportMessage; + bool m_ignorePackageDependencies; + bool m_ensurePackageAgreements; }; // Stores the existing set of packages in ARP. diff --git a/src/AppInstallerCLICore/Workflows/ShowFlow.cpp b/src/AppInstallerCLICore/Workflows/ShowFlow.cpp @@ -172,7 +172,10 @@ namespace AppInstaller::CLI::Workflow dependencies.ApplyToType(Manifest::DependencyType::Package, [&info](Manifest::Dependency dependency) { info << " "_liv << dependency.Id; - if (dependency.MinVersion) info << " [>= " << dependency.MinVersion.value() << "]"; + if (dependency.MinVersion) + { + info << " [>= " << dependency.MinVersion.value().ToString() << "]"; + } info << std::endl; }); } diff --git a/src/AppInstallerCLICore/Workflows/WorkflowBase.cpp b/src/AppInstallerCLICore/Workflows/WorkflowBase.cpp @@ -297,12 +297,22 @@ namespace AppInstaller::CLI::Workflow return E_UNEXPECTED; } - void OpenSource(Execution::Context& context) + void OpenSource::operator()(Execution::Context& context) const { std::string_view sourceName; - if (context.Args.Contains(Execution::Args::Type::Source)) + if (m_forDependencies) { - sourceName = context.Args.GetArg(Execution::Args::Type::Source); + if (context.Args.Contains(Execution::Args::Type::DependencySource)) + { + sourceName = context.Args.GetArg(Execution::Args::Type::DependencySource); + } + } + else + { + if (context.Args.Contains(Execution::Args::Type::Source)) + { + sourceName = context.Args.GetArg(Execution::Args::Type::Source); + } } auto source = OpenNamedSource(context, sourceName); @@ -311,13 +321,21 @@ namespace AppInstaller::CLI::Workflow return; } + context << HandleSourceAgreements(source); if (context.IsTerminated()) { return; } - context.Add<Execution::Data::Source>(std::move(source)); + if (m_forDependencies) + { + context.Add<Execution::Data::DependencySource>(std::move(source)); + } + else + { + context.Add<Execution::Data::Source>(std::move(source)); + } } void OpenNamedSourceForSources::operator()(Execution::Context& context) const @@ -360,22 +378,55 @@ namespace AppInstaller::CLI::Workflow // A well known predefined source should return a value. THROW_HR_IF(E_UNEXPECTED, !source); - context.Add<Execution::Data::Source>(std::move(source)); + if (m_forDependencies) + { + context.Add<Execution::Data::DependencySource>(std::move(source)); + } + else + { + context.Add<Execution::Data::Source>(std::move(source)); + } } void OpenCompositeSource::operator()(Execution::Context& context) const { // Get the already open source for use as the available. - std::shared_ptr<Repository::ISource> availableSource = context.Get<Execution::Data::Source>(); + std::shared_ptr<Repository::ISource> availableSource; + if (m_forDependencies) + { + availableSource = context.Get<Execution::Data::DependencySource>(); + } + else + { + availableSource = context.Get<Execution::Data::Source>(); + } // Open the predefined source. - context << OpenPredefinedSource(m_predefinedSource); + context << OpenPredefinedSource(m_predefinedSource, m_forDependencies); // Create the composite source from the two. - std::shared_ptr<Repository::ISource> compositeSource = Repository::CreateCompositeSource(context.Get<Execution::Data::Source>(), availableSource); + std::shared_ptr<Repository::ISource> source; + std::shared_ptr<Repository::ISource> compositeSource; + if (m_forDependencies) + { + source = context.Get<Execution::Data::DependencySource>(); + compositeSource = Repository::CreateCompositeSource(source, availableSource, CompositeSearchBehavior::AvailablePackages); + } + else + { + source = context.Get<Execution::Data::Source>(); + compositeSource = Repository::CreateCompositeSource(source, availableSource); + } // Overwrite the source with the composite. - context.Add<Execution::Data::Source>(std::move(compositeSource)); + if (m_forDependencies) + { + context.Add<Execution::Data::DependencySource>(std::move(compositeSource)); + } + else + { + context.Add<Execution::Data::Source>(std::move(compositeSource)); + } } void SearchSourceForMany(Execution::Context& context) @@ -898,7 +949,7 @@ namespace AppInstaller::CLI::Workflow else { context << - OpenSource << + OpenSource() << SearchSourceForSingle << HandleSearchResultFailures << EnsureOneMatchFromSearchResult(false) << diff --git a/src/AppInstallerCLICore/Workflows/WorkflowBase.h b/src/AppInstallerCLICore/Workflows/WorkflowBase.h @@ -63,7 +63,15 @@ namespace AppInstaller::CLI::Workflow // Required Args: None // Inputs: None // Outputs: Source - void OpenSource(Execution::Context& context); + struct OpenSource : public WorkflowTask + { + OpenSource(bool forDependencies = false) : WorkflowTask("OpenSource"), m_forDependencies(forDependencies) {} + + void operator()(Execution::Context& context) const override; + + private: + bool m_forDependencies; + }; // Creates a source object for a source specified by name, and adds it to the list of open sources. // Required Args: None @@ -85,12 +93,13 @@ namespace AppInstaller::CLI::Workflow // Outputs: Source struct OpenPredefinedSource : public WorkflowTask { - OpenPredefinedSource(Repository::PredefinedSource source) : WorkflowTask("OpenPredefinedSource"), m_predefinedSource(source) {} + OpenPredefinedSource(Repository::PredefinedSource source, bool forDependencies = false) : WorkflowTask("OpenPredefinedSource"), m_predefinedSource(source), m_forDependencies(forDependencies) {} void operator()(Execution::Context& context) const override; private: Repository::PredefinedSource m_predefinedSource; + bool m_forDependencies; }; // Creates a composite source from the given predefined source and the existing source. @@ -99,12 +108,13 @@ namespace AppInstaller::CLI::Workflow // Outputs: Source struct OpenCompositeSource : public WorkflowTask { - OpenCompositeSource(Repository::PredefinedSource source) : WorkflowTask("OpenCompositeSource"), m_predefinedSource(source) {} + OpenCompositeSource(Repository::PredefinedSource source, bool forDependencies = false) : WorkflowTask("OpenCompositeSource"), m_predefinedSource(source), m_forDependencies(forDependencies) {} void operator()(Execution::Context& context) const override; private: Repository::PredefinedSource m_predefinedSource; + bool m_forDependencies; }; // Performs a search on the source. diff --git a/src/AppInstallerCLIPackage/Shared/Strings/en-us/winget.resw b/src/AppInstallerCLIPackage/Shared/Strings/en-us/winget.resw @@ -939,6 +939,45 @@ Configuration is disabled due to Group Policy.</value> <value>This package requires the following dependencies:</value> <comment>Install and Upgrade commands sentence showed before reporting dependencies</comment> </data> + <data name="DependenciesFlowInstall" xml:space="preserve"> + <value>Installing dependencies:</value> + </data> + <data name="DependenciesFlowSourceNotFound" xml:space="preserve"> + <value>Dependency source not found</value> + </data> + <data name="DependenciesFlowSourceTooManyMatches" xml:space="preserve"> + <value>Package search yield more than one result.</value> + <comment>When node package id search yield too many matches</comment> + </data> + <data name="DependenciesFlowPackageVersionNotFound" xml:space="preserve"> + <value>Latest version not found for package</value> + <comment>When no suitable version found for the specific package.</comment> + </data> + <data name="DependenciesFlowNoInstallerFound" xml:space="preserve"> + <value>No installers found</value> + </data> + <data name="DependenciesFlowNoMinVersion" xml:space="preserve"> + <value>Minimum required version not available for package</value> + </data> + <data name="DependenciesFlowNoMatches" xml:space="preserve"> + <value>No matches</value> + <comment>When package search yields no matches</comment> + </data> + <data name="DependenciesFlowContainsLoop" xml:space="preserve"> + <value>Has loop</value> + <comment>Dependency graph has loop</comment> + </data> + <data name="DependenciesFlowNoSuitableInstallerFound" xml:space="preserve"> + <value>No suitable installer found for manifest</value> + <comment>Attempt to get preferred installer for manifest failed.</comment> + </data> + <data name="DependenciesManagementError" xml:space="preserve"> + <value>Error processing package dependencies, do you wish to continue installation?</value> + <comment>Prompt message shown when dependencies processing yields errors.</comment> + </data> + <data name="DependenciesManagementExitMessage" xml:space="preserve"> + <value>Error processing package dependencies, exiting...</value> + </data> <data name="PackageDependencies" xml:space="preserve"> <value>Packages</value> </data> @@ -956,6 +995,10 @@ Configuration is disabled due to Group Policy.</value> <data name="WindowsLibrariesDependencies" xml:space="preserve"> <value>Windows Libraries</value> </data> + <data name="DependencySourceArgumentDescription" xml:space="preserve"> + <value>Find package dependencies using the specified source</value> + <comment>For getting package type dependencies when installing from a local manifest</comment> + </data> <data name="WindowsStoreTerms" xml:space="preserve"> <value>Windows Store Terms</value> </data> diff --git a/src/AppInstallerCLITests/AppInstallerCLITests.vcxproj b/src/AppInstallerCLITests/AppInstallerCLITests.vcxproj @@ -174,6 +174,7 @@ </PostBuildEvent> </ItemDefinitionGroup> <ItemGroup> + <ClInclude Include="DependenciesTestSource.h" /> <ClInclude Include="pch.h" /> <ClInclude Include="TestCommon.h" /> <ClInclude Include="TestRestRequestHandler.h" /> @@ -187,6 +188,7 @@ <ClCompile Include="Completion.cpp" /> <ClCompile Include="CompositeSource.cpp" /> <ClCompile Include="CustomHeader.cpp" /> + <ClCompile Include="Dependencies.cpp" /> <ClCompile Include="Downloader.cpp" /> <ClCompile Include="ExperimentalFeature.cpp" /> <ClCompile Include="GroupPolicy.cpp" /> diff --git a/src/AppInstallerCLITests/AppInstallerCLITests.vcxproj.filters b/src/AppInstallerCLITests/AppInstallerCLITests.vcxproj.filters @@ -42,6 +42,9 @@ <ClInclude Include="TestRestRequestHandler.h"> <Filter>Header Files</Filter> </ClInclude> + <ClInclude Include="DependenciesTestSource.h"> + <Filter>Header Files</Filter> + </ClInclude> </ItemGroup> <ItemGroup> <ClCompile Include="pch.cpp"> @@ -176,6 +179,9 @@ <ClCompile Include="RestInterface_1_1.cpp"> <Filter>Source Files</Filter> </ClCompile> + <ClCompile Include="Dependencies.cpp"> + <Filter>Source Files</Filter> + </ClCompile> <ClCompile Include="PackageTrackingCatalog.cpp"> <Filter>Source Files</Filter> </ClCompile> diff --git a/src/AppInstallerCLITests/Dependencies.cpp b/src/AppInstallerCLITests/Dependencies.cpp @@ -0,0 +1,209 @@ +#include "pch.h" +#include "TestCommon.h" +#include "TestSource.h" +#include "DependenciesTestSource.h" +#include <winget/DependenciesGraph.h> +#include <Workflows/DependencyNodeProcessor.h> +#include <AppInstallerErrors.h> +#include <AppInstallerStrings.h> +#include <Workflows/DependenciesFlow.h> +#include <Workflows/WorkflowBase.h> +#include <Public/AppInstallerRepositorySource.h> +#include <Public/AppInstallerRepositorySearch.h> +#include <winget/ManifestYamlParser.h> +#include <Resources.h> + +using namespace winrt::Windows::Foundation; +using namespace winrt::Windows::Management::Deployment; +using namespace TestCommon; +using namespace AppInstaller::CLI; +using namespace AppInstaller::CLI::Execution; +using namespace AppInstaller::CLI::Workflow; +using namespace AppInstaller::Logging; +using namespace AppInstaller::Manifest; +using namespace AppInstaller::Repository; +using namespace AppInstaller::Settings; +using namespace AppInstaller::Utility; + +TEST_CASE("DependencyGraph_BFirst", "[dependencyGraph][dependencies]") +{ + TestCommon::TempFile installResultPath("TestExeInstalled.txt"); + std::vector<Dependency> installationOrder; + + const auto& manifest = CreateFakeManifestWithDependencies("NeedsToInstallBFirst"); + const auto& installers = manifest.Installers; + const Dependency& rootAsDependency = Dependency(DependencyType::Package, manifest.Id); + DependencyList rootDependencies; + std::for_each(installers.begin(), installers.end(), [&](ManifestInstaller installer) { rootDependencies.Add(installer.Dependencies); }); + + DependencyGraph graph(rootAsDependency, rootDependencies, [&](Dependency node) + { + DependencyList dependencyList; + auto dependencyManifest = CreateFakeManifestWithDependencies(manifest.Id); + + for (auto installer : dependencyManifest.Installers) + { + dependencyList.Add(installer.Dependencies); + } + + return dependencyList; + }); + + graph.BuildGraph(); + + installationOrder = graph.GetInstallationOrder(); + + REQUIRE(installationOrder.size() == 3); + REQUIRE(installationOrder.at(0).Id == "C"); + REQUIRE(installationOrder.at(1).Id == "B"); + REQUIRE(installationOrder.at(2).Id == "NeedsToInstallBFirst"); +} + +TEST_CASE("DependencyGraph_InStackNoLoop", "[dependencyGraph][dependencies]") +{ + TestCommon::TempFile installResultPath("TestExeInstalled.txt"); + std::vector<Dependency> installationOrder; + + const auto& manifest = CreateFakeManifestWithDependencies("DependencyAlreadyInStackButNoLoop"); + const auto& installers = manifest.Installers; + const Dependency& rootAsDependency = Dependency(DependencyType::Package, manifest.Id); + DependencyList rootDependencies; + std::for_each(installers.begin(), installers.end(), [&](ManifestInstaller installer) { rootDependencies.Add(installer.Dependencies); }); + + DependencyGraph graph(rootAsDependency, rootDependencies, [&](Dependency node) + { + DependencyList dependencyList; + auto dependencyManifest = CreateFakeManifestWithDependencies(manifest.Id); + + for (auto installer : dependencyManifest.Installers) + { + dependencyList.Add(installer.Dependencies); + } + + return dependencyList; + }); + + graph.BuildGraph(); + + installationOrder = graph.GetInstallationOrder(); + + REQUIRE(installationOrder.size() == 3); + REQUIRE(installationOrder.at(0).Id == "F"); + REQUIRE(installationOrder.at(1).Id == "C"); + REQUIRE(installationOrder.at(2).Id == "DependencyAlreadyInStackButNoLoop"); +} + +TEST_CASE("DependencyGraph_EasyToSeeLoop", "[dependencyGraph][dependencies]") +{ + TestCommon::TempFile installResultPath("TestExeInstalled.txt"); + std::vector<Dependency> installationOrder; + + const auto& manifest = CreateFakeManifestWithDependencies("EasyToSeeLoop"); + const auto& installers = manifest.Installers; + const Dependency& rootAsDependency = Dependency(DependencyType::Package, manifest.Id); + DependencyList rootDependencies; + std::for_each(installers.begin(), installers.end(), [&](ManifestInstaller installer) { rootDependencies.Add(installer.Dependencies); }); + + DependencyGraph graph(rootAsDependency, rootDependencies, [&](Dependency node) { + DependencyList dependencyList; + auto dependencyManifest = CreateFakeManifestWithDependencies(manifest.Id); + + for (auto installer : dependencyManifest.Installers) + { + dependencyList.Add(installer.Dependencies); + } + + return dependencyList; + }); + + graph.BuildGraph(); + + installationOrder = graph.GetInstallationOrder(); + + bool hasLoop = graph.HasLoop(); + + REQUIRE(hasLoop); + + REQUIRE(installationOrder.size() == 2); + REQUIRE(installationOrder.at(0).Id == "D"); + REQUIRE(installationOrder.at(1).Id == "EasyToSeeLoop"); +} + +TEST_CASE("DependencyNodeProcessor_SkipInstalled", "[dependencies]") +{ + TestCommon::TempFile installResultPath("TestExeInstalled.txt"); + + std::ostringstream installOutput; + Context context{ installOutput, std::cin }; + + Manifest manifest = CreateFakeManifestWithDependencies("installed1"); + + context.Add<Execution::Data::DependencySource>(std::make_shared<DependenciesTestSource>()); + DependencyNodeProcessor nodeProcessor(context); + + Dependency rootAsDependency(DependencyType::Package, manifest.Id); + + DependencyNodeProcessorResult result = nodeProcessor.EvaluateDependencies(rootAsDependency); + REQUIRE(result == DependencyNodeProcessorResult::Skipped); +} + +TEST_CASE("DependencyNodeProcessor_NoInstallers", "[dependencies]") +{ + TestCommon::TempFile installResultPath("TestExeInstalled.txt"); + + std::ostringstream installOutput; + Context context { installOutput, std::cin }; + + Manifest manifest = CreateFakeManifestWithDependencies("withoutInstallers"); + + context.Add<Execution::Data::DependencySource>(std::make_shared<DependenciesTestSource>()); + DependencyNodeProcessor nodeProcessor(context); + + Dependency rootAsDependency(DependencyType::Package, manifest.Id); + + DependencyNodeProcessorResult result = nodeProcessor.EvaluateDependencies(rootAsDependency); + REQUIRE(installOutput.str().find(Resource::LocString(Resource::String::DependenciesFlowNoInstallerFound)) != std::string::npos); + REQUIRE(result == DependencyNodeProcessorResult::Error); +} + +TEST_CASE("DependencyNodeProcessor_StackOrderIsOk", "[dependencies]") +{ + TestCommon::TempFile installResultPath("TestExeInstalled.txt"); + + std::ostringstream installOutput; + Context context{ installOutput, std::cin }; + + Manifest manifest = CreateFakeManifestWithDependencies("StackOrderIsOk"); + + context.Add<Execution::Data::DependencySource>(std::make_shared<DependenciesTestSource>()); + DependencyNodeProcessor nodeProcessor(context); + + Dependency rootAsDependency(DependencyType::Package, manifest.Id); + + DependencyNodeProcessorResult result = nodeProcessor.EvaluateDependencies(rootAsDependency); + auto dependencyList = nodeProcessor.GetDependencyList(); + REQUIRE(dependencyList.Size() == 1); + REQUIRE(dependencyList.HasDependency(Dependency(DependencyType::Package, "C"))); + REQUIRE(result == DependencyNodeProcessorResult::Success); +} + +TEST_CASE("DependencyNodeProcessor_NoMatches", "[dependencies]") +{ + TestCommon::TempFile installResultPath("TestExeInstalled.txt"); + + std::ostringstream installOutput; + Context context{ installOutput, std::cin }; + + Manifest manifest = CreateFakeManifestWithDependencies("NoMatches"); + + context.Add<Execution::Data::DependencySource>(std::make_shared<DependenciesTestSource>()); + DependencyNodeProcessor nodeProcessor(context); + + Dependency rootAsDependency(DependencyType::Package, manifest.Id); + + DependencyNodeProcessorResult result = nodeProcessor.EvaluateDependencies(rootAsDependency); + auto dependencyList = nodeProcessor.GetDependencyList(); + REQUIRE(dependencyList.Size() == 0); + REQUIRE(installOutput.str().find(Resource::LocString(Resource::String::DependenciesFlowNoMatches)) != std::string::npos); + REQUIRE(result == DependencyNodeProcessorResult::Error); +}+ \ No newline at end of file diff --git a/src/AppInstallerCLITests/DependenciesTestSource.h b/src/AppInstallerCLITests/DependenciesTestSource.h @@ -0,0 +1,203 @@ +#pragma once + +#include "pch.h" +#include "TestSource.h" +#include "TestCommon.h" +#include <Public/AppInstallerRepositorySearch.h> +#include <Public/AppInstallerRepositorySource.h> +#include <winget/ManifestYamlParser.h> + +using namespace AppInstaller::Repository; +using namespace AppInstaller::Manifest; + +namespace TestCommon +{ + namespace + { + Manifest CreateFakeManifestWithDependencies(std::string input) + { + auto manifest = YamlParser::CreateFromPath(TestDataFile("Installer_Exe_Dependencies.yaml")); + manifest.Id = input; + manifest.Moniker = input; + + auto& installer = manifest.Installers.at(0); + installer.ProductId = input; + installer.Dependencies.Clear(); + + if (input == "withoutInstallers") + { + manifest.Installers.clear(); + return manifest; + } + + /* + * Dependencies: + * "A": Depends on the test + * B: NoDependency + * C: B + * D: E + * E: D + * F: B + * G: C + * H: G, B + * + * installed1 + * minVersion1.0 + * minVersion1.5 + * requires1.5: minVersion1.5 + * minVersion2.0 //invalid version (not returned as result) + */ + + //-- predefined + if (input == "C") + { + installer.Dependencies.Add(Dependency(DependencyType::Package, "B")); + } + if (input == "D") + { + installer.Dependencies.Add(Dependency(DependencyType::Package, "E")); + } + if (input == "E") + { + installer.Dependencies.Add(Dependency(DependencyType::Package, "D")); + } + if (input == "F") + { + installer.Dependencies.Add(Dependency(DependencyType::Package, "B")); + } + if (input == "G") + { + installer.Dependencies.Add(Dependency(DependencyType::Package, "C")); + } + if (input == "H") + { + installer.Dependencies.Add(Dependency(DependencyType::Package, "G")); + installer.Dependencies.Add(Dependency(DependencyType::Package, "B")); + } + if (input == "installed1") + { + installer.Dependencies.Add(Dependency(DependencyType::Package, "installed1Dep")); + } + if (input == "minVersion1.0") + { + manifest.Id = "minVersion"; + manifest.Version = "1.0"; + } + if (input == "minVersion1.5") + { + manifest.Id = "minVersion"; + manifest.Version = "1.5"; + } + if (input == "requires1.5") + { + installer.Dependencies.Add(Dependency(DependencyType::Package, "minVersion", "1.5")); + } + + // depends on test + if (input == "StackOrderIsOk") + { + installer.Dependencies.Add(Dependency(DependencyType::Package, "C")); + } + if (input == "NeedsToInstallBFirst") + { + installer.Dependencies.Add(Dependency(DependencyType::Package, "B")); + installer.Dependencies.Add(Dependency(DependencyType::Package, "C")); + } + if (input == "EasyToSeeLoop") + { + installer.Dependencies.Add(Dependency(DependencyType::Package, "D")); + } + if (input == "DependencyAlreadyInStackButNoLoop") + { + installer.Dependencies.Add(Dependency(DependencyType::Package, "C")); + installer.Dependencies.Add(Dependency(DependencyType::Package, "F")); + } + if (input == "PathBetweenBranchesButNoLoop") + { + installer.Dependencies.Add(Dependency(DependencyType::Package, "C")); + installer.Dependencies.Add(Dependency(DependencyType::Package, "H")); + } + if (input == "DependenciesInstalled") + { + installer.Dependencies.Add(Dependency(DependencyType::Package, "installed1")); + } + if (input == "DependenciesValidMinVersions") + { + installer.Dependencies.Add(Dependency(DependencyType::Package, "minVersion", "1.0")); + } + if (input == "DependenciesValidMinVersionsMultiple") + { + installer.Dependencies.Add(Dependency(DependencyType::Package, "minVersion", "1.0")); + installer.Dependencies.Add(Dependency(DependencyType::Package, "requires1.5")); + } + + return manifest; + } + } + + struct DependenciesTestSource : public TestSource + { + SearchResult Search(const SearchRequest& request) const override + { + SearchResult result; + + std::string input; + + if (request.Query) + { + input = request.Query->Value; + } + else if (!request.Inclusions.empty()) + { + input = request.Inclusions[0].Value; + } + else if (!request.Filters.empty()) + { + input = request.Filters[0].Value; + }// else: default? + + bool installed = false; + if (input == "installed1") + { + installed = true; + } + + if (input == "NoMatches") + { + return result; + } + + Manifest manifest = CreateFakeManifestWithDependencies(input); + + //TODO: + // test for installed packages and packages that need upgrades + // test for different min Version of dependencies + if (installed) + { + //auto manifest2 = YamlParser::CreateFromPath(TestDataFile("UpdateFlowTest_Exe.yaml")); + result.Matches.emplace_back( + ResultMatch( + TestPackage::Make( + manifest, + TestPackage::MetadataMap{ { PackageVersionMetadata::InstalledType, "Exe" } }, + std::vector<Manifest>{ manifest }, + const_cast<DependenciesTestSource*>(this)->shared_from_this() + ), + PackageMatchFilter(PackageMatchField::Id, MatchType::CaseInsensitive, manifest.Id))); + } + else + { + result.Matches.emplace_back( + ResultMatch( + TestPackage::Make( + std::vector<Manifest>{ manifest }, + const_cast<DependenciesTestSource*>(this)->shared_from_this() + ), + PackageMatchFilter(PackageMatchField::Id, MatchType::CaseInsensitive, manifest.Id))); + } + + return result; + } + }; + +} diff --git a/src/AppInstallerCLITests/TestData/Installer_Exe_Dependencies.yaml b/src/AppInstallerCLITests/TestData/Installer_Exe_Dependencies.yaml @@ -1,5 +1,5 @@ PackageIdentifier: AppInstallerCliTest.TestExeInstaller.Dependencies -PackageVersion: 1.0.0.0 +PackageVersion: 1.0 PackageLocale: en-US PackageName: AppInstaller Test Exe Installer With Package Dep Publisher: Microsoft Corporation diff --git a/src/AppInstallerCLITests/WorkFlow.cpp b/src/AppInstallerCLITests/WorkFlow.cpp @@ -5,6 +5,7 @@ #include "TestSource.h" #include "TestHooks.h" #include "TestSettings.h" +#include "DependenciesTestSource.h" #include <AppInstallerErrors.h> #include <AppInstallerLogging.h> #include <AppInstallerDownloader.h> @@ -15,6 +16,7 @@ #include <Workflows/MsiInstallFlow.h> #include <Workflows/UninstallFlow.h> #include <Workflows/UpdateFlow.h> +#include <Workflows/DependenciesFlow.h> #include <Workflows/MSStoreInstallerHandler.h> #include <Workflows/ShowFlow.h> #include <Workflows/SourceFlow.h> @@ -80,7 +82,7 @@ namespace auto manifest = YamlParser::CreateFromPath(TestDataFile("InstallFlowTest_Exe.yaml")); result.Matches.emplace_back( ResultMatch( - TestPackage::Make(std::vector<Manifest>{ manifest }, this->shared_from_this()), + TestPackage::Make(std::vector<Manifest>{ manifest }, const_cast<WorkflowTestSource*>(this)->shared_from_this()), PackageMatchFilter(PackageMatchField::Id, MatchType::Exact, "TestQueryReturnOne"))); } else if (input == "TestQueryReturnTwo") @@ -88,13 +90,13 @@ namespace auto manifest = YamlParser::CreateFromPath(TestDataFile("InstallFlowTest_Exe.yaml")); result.Matches.emplace_back( ResultMatch( - TestPackage::Make(std::vector<Manifest>{ manifest }, this->shared_from_this()), + TestPackage::Make(std::vector<Manifest>{ manifest }, const_cast<WorkflowTestSource*>(this)->shared_from_this()), PackageMatchFilter(PackageMatchField::Id, MatchType::Exact, "TestQueryReturnTwo"))); auto manifest2 = YamlParser::CreateFromPath(TestDataFile("Manifest-Good.yaml")); result.Matches.emplace_back( ResultMatch( - TestPackage::Make(std::vector<Manifest>{ manifest2 }, this->shared_from_this()), + TestPackage::Make(std::vector<Manifest>{ manifest2 }, const_cast<WorkflowTestSource*>(this)->shared_from_this()), PackageMatchFilter(PackageMatchField::Id, MatchType::Exact, "TestQueryReturnTwo"))); } @@ -142,7 +144,7 @@ namespace { PackageVersionMetadata::SilentUninstallCommand, "C:\\uninstall.exe /silence" }, }, std::vector<Manifest>{ manifest3, manifest2, manifest }, - this->shared_from_this() + const_cast<WorkflowTestCompositeSource*>(this)->shared_from_this() ), PackageMatchFilter(PackageMatchField::Id, MatchType::Exact, "AppInstallerCliTest.TestExeInstaller"))); } @@ -157,7 +159,7 @@ namespace manifest, TestPackage::MetadataMap{ { PackageVersionMetadata::InstalledType, "Msix" } }, std::vector<Manifest>{ manifest2, manifest }, - this->shared_from_this() + const_cast<WorkflowTestCompositeSource*>(this)->shared_from_this() ), PackageMatchFilter(PackageMatchField::Id, MatchType::Exact, "AppInstallerCliTest.TestMsixInstaller"))); } @@ -171,7 +173,7 @@ namespace manifest, TestPackage::MetadataMap{ { PackageVersionMetadata::InstalledType, "MSStore" } }, std::vector<Manifest>{ manifest }, - this->shared_from_this() + const_cast<WorkflowTestCompositeSource*>(this)->shared_from_this() ), PackageMatchFilter(PackageMatchField::Id, MatchType::Exact, "AppInstallerCliTest.TestMSStoreInstaller"))); } @@ -186,7 +188,7 @@ namespace manifest2, TestPackage::MetadataMap{ { PackageVersionMetadata::InstalledType, "Exe" } }, std::vector<Manifest>{ manifest2, manifest }, - this->shared_from_this() + const_cast<WorkflowTestCompositeSource*>(this)->shared_from_this() ), PackageMatchFilter(PackageMatchField::Id, MatchType::Exact, "AppInstallerCliTest.TestExeInstaller"))); } @@ -201,7 +203,7 @@ namespace manifest, TestPackage::MetadataMap{ { PackageVersionMetadata::InstalledType, "Msix" } }, std::vector<Manifest>{ manifest2, manifest }, - this->shared_from_this() + const_cast<WorkflowTestCompositeSource*>(this)->shared_from_this() ), PackageMatchFilter(PackageMatchField::Id, MatchType::Exact, "AppInstallerCliTest.TestExeInstaller"))); } @@ -213,7 +215,7 @@ namespace ResultMatch( TestPackage::Make( std::vector<Manifest>{ manifest }, - this->shared_from_this() + const_cast<WorkflowTestCompositeSource*>(this)->shared_from_this() ), PackageMatchFilter(PackageMatchField::Id, MatchType::Exact, "AppInstallerCliTest.TestExeInstaller"))); } @@ -233,7 +235,7 @@ namespace { PackageVersionMetadata::SilentUninstallCommand, "C:\\uninstall.exe /silence" }, }, std::vector<Manifest>{ manifest2, manifest }, - this->shared_from_this() + const_cast<WorkflowTestCompositeSource*>(this)->shared_from_this() ), PackageMatchFilter(PackageMatchField::Id, MatchType::Exact, "AppInstallerCliTest.TestExeInstaller.Dependencies"))); } @@ -245,7 +247,7 @@ namespace ResultMatch( TestPackage::Make( std::vector<Manifest>{ manifest }, - this->shared_from_this() + const_cast<WorkflowTestCompositeSource*>(this)->shared_from_this() ), PackageMatchFilter(PackageMatchField::Id, MatchType::Exact, "AppInstallerCliTest.TestMsixInstaller.WFDep"))); } @@ -363,7 +365,7 @@ namespace void OverrideForOpenSource(TestContext& context) { - context.Override({ Workflow::OpenSource, [](TestContext& context) + context.Override({ "OpenSource", [](TestContext& context) { context.Add<Execution::Data::Source>(std::make_shared<WorkflowTestSource>()); } }); @@ -371,7 +373,7 @@ void OverrideForOpenSource(TestContext& context) void OverrideForCompositeInstalledSource(TestContext& context, bool upgradeUsesLicenses = false) { - context.Override({ Workflow::OpenSource, [](TestContext&) + context.Override({ "OpenSource", [](TestContext&) { } }); @@ -394,6 +396,27 @@ void OverrideForImportSource(TestContext& context) } }); } +void OverrideOpenSourceForDependencies(TestContext& context) +{ + context.Override({ "OpenSource", [](TestContext& context) + { + context.Add<Execution::Data::Source>(std::make_shared<DependenciesTestSource>()); + } }); + + context.Override({ Workflow::OpenDependencySource, [](TestContext& context) + { + context.Add<Execution::Data::DependencySource>(std::make_shared<DependenciesTestSource>()); + } }); +} + +void OverrideDependencySource(TestContext& context) +{ + context.Override({ Workflow::OpenDependencySource, [](TestContext& context) + { + context.Add<Execution::Data::DependencySource>(std::make_shared<DependenciesTestSource>()); + } }); +} + void OverrideForUpdateInstallerMotw(TestContext& context) { context.Override({ UpdateInstallerFileMotwIfApplicable, [](TestContext&) @@ -425,6 +448,24 @@ void OverrideForShellExecute(TestContext& context) OverrideForUpdateInstallerMotw(context); } +void OverrideForShellExecute(TestContext& context, std::vector<Dependency>& installationLog) +{ + context.Override({ DownloadInstallerFile, [&installationLog](TestContext& context) + { + context.Add<Data::HashPair>({ {}, {} }); + context.Add<Data::InstallerPath>(TestDataFile("AppInstallerTestExeInstaller.exe")); + + auto dependency = Dependency(DependencyType::Package, context.Get<Execution::Data::Manifest>().Id, context.Get<Execution::Data::Manifest>().Version); + installationLog.push_back(dependency); + }}); + + context.Override({ RenameDownloadedInstaller, [](TestContext&) + { + } }); + + OverrideForUpdateInstallerMotw(context); +} + void OverrideForDirectMsi(TestContext& context) { OverrideForCheckExistingInstaller(context); @@ -435,7 +476,7 @@ void OverrideForDirectMsi(TestContext& context) // We don't have an msi installer for tests, but we won't execute it anyway context.Add<Data::InstallerPath>(TestDataFile("AppInstallerTestExeInstaller.exe")); } }); - + context.Override({ RenameDownloadedInstaller, [](TestContext&) { } }); @@ -557,6 +598,27 @@ void OverrideForSourceAddWithAgreements(TestContext& context) } }); } +void OverrideOpenDependencySource(TestContext& context) +{ + context.Override({ Workflow::OpenDependencySource, [](TestContext& context) + { + context.Add<Execution::Data::DependencySource>(std::make_shared<DependenciesTestSource>()); + } }); +} + +void OverrideForInstallMultiplePackages(TestContext& context) +{ + context.Override({ Workflow::InstallMultiplePackages( + Resource::String::InstallAndUpgradeCommandsReportDependencies, + APPINSTALLER_CLI_ERROR_INSTALL_DEPENDENCIES, + {}, + false, + true), [](TestContext&) + { + + } }); +} + TEST_CASE("ExeInstallFlowWithTestManifest", "[InstallFlow][workflow]") { TestCommon::TempFile installResultPath("TestExeInstalled.txt"); @@ -1055,6 +1117,90 @@ TEST_CASE("ShowFlow_Dependencies", "[ShowFlow][workflow][dependencies]") REQUIRE(showOutput.str().find("ExternalDep") != std::string::npos); } +TEST_CASE("DependencyGraph_SkipInstalled", "[InstallFlow][workflow][dependencyGraph][dependencies]") +{ + TestCommon::TempFile installResultPath("TestExeInstalled.txt"); + + std::ostringstream installOutput; + TestContext context{ installOutput, std::cin }; + + Manifest manifest = CreateFakeManifestWithDependencies("DependenciesInstalled"); + OverrideOpenDependencySource(context); + OverrideForInstallMultiplePackages(context); + + context.Add<Execution::Data::DependencySource>(std::make_shared<DependenciesTestSource>()); + context.Add<Execution::Data::Manifest>(manifest); + context.Add<Execution::Data::Installer>(manifest.Installers[0]); + + TestUserSettings settings; + settings.Set<AppInstaller::Settings::Setting::EFDependencies>({ true }); + + context << ManagePackageDependencies(Resource::String::InstallAndUpgradeCommandsReportDependencies); + + std::vector<Execution::PackageToInstall> installers = context.Get<Execution::Data::PackagesToInstall>(); + REQUIRE(installOutput.str().find(Resource::LocString(Resource::String::DependenciesFlowContainsLoop)) == std::string::npos); + REQUIRE(installers.size() == 0); +} + +TEST_CASE("DependencyGraph_validMinVersions", "[InstallFlow][workflow][dependencyGraph][dependencies]") +{ + TestCommon::TempFile installResultPath("TestExeInstalled.txt"); + + std::ostringstream installOutput; + TestContext context{ installOutput, std::cin }; + Manifest manifest = CreateFakeManifestWithDependencies("DependenciesValidMinVersions"); + OverrideOpenDependencySource(context); + OverrideForInstallMultiplePackages(context); + + context.Add<Execution::Data::DependencySource>(std::make_shared<DependenciesTestSource>()); + context.Add<Execution::Data::Manifest>(manifest); + context.Add<Execution::Data::Installer>(manifest.Installers[0]); + + TestUserSettings settings; + settings.Set<AppInstaller::Settings::Setting::EFDependencies>({ true }); + + context << ManagePackageDependencies(Resource::String::InstallAndUpgradeCommandsReportDependencies); + + std::vector<Execution::PackageToInstall> installers = context.Get<Execution::Data::PackagesToInstall>(); + + REQUIRE(installOutput.str().find(Resource::LocString(Resource::String::DependenciesFlowContainsLoop)) == std::string::npos); + REQUIRE(installers.size() == 1); + REQUIRE(installers.at(0).Manifest.Id == "minVersion"); + // minVersion 1.5 is available but this requires 1.0 so that version is installed + REQUIRE(installers.at(0).Manifest.Version == "1.0"); +} + +TEST_CASE("DependencyGraph_PathNoLoop", "[InstallFlow][workflow][dependencyGraph][dependencies]", ) +{ + TestCommon::TempFile installResultPath("TestExeInstalled.txt"); + + std::ostringstream installOutput; + TestContext context{ installOutput, std::cin }; + Manifest manifest = CreateFakeManifestWithDependencies("PathBetweenBranchesButNoLoop"); + OverrideOpenDependencySource(context); + OverrideForInstallMultiplePackages(context); + + context.Add<Execution::Data::DependencySource>(std::make_shared<DependenciesTestSource>()); + context.Add<Execution::Data::Manifest>(manifest); + context.Add<Execution::Data::Installer>(manifest.Installers[0]); + + TestUserSettings settings; + settings.Set<AppInstaller::Settings::Setting::EFDependencies>({ true }); + + context << ManagePackageDependencies(Resource::String::InstallAndUpgradeCommandsReportDependencies); + + std::vector<Execution::PackageToInstall> installers = context.Get<Execution::Data::PackagesToInstall>(); + + REQUIRE(installOutput.str().find(Resource::LocString(Resource::String::DependenciesFlowContainsLoop)) == std::string::npos); + + // Verify installers are called in order + REQUIRE(installers.size() == 4); + REQUIRE(installers.at(0).Manifest.Id == "B"); + REQUIRE(installers.at(1).Manifest.Id == "C"); + REQUIRE(installers.at(2).Manifest.Id == "G"); + REQUIRE(installers.at(3).Manifest.Id == "H"); +} + TEST_CASE("UpdateFlow_UpdateWithManifest", "[UpdateFlow][workflow]") { TestCommon::TempFile updateResultPath("TestExeInstalled.txt"); @@ -2002,26 +2148,90 @@ TEST_CASE("InstallFlowMultiLocale_PreferenceWithBetterLocale", "[InstallFlow][wo REQUIRE(installResultStr.find("/en-GB") != std::string::npos); } -TEST_CASE("InstallFlow_Dependencies", "[InstallFlow][workflow][dependencies]") +// TODO +// add dependencies for installer tests to DependenciesTestSource (or a new one) +// add tests for min version dependency solving +// add tests that check for correct installation of dependencies (not only the order) + +TEST_CASE("SourceAddFlow_Agreement", "[SourceAddFlow][workflow]") { - TestCommon::TempFile installResultPath("TestExeInstalled.txt"); + std::ostringstream sourceAddOutput; + TestContext context{ sourceAddOutput, std::cin }; + OverrideForSourceAddWithAgreements(context); + context.Args.AddArg(Execution::Args::Type::SourceName, "TestSource"sv); + context.Args.AddArg(Execution::Args::Type::SourceType, "Microsoft.Test"sv); + context.Args.AddArg(Execution::Args::Type::SourceArg, "TestArg"sv); + context.Args.AddArg(Execution::Args::Type::AcceptSourceAgreements); - std::ostringstream installOutput; - TestContext context{ installOutput, std::cin }; - OverrideForShellExecute(context); + SourceAddCommand sourceAdd({}); + sourceAdd.Execute(context); + INFO(sourceAddOutput.str()); - context.Args.AddArg(Execution::Args::Type::Manifest, TestDataFile("Installer_Exe_Dependencies.yaml").GetPath().u8string()); + // Verify agreements are shown + REQUIRE(sourceAddOutput.str().find("Agreement Label") != std::string::npos); + REQUIRE(sourceAddOutput.str().find("Agreement Text") != std::string::npos); + REQUIRE(sourceAddOutput.str().find("https://test") != std::string::npos); + REQUIRE(sourceAddOutput.str().find(Resource::LocString(Resource::String::SourceAgreementsMarketMessage).get()) != std::string::npos); - TestUserSettings settings; - settings.Set<AppInstaller::Settings::Setting::EFDependencies>({ true }); + // Verify Installer is called. + REQUIRE(context.GetTerminationHR() == S_OK); +} - InstallCommand install({}); - install.Execute(context); - INFO(installOutput.str()); +TEST_CASE("SourceAddFlow_Agreement_Prompt_Yes", "[SourceAddFlow][workflow]") +{ + // Accept the agreements by saying "Yes" at the prompt + std::istringstream sourceAddInput{ "y" }; + std::ostringstream sourceAddOutput; + TestContext context{ sourceAddOutput, sourceAddInput }; + OverrideForSourceAddWithAgreements(context); + context.Args.AddArg(Execution::Args::Type::SourceName, "TestSource"sv); + context.Args.AddArg(Execution::Args::Type::SourceType, "Microsoft.Test"sv); + context.Args.AddArg(Execution::Args::Type::SourceArg, "TestArg"sv); - // Verify all types of dependencies are printed - REQUIRE(installOutput.str().find(Resource::LocString(Resource::String::InstallAndUpgradeCommandsReportDependencies).get()) != std::string::npos); - REQUIRE(installOutput.str().find("PreviewIIS") != std::string::npos); + SourceAddCommand sourceAdd({}); + sourceAdd.Execute(context); + INFO(sourceAddOutput.str()); + + // Verify agreements are shown + REQUIRE(sourceAddOutput.str().find("Agreement Label") != std::string::npos); + REQUIRE(sourceAddOutput.str().find("Agreement Text") != std::string::npos); + REQUIRE(sourceAddOutput.str().find("https://test") != std::string::npos); + REQUIRE(sourceAddOutput.str().find(Resource::LocString(Resource::String::SourceAgreementsMarketMessage).get()) != std::string::npos); + + // Verify Installer is called. + REQUIRE(context.GetTerminationHR() == S_OK); +} + +TEST_CASE("SourceAddFlow_Agreement_Prompt_No", "[SourceAddFlow][workflow]") +{ + // Accept the agreements by saying "No" at the prompt + std::istringstream sourceAddInput{ "n" }; + std::ostringstream sourceAddOutput; + TestContext context{ sourceAddOutput, sourceAddInput }; + OverrideForSourceAddWithAgreements(context); + // This tests RemoveSource is called after agreement is not accepted. If they are not called, the test fails with unused override. + context.Override({ GetSourceListWithFilter, [](TestContext&) + { + } }); + context.Override({ RemoveSources, [](TestContext&) + { + } }); + context.Args.AddArg(Execution::Args::Type::SourceName, "TestSource"sv); + context.Args.AddArg(Execution::Args::Type::SourceType, "Microsoft.Test"sv); + context.Args.AddArg(Execution::Args::Type::SourceArg, "TestArg"sv); + + SourceAddCommand sourceAdd({}); + sourceAdd.Execute(context); + INFO(sourceAddOutput.str()); + + // Verify agreements are shown + REQUIRE(sourceAddOutput.str().find("Agreement Label") != std::string::npos); + REQUIRE(sourceAddOutput.str().find("Agreement Text") != std::string::npos); + REQUIRE(sourceAddOutput.str().find("https://test") != std::string::npos); + REQUIRE(sourceAddOutput.str().find(Resource::LocString(Resource::String::SourceAgreementsMarketMessage).get()) != std::string::npos); + + // Verify Installer is called. + REQUIRE(context.GetTerminationHR() == APPINSTALLER_CLI_ERROR_SOURCE_AGREEMENTS_NOT_ACCEPTED); } TEST_CASE("ValidateCommand_Dependencies", "[workflow][dependencies]") @@ -2048,15 +2258,17 @@ TEST_CASE("ValidateCommand_Dependencies", "[workflow][dependencies]") REQUIRE(validateOutput.str().find("ExternalDep") != std::string::npos); } -TEST_CASE("DependenciesMultideclaration_InstallerDependenciesPreference", "[dependencies]") +TEST_CASE("DependencyGraph_StackOrderIsOk", "[InstallFlow][workflow][dependencyGraph][dependencies]") { TestCommon::TempFile installResultPath("TestExeInstalled.txt"); + std::vector<Dependency> installationOrder; std::ostringstream installOutput; TestContext context{ installOutput, std::cin }; - OverrideForShellExecute(context); + OverrideOpenSourceForDependencies(context); + OverrideForShellExecute(context, installationOrder); - context.Args.AddArg(Execution::Args::Type::Manifest, TestDataFile("Installer_Exe_DependenciesMultideclaration.yaml").GetPath().u8string()); + context.Args.AddArg(Execution::Args::Type::Query, "StackOrderIsOk"sv); TestUserSettings settings; settings.Set<AppInstaller::Settings::Setting::EFDependencies>({ true }); @@ -2065,11 +2277,13 @@ TEST_CASE("DependenciesMultideclaration_InstallerDependenciesPreference", "[depe install.Execute(context); INFO(installOutput.str()); - // Verify installer dependencies are shown - REQUIRE(installOutput.str().find(Resource::LocString(Resource::String::InstallAndUpgradeCommandsReportDependencies).get()) != std::string::npos); - REQUIRE(installOutput.str().find("PreviewIIS") != std::string::npos); - // and root dependencies are not - REQUIRE(installOutput.str().find("PreviewIISOnRoot") == std::string::npos); + REQUIRE(installOutput.str().find(Resource::LocString(Resource::String::DependenciesFlowContainsLoop)) == std::string::npos); + + // Verify installers are called in order + REQUIRE(installationOrder.size() == 3); + REQUIRE(installationOrder.at(0).Id == "B"); + REQUIRE(installationOrder.at(1).Id == "C"); + REQUIRE(installationOrder.at(2).Id == "StackOrderIsOk"); } TEST_CASE("InstallerWithoutDependencies_RootDependenciesAreUsed", "[dependencies]") @@ -2079,6 +2293,7 @@ TEST_CASE("InstallerWithoutDependencies_RootDependenciesAreUsed", "[dependencies std::ostringstream installOutput; TestContext context{ installOutput, std::cin }; OverrideForShellExecute(context); + OverrideDependencySource(context); context.Args.AddArg(Execution::Args::Type::Manifest, TestDataFile("Installer_Exe_DependenciesOnRoot.yaml").GetPath().u8string()); @@ -2094,85 +2309,52 @@ TEST_CASE("InstallerWithoutDependencies_RootDependenciesAreUsed", "[dependencies REQUIRE(installOutput.str().find("PreviewIISOnRoot") != std::string::npos); } -TEST_CASE("SourceAddFlow_Agreement", "[SourceAddFlow][workflow]") +TEST_CASE("DependenciesMultideclaration_InstallerDependenciesPreference", "[dependencies]") { - std::ostringstream sourceAddOutput; - TestContext context{ sourceAddOutput, std::cin }; - OverrideForSourceAddWithAgreements(context); - context.Args.AddArg(Execution::Args::Type::SourceName, "TestSource"sv); - context.Args.AddArg(Execution::Args::Type::SourceType, "Microsoft.Test"sv); - context.Args.AddArg(Execution::Args::Type::SourceArg, "TestArg"sv); - context.Args.AddArg(Execution::Args::Type::AcceptSourceAgreements); - - SourceAddCommand sourceAdd({}); - sourceAdd.Execute(context); - INFO(sourceAddOutput.str()); - - // Verify agreements are shown - REQUIRE(sourceAddOutput.str().find("Agreement Label") != std::string::npos); - REQUIRE(sourceAddOutput.str().find("Agreement Text") != std::string::npos); - REQUIRE(sourceAddOutput.str().find("https://test") != std::string::npos); - REQUIRE(sourceAddOutput.str().find(Resource::LocString(Resource::String::SourceAgreementsMarketMessage).get()) != std::string::npos); + TestCommon::TempFile installResultPath("TestExeInstalled.txt"); - // Verify Installer is called. - REQUIRE(context.GetTerminationHR() == S_OK); -} + std::ostringstream installOutput; + TestContext context{ installOutput, std::cin }; + OverrideForShellExecute(context); + OverrideDependencySource(context); -TEST_CASE("SourceAddFlow_Agreement_Prompt_Yes", "[SourceAddFlow][workflow]") -{ - // Accept the agreements by saying "Yes" at the prompt - std::istringstream sourceAddInput{ "y" }; - std::ostringstream sourceAddOutput; - TestContext context{ sourceAddOutput, sourceAddInput }; - OverrideForSourceAddWithAgreements(context); - context.Args.AddArg(Execution::Args::Type::SourceName, "TestSource"sv); - context.Args.AddArg(Execution::Args::Type::SourceType, "Microsoft.Test"sv); - context.Args.AddArg(Execution::Args::Type::SourceArg, "TestArg"sv); + context.Args.AddArg(Execution::Args::Type::Manifest, TestDataFile("Installer_Exe_DependenciesMultideclaration.yaml").GetPath().u8string()); - SourceAddCommand sourceAdd({}); - sourceAdd.Execute(context); - INFO(sourceAddOutput.str()); + TestUserSettings settings; + settings.Set<AppInstaller::Settings::Setting::EFDependencies>({ true }); - // Verify agreements are shown - REQUIRE(sourceAddOutput.str().find("Agreement Label") != std::string::npos); - REQUIRE(sourceAddOutput.str().find("Agreement Text") != std::string::npos); - REQUIRE(sourceAddOutput.str().find("https://test") != std::string::npos); - REQUIRE(sourceAddOutput.str().find(Resource::LocString(Resource::String::SourceAgreementsMarketMessage).get()) != std::string::npos); + InstallCommand install({}); + install.Execute(context); + INFO(installOutput.str()); - // Verify Installer is called. - REQUIRE(context.GetTerminationHR() == S_OK); + // Verify installer dependencies are shown + REQUIRE(installOutput.str().find(Resource::LocString(Resource::String::InstallAndUpgradeCommandsReportDependencies).get()) != std::string::npos); + REQUIRE(installOutput.str().find("PreviewIIS") != std::string::npos); + // and root dependencies are not + REQUIRE(installOutput.str().find("PreviewIISOnRoot") == std::string::npos); } -TEST_CASE("SourceAddFlow_Agreement_Prompt_No", "[SourceAddFlow][workflow]") +TEST_CASE("InstallFlow_Dependencies", "[InstallFlow][workflow][dependencies]") { - // Accept the agreements by saying "No" at the prompt - std::istringstream sourceAddInput{ "n" }; - std::ostringstream sourceAddOutput; - TestContext context{ sourceAddOutput, sourceAddInput }; - OverrideForSourceAddWithAgreements(context); - // This tests RemoveSource is called after agreement is not accepted. If they are not called, the test fails with unused override. - context.Override({ GetSourceListWithFilter, [](TestContext&) - { - } }); - context.Override({ RemoveSources, [](TestContext&) - { - } }); - context.Args.AddArg(Execution::Args::Type::SourceName, "TestSource"sv); - context.Args.AddArg(Execution::Args::Type::SourceType, "Microsoft.Test"sv); - context.Args.AddArg(Execution::Args::Type::SourceArg, "TestArg"sv); + TestCommon::TempFile installResultPath("TestExeInstalled.txt"); - SourceAddCommand sourceAdd({}); - sourceAdd.Execute(context); - INFO(sourceAddOutput.str()); + std::ostringstream installOutput; + TestContext context{ installOutput, std::cin }; + OverrideForShellExecute(context); + OverrideDependencySource(context); - // Verify agreements are shown - REQUIRE(sourceAddOutput.str().find("Agreement Label") != std::string::npos); - REQUIRE(sourceAddOutput.str().find("Agreement Text") != std::string::npos); - REQUIRE(sourceAddOutput.str().find("https://test") != std::string::npos); - REQUIRE(sourceAddOutput.str().find(Resource::LocString(Resource::String::SourceAgreementsMarketMessage).get()) != std::string::npos); + context.Args.AddArg(Execution::Args::Type::Manifest, TestDataFile("Installer_Exe_Dependencies.yaml").GetPath().u8string()); - // Verify Installer is called. - REQUIRE(context.GetTerminationHR() == APPINSTALLER_CLI_ERROR_SOURCE_AGREEMENTS_NOT_ACCEPTED); + TestUserSettings settings; + settings.Set<AppInstaller::Settings::Setting::EFDependencies>({ true }); + + InstallCommand install({}); + install.Execute(context); + INFO(installOutput.str()); + + // Verify all types of dependencies are printed + REQUIRE(installOutput.str().find(Resource::LocString(Resource::String::InstallAndUpgradeCommandsReportDependencies).get()) != std::string::npos); + REQUIRE(installOutput.str().find("PreviewIIS") != std::string::npos); } TEST_CASE("OpenSource_WithCustomHeader", "[OpenSource][CustomHeader]") @@ -2203,7 +2385,7 @@ TEST_CASE("OpenSource_WithCustomHeader", "[OpenSource][CustomHeader]") context.Args.AddArg(Execution::Args::Type::CustomHeader, customHeader2); context.Args.AddArg(Execution::Args::Type::Source, details.Name); - OpenSource(context); + AppInstaller::CLI::Workflow::OpenSource()(context); auto source = context.Get<Execution::Data::Source>(); REQUIRE(source.get()->GetDetails().CustomHeader.value_or("").compare(customHeader2) == 0); } @@ -2247,4 +2429,4 @@ TEST_CASE("AdminSetting_LocalManifestFiles", "[LocalManifests][workflow]") args3.AddArg(Execution::Args::Type::Manifest, TestDataFile("InstallFlowTest_Exe.yaml").GetPath().u8string()); InstallCommand installCommand3({}); REQUIRE_THROWS(installCommand3.ValidateArguments(args3)); -}- \ No newline at end of file +} diff --git a/src/AppInstallerCommonCore/AppInstallerCommonCore.vcxproj b/src/AppInstallerCommonCore/AppInstallerCommonCore.vcxproj @@ -272,6 +272,7 @@ <ItemGroup> <ClInclude Include="DODownloader.h" /> <ClInclude Include="Public\winget\AdminSettings.h" /> + <ClInclude Include="Public\winget\DependenciesGraph.h" /> <ClInclude Include="Public\winget\GroupPolicy.h" /> <ClInclude Include="HttpStream\HttpClientWrapper.h" /> <ClInclude Include="HttpStream\HttpLocalCache.h" /> @@ -324,6 +325,7 @@ </ItemGroup> <ItemGroup> <ClCompile Include="AdminSettings.cpp" /> + <ClCompile Include="DependenciesGraph.cpp" /> <ClCompile Include="DODownloader.cpp" /> <ClCompile Include="GroupPolicy.cpp"> <ExcludedFromBuild Condition="'$(Configuration)'=='Fuzzing'">true</ExcludedFromBuild> diff --git a/src/AppInstallerCommonCore/AppInstallerCommonCore.vcxproj.filters b/src/AppInstallerCommonCore/AppInstallerCommonCore.vcxproj.filters @@ -183,6 +183,9 @@ <ClInclude Include="Public\winget\MsiExecArguments.h"> <Filter>Public\winget</Filter> </ClInclude> + <ClInclude Include="Public\winget\DependenciesGraph.h"> + <Filter>Public\winget</Filter> + </ClInclude> </ItemGroup> <ItemGroup> <ClCompile Include="pch.cpp"> @@ -314,6 +317,9 @@ <ClCompile Include="AdminSettings.cpp"> <Filter>Source Files</Filter> </ClCompile> + <ClCompile Include="DependenciesGraph.cpp"> + <Filter>Source Files</Filter> + </ClCompile> </ItemGroup> <ItemGroup> <None Include="PropertySheet.props" /> diff --git a/src/AppInstallerCommonCore/DependenciesGraph.cpp b/src/AppInstallerCommonCore/DependenciesGraph.cpp @@ -0,0 +1,134 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. +#include "pch.h" +#include "winget\DependenciesGraph.h" + +namespace AppInstaller::Manifest +{ + // this constructor was intented for use during installation flow (we already have installer dependencies and there's no need to search the source again) + DependencyGraph::DependencyGraph(const Dependency& root, const DependencyList& rootDependencies, + std::function<const DependencyList(const Dependency&)> infoFunction) : m_root(root), getDependencies(infoFunction) + { + m_adjacents[m_root] = std::set<Dependency>(); + m_toCheck = std::vector<Dependency>(); + rootDependencies.ApplyToType(DependencyType::Package, [&](Dependency dependency) + { + m_toCheck.push_back(dependency); + AddNode(dependency); + AddAdjacent(root, dependency); + }); + m_rootDependencyEvaluated = true; + } + + DependencyGraph::DependencyGraph(const Dependency& root, std::function<const DependencyList(const Dependency&)> infoFunction) : m_root(root), getDependencies(infoFunction) + { + m_adjacents[m_root] = std::set<Dependency>(); + m_toCheck = std::vector<Dependency>(); + } + + void DependencyGraph::BuildGraph() + { + if (!m_rootDependencyEvaluated) + { + const DependencyList& rootDependencies = getDependencies(m_root); + rootDependencies.ApplyToType(DependencyType::Package, [&](Dependency dependency) + { + m_toCheck.push_back(dependency); + AddNode(dependency); + AddAdjacent(m_root, dependency); + }); + m_rootDependencyEvaluated = true; + } + + if (m_toCheck.empty()) + { + return; + } + + for (unsigned int i = 0; i < m_toCheck.size(); ++i) + { + auto node = m_toCheck.at(i); + + const auto& nodeDependencies = getDependencies(node); + nodeDependencies.ApplyToType(DependencyType::Package, [&](Dependency dependency) + { + if (!HasNode(dependency)) + { + m_toCheck.push_back(dependency); + AddNode(dependency); + } + + AddAdjacent(node, dependency); + }); + } + + CheckForLoopsAndGetOrder(); + } + + void DependencyGraph::AddNode(const Dependency& node) + { + m_adjacents[node] = std::set<Dependency>(); + } + + void DependencyGraph::AddAdjacent(const Dependency& node, const Dependency& adjacent) + { + m_adjacents[node].emplace(adjacent); + } + + bool DependencyGraph::HasNode(const Dependency& dependency) + { + auto search = m_adjacents.find(dependency); + return search != m_adjacents.end(); + } + + bool DependencyGraph::HasLoop() + { + return m_HasLoop; + } + + void DependencyGraph::CheckForLoopsAndGetOrder() + { + m_installationOrder = std::vector<Dependency>(); + std::set<Dependency> visited; + m_HasLoop = HasLoopDFS(visited, m_root); + } + + std::vector<Dependency> DependencyGraph::GetInstallationOrder() + { + return m_installationOrder; + } + + // TODO make this function iterative + bool DependencyGraph::HasLoopDFS(std::set<Dependency> visited, const Dependency& node) + { + bool loop = false; + + visited.insert(node); + auto lAdjacents = m_adjacents.at(node); + for (const auto& adjacent : m_adjacents.at(node)) + { + auto search = visited.find(adjacent); + if (search == visited.end()) // if not found + { + if (HasLoopDFS(visited, adjacent)) + { + loop = true; + // didn't break the loop to have a complete order at the end (even if a loop exists) + } + } + else + { + loop = true; + // didn't break the loop to have a complete order at the end (even if a loop exists) + } + } + + // Adding to have an order even if a loop is present + if (std::find(m_installationOrder.begin(), m_installationOrder.end(), node) == m_installationOrder.end()) + { + m_installationOrder.push_back(node); + } + + return loop; + } +}+ \ No newline at end of file diff --git a/src/AppInstallerCommonCore/Manifest/ManifestCommon.cpp b/src/AppInstallerCommonCore/Manifest/ManifestCommon.cpp @@ -523,4 +523,110 @@ namespace AppInstaller::Manifest return {}; } } + + void DependencyList::Add(const Dependency& newDependency) + { + Dependency* existingDependency = this->HasDependency(newDependency); + + if (existingDependency != NULL) { + if (newDependency.MinVersion) + { + if (existingDependency->MinVersion) + { + const auto& newDependencyVersion = Utility::Version(newDependency.MinVersion.value()); + const auto& existingDependencyVersion = Utility::Version(existingDependency->MinVersion.value()); + if (newDependencyVersion > existingDependencyVersion) + { + existingDependency->MinVersion.value() = newDependencyVersion.ToString(); + } + } + else + { + existingDependency->MinVersion.value() = newDependency.MinVersion.value(); + } + } + } + else + { + m_dependencies.push_back(newDependency); + } + } + + void DependencyList::Add(const DependencyList& otherDependencyList) + { + for (const auto& dependency : otherDependencyList.m_dependencies) + { + this->Add(dependency); + } + } + + bool DependencyList::HasAny() const { return !m_dependencies.empty(); } + bool DependencyList::HasAnyOf(DependencyType type) const + { + for (const auto& dependency : m_dependencies) + { + if (dependency.Type == type) return true; + }; + return false; + } + + Dependency* DependencyList::HasDependency(const Dependency& dependencyToSearch) + { + for (auto& dependency : m_dependencies) { + if (dependency.Type == dependencyToSearch.Type && ICUCaseInsensitiveEquals(dependency.Id, dependencyToSearch.Id)) + { + return &dependency; + } + } + return nullptr; + } + + // for testing purposes + bool DependencyList::HasExactDependency(DependencyType type, string_t id, string_t minVersion) + { + for (const auto& dependency : m_dependencies) + { + if (dependency.Type == type && Utility::ICUCaseInsensitiveEquals(dependency.Id, id)) + { + if (dependency.MinVersion) { + if (dependency.MinVersion.value() == minVersion) + { + return true; + } + } + else { + return true; + } + } + } + return false; + } + + size_t DependencyList::Size() + { + return m_dependencies.size(); + } + + void DependencyList::ApplyToType(DependencyType type, std::function<void(const Dependency&)> func) const + { + for (const auto& dependency : m_dependencies) + { + if (dependency.Type == type) func(dependency); + } + } + + void DependencyList::ApplyToAll(std::function<void(const Dependency&)> func) const + { + for (const auto& dependency : m_dependencies) + { + func(dependency); + } + } + + bool DependencyList::Empty() const + { + return m_dependencies.empty(); + } + + void DependencyList::Clear() { m_dependencies.clear(); } } diff --git a/src/AppInstallerCommonCore/Manifest/ManifestYamlPopulator.cpp b/src/AppInstallerCommonCore/Manifest/ManifestYamlPopulator.cpp @@ -440,7 +440,7 @@ namespace AppInstaller::Manifest result = { { "PackageIdentifier", [this](const YAML::Node& value)->ValidationErrors { m_p_packageDependency->Id = Utility::Trim(value.as<std::string>()); return {}; } }, - { "MinimumVersion", [this](const YAML::Node& value)->ValidationErrors { m_p_packageDependency->MinVersion = Utility::Trim(value.as<std::string>()); return {}; } }, + { "MinimumVersion", [this](const YAML::Node& value)->ValidationErrors { m_p_packageDependency->MinVersion = Utility::Version(Utility::Trim(value.as<std::string>())); return {}; } }, }; } diff --git a/src/AppInstallerCommonCore/Public/AppInstallerErrors.h b/src/AppInstallerCommonCore/Public/AppInstallerErrors.h @@ -104,6 +104,7 @@ #define APPINSTALLER_CLI_ERROR_INSTALL_ALREADY_INSTALLED ((HRESULT)0x8A15010D) #define APPINSTALLER_CLI_ERROR_INSTALL_DOWNGRADE ((HRESULT)0x8A15010E) #define APPINSTALLER_CLI_ERROR_INSTALL_BLOCKED_BY_POLICY ((HRESULT)0x8A15010F) +#define APPINSTALLER_CLI_ERROR_INSTALL_DEPENDENCIES ((HRESULT)0x8A150110) namespace AppInstaller diff --git a/src/AppInstallerCommonCore/Public/winget/DependenciesGraph.h b/src/AppInstallerCommonCore/Public/winget/DependenciesGraph.h @@ -0,0 +1,42 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. +#pragma once +#include "winget/ManifestCommon.h" + +namespace AppInstaller::Manifest +{ + struct DependencyGraph + { + // this constructor was intented for use during installation flow (we already have installer dependencies and there's no need to search the source again) + DependencyGraph(const Dependency& root, const DependencyList& rootDependencies, + std::function<const DependencyList(const Dependency&)> infoFunction); + + DependencyGraph(const Dependency& root, std::function<const DependencyList(const Dependency&)> infoFunction); + + void BuildGraph(); + + void AddNode(const Dependency& node); + + void AddAdjacent(const Dependency& node, const Dependency& adjacent); + + bool HasNode(const Dependency& dependency); + + bool HasLoop(); + + void CheckForLoopsAndGetOrder(); + + std::vector<Dependency> GetInstallationOrder(); + + private: + // TODO make this function iterative + bool HasLoopDFS(std::set<Dependency> visited, const Dependency& node); + + const Dependency& m_root; + std::map<Dependency, std::set<Dependency>> m_adjacents; + std::function<const DependencyList(const Dependency&)> getDependencies; + bool m_HasLoop; + bool m_rootDependencyEvaluated = false; + std::vector<Dependency> m_installationOrder; + std::vector<Dependency> m_toCheck; + }; +} diff --git a/src/AppInstallerCommonCore/Public/winget/ManifestCommon.h b/src/AppInstallerCommonCore/Public/winget/ManifestCommon.h @@ -5,6 +5,7 @@ #include <AppInstallerVersions.h> #include <functional> #include <map> +#include <set> #include <string_view> namespace AppInstaller::Manifest @@ -53,7 +54,7 @@ namespace AppInstaller::Manifest bool HasExtension() const; - bool HasExtension(std::string_view extension) const; +bool HasExtension(std::string_view extension) const; private: std::vector<Version> m_extensions; @@ -171,11 +172,43 @@ namespace AppInstaller::Manifest { DependencyType Type; string_t Id; - std::optional<string_t> MinVersion; + std::optional<Utility::Version> MinVersion; - Dependency(DependencyType type, string_t id, string_t minVersion) : Type(type), Id(std::move(id)), MinVersion(std::move(minVersion)) {} - Dependency(DependencyType type, string_t id) : Type(std::move(type)), Id(std::move(id)) {} + Dependency(DependencyType type, string_t id, string_t minVersion) : Type(type), Id(std::move(id)), MinVersion(Utility::Version(minVersion)) {} + Dependency(DependencyType type, string_t id) : Type(type), Id(std::move(id)) {} Dependency(DependencyType type) : Type(type) {} + + bool operator==(const Dependency& rhs) const { + return Type == rhs.Type && ICUCaseInsensitiveEquals(Id, rhs.Id) && MinVersion == rhs.MinVersion; + } + + bool operator <(const Dependency& rhs) const + { + return Id < rhs.Id; + } + + bool IsVersionOk(Utility::Version version) + { + return MinVersion <= Utility::Version(version); + } + }; + + struct DependencyList + { + void Add(const Dependency& newDependency); + void Add(const DependencyList& otherDependencyList); + bool HasAny() const; + bool HasAnyOf(DependencyType type) const; + Dependency* HasDependency(const Dependency& dependencyToSearch); + void ApplyToType(DependencyType type, std::function<void(const Dependency&)> func) const; + void ApplyToAll(std::function<void(const Dependency&)> func) const; + bool Empty() const; + void Clear(); + bool HasExactDependency(DependencyType type, string_t id, string_t minVersion = ""); + size_t Size(); + + private: + std::vector<Dependency> m_dependencies; }; struct AppsAndFeaturesEntry @@ -194,107 +227,6 @@ namespace AppInstaller::Manifest std::vector<string_t> ExcludedMarkets; }; - struct DependencyList - { - DependencyList() = default; - - void Add(const Dependency& newDependency) - { - Dependency* existingDependency = this->HasDependency(newDependency); - - if (existingDependency != NULL) { - if (newDependency.MinVersion) - { - if (existingDependency->MinVersion) - { - const auto& newDependencyVersion = AppInstaller::Utility::Version(newDependency.MinVersion.value()); - const auto& existingDependencyVersion = AppInstaller::Utility::Version(existingDependency->MinVersion.value()); - if (newDependencyVersion > existingDependencyVersion) - { - existingDependency->MinVersion.value() = newDependencyVersion.ToString(); - } - } - else - { - existingDependency->MinVersion.value() = newDependency.MinVersion.value(); - } - } - } - else - { - dependencies.push_back(newDependency); - } - } - - void Add(const DependencyList& otherDependencyList) - { - for (const auto& dependency : otherDependencyList.dependencies) - { - this->Add(dependency); - } - } - - bool HasAny() const { return !dependencies.empty(); } - bool HasAnyOf(DependencyType type) const - { - for (const auto& dependency : dependencies) - { - if (dependency.Type == type) return true; - }; - return false; - } - - Dependency* HasDependency(const Dependency& dependencyToSearch) - { - for (auto& dependency : dependencies) { - if (dependency.Type == dependencyToSearch.Type && ICUCaseInsensitiveEquals(dependency.Id, dependencyToSearch.Id)) - { - return &dependency; - } - } - return nullptr; - } - - // for testing purposes - bool HasExactDependency(DependencyType type, string_t id, string_t minVersion = "") - { - for (const auto& dependency : dependencies) - { - if (dependency.Type == type && Utility::ICUCaseInsensitiveEquals(dependency.Id, id)) - { - if (dependency.MinVersion) { - if (dependency.MinVersion.value() == minVersion) - { - return true; - } - } - else { - return true; - } - } - } - return false; - } - - size_t Size() - { - return dependencies.size(); - } - - void ApplyToType(DependencyType type, std::function<void(const Dependency&)> func) const - { - for (const auto& dependency : dependencies) - { - if (dependency.Type == type) func(dependency); - } - } - - void Clear() { dependencies.clear(); } - - private: - std::vector<Dependency> dependencies; - }; - InstallerTypeEnum ConvertToInstallerTypeEnum(const std::string& in); UpdateBehaviorEnum ConvertToUpdateBehaviorEnum(const std::string& in); diff --git a/src/AppInstallerRepositoryCore/AppInstallerRepositoryCore.vcxproj b/src/AppInstallerRepositoryCore/AppInstallerRepositoryCore.vcxproj @@ -230,6 +230,7 @@ </ItemDefinitionGroup> <ItemGroup> <ClInclude Include="CompositeSource.h" /> + <ClInclude Include="DependenciesTable.h" /> <ClInclude Include="ICU\SQLiteICU.h" /> <ClInclude Include="Microsoft\ARPHelper.h" /> <ClInclude Include="Microsoft\PredefinedInstalledSourceFactory.h" /> diff --git a/src/AppInstallerRepositoryCore/AppInstallerRepositoryCore.vcxproj.filters b/src/AppInstallerRepositoryCore/AppInstallerRepositoryCore.vcxproj.filters @@ -216,6 +216,9 @@ <ClInclude Include="Microsoft\Schema\1_3\HashVirtualTable.h"> <Filter>Microsoft\Schema\1_3</Filter> </ClInclude> + <ClInclude Include="DependenciesTable.h"> + <Filter>Microsoft\Schema\1_3</Filter> + </ClInclude> <ClInclude Include="SourceList.h"> <Filter>Header Files</Filter> </ClInclude> diff --git a/src/AppInstallerRepositoryCore/Microsoft/SQLiteIndexSource.cpp b/src/AppInstallerRepositoryCore/Microsoft/SQLiteIndexSource.cpp @@ -16,25 +16,25 @@ namespace AppInstaller::Repository::Microsoft // The base for the package objects. struct SourceReference { - SourceReference(const std::shared_ptr<const SQLiteIndexSource>& source) : + SourceReference(const std::shared_ptr<SQLiteIndexSource>& source) : m_source(source) {} protected: - std::shared_ptr<const SQLiteIndexSource> GetReferenceSource() const + std::shared_ptr<SQLiteIndexSource> GetReferenceSource() const { - std::shared_ptr<const SQLiteIndexSource> source = m_source.lock(); + std::shared_ptr<SQLiteIndexSource> source = m_source.lock(); THROW_HR_IF(E_NOT_VALID_STATE, !source); return source; } private: - std::weak_ptr<const SQLiteIndexSource> m_source; + std::weak_ptr<SQLiteIndexSource> m_source; }; // The IPackageVersion impl for SQLiteIndexSource. struct PackageVersion : public SourceReference, public IPackageVersion { - PackageVersion(const std::shared_ptr<const SQLiteIndexSource>& source, SQLiteIndex::IdType manifestId) : + PackageVersion(const std::shared_ptr<SQLiteIndexSource>& source, SQLiteIndex::IdType manifestId) : SourceReference(source), m_manifestId(manifestId) {} // Inherited via IPackageVersion @@ -67,7 +67,7 @@ namespace AppInstaller::Repository::Microsoft Manifest::Manifest GetManifest() override { - std::shared_ptr<const SQLiteIndexSource> source = GetReferenceSource(); + std::shared_ptr<SQLiteIndexSource> source = GetReferenceSource(); std::optional<std::string> relativePathOpt = source->GetIndex().GetPropertyByManifestId(m_manifestId, PackageVersionProperty::RelativePath); THROW_HR_IF(E_NOT_SET, !relativePathOpt); @@ -178,7 +178,7 @@ namespace AppInstaller::Repository::Microsoft // The base for IPackage implementations here. struct PackageBase : public SourceReference { - PackageBase(const std::shared_ptr<const SQLiteIndexSource>& source, SQLiteIndex::IdType idId) : + PackageBase(const std::shared_ptr<SQLiteIndexSource>& source, SQLiteIndex::IdType idId) : SourceReference(source), m_idId(idId) {} Utility::LocIndString GetProperty(PackageProperty property) const @@ -210,7 +210,7 @@ namespace AppInstaller::Repository::Microsoft protected: std::shared_ptr<IPackageVersion> GetLatestVersionInternal() const { - std::shared_ptr<const SQLiteIndexSource> source = GetReferenceSource(); + std::shared_ptr<SQLiteIndexSource> source = GetReferenceSource(); std::optional<SQLiteIndex::IdType> manifestId = source->GetIndex().GetManifestIdByKey(m_idId, {}, {}); if (manifestId) @@ -242,7 +242,7 @@ namespace AppInstaller::Repository::Microsoft std::vector<PackageVersionKey> GetAvailableVersionKeys() const override { - std::shared_ptr<const SQLiteIndexSource> source = GetReferenceSource(); + std::shared_ptr<SQLiteIndexSource> source = GetReferenceSource(); std::vector<Utility::VersionAndChannel> versions = source->GetIndex().GetVersionKeysById(m_idId); std::vector<PackageVersionKey> result; @@ -260,7 +260,7 @@ namespace AppInstaller::Repository::Microsoft std::shared_ptr<IPackageVersion> GetAvailableVersion(const PackageVersionKey& versionKey) const override { - std::shared_ptr<const SQLiteIndexSource> source = GetReferenceSource(); + std::shared_ptr<SQLiteIndexSource> source = GetReferenceSource(); // Ensure that this key targets this (or any) source if (!versionKey.SourceId.empty() && versionKey.SourceId != source->GetIdentifier()) @@ -367,7 +367,7 @@ namespace AppInstaller::Repository::Microsoft auto indexResults = m_index.Search(request); SearchResult result; - std::shared_ptr<const SQLiteIndexSource> sharedThis = shared_from_this(); + std::shared_ptr<SQLiteIndexSource> sharedThis = const_cast<SQLiteIndexSource*>(this)->shared_from_this(); for (auto& indexResult : indexResults.Matches) { std::unique_ptr<IPackage> package; diff --git a/src/AppInstallerRepositoryCore/Rest/RestSource.cpp b/src/AppInstallerRepositoryCore/Rest/RestSource.cpp @@ -14,25 +14,25 @@ namespace AppInstaller::Repository::Rest // The source reference used by package objects. struct SourceReference { - SourceReference(const std::shared_ptr<const RestSource>& source) : + SourceReference(const std::shared_ptr<RestSource>& source) : m_source(source) {} protected: - std::shared_ptr<const RestSource> GetReferenceSource() const + std::shared_ptr<RestSource> GetReferenceSource() const { - std::shared_ptr<const RestSource> source = m_source.lock(); + std::shared_ptr<RestSource> source = m_source.lock(); THROW_HR_IF(E_NOT_VALID_STATE, !source); return source; } private: - std::weak_ptr<const RestSource> m_source; + std::weak_ptr<RestSource> m_source; }; // The IPackage implementation for Available packages from RestSource. struct AvailablePackage : public std::enable_shared_from_this<AvailablePackage>, public SourceReference, public IPackage { - AvailablePackage(const std::shared_ptr<const RestSource>& source, IRestClient::Package&& package) : + AvailablePackage(const std::shared_ptr<RestSource>& source, IRestClient::Package&& package) : SourceReference(source), m_package(std::move(package)) { SortVersionsInternal(); @@ -181,7 +181,7 @@ namespace AppInstaller::Repository::Rest struct PackageVersion : public SourceReference, public IPackageVersion { PackageVersion( - const std::shared_ptr<const RestSource>& source, std::shared_ptr<AvailablePackage>&& package, IRestClient::VersionInfo versionInfo) + const std::shared_ptr<RestSource>& source, std::shared_ptr<AvailablePackage>&& package, IRestClient::VersionInfo versionInfo) : SourceReference(source), m_package(std::move(package)), m_versionInfo(std::move(versionInfo)) {} // Inherited via IPackageVersion @@ -322,7 +322,7 @@ namespace AppInstaller::Repository::Rest std::shared_ptr<IPackageVersion> AvailablePackage::GetAvailableVersion(const PackageVersionKey& versionKey) const { - std::shared_ptr<const RestSource> source = GetReferenceSource(); + std::shared_ptr<RestSource> source = GetReferenceSource(); std::scoped_lock versionsLock{ m_packageVersionsLock }; // Ensure that this key targets this (or any) source @@ -418,7 +418,7 @@ namespace AppInstaller::Repository::Rest IRestClient::SearchResult results = m_restClient.Search(request); SearchResult searchResult; - std::shared_ptr<const RestSource> sharedThis = shared_from_this(); + std::shared_ptr<RestSource> sharedThis = const_cast<RestSource*>(this)->shared_from_this(); for (auto& result : results.Matches) { std::shared_ptr<IPackage> package = std::make_shared<AvailablePackage>(sharedThis, std::move(result));