winget-cli

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

commit fe8ac77f4dc38385bad0e6117cfa5dc8a86bab82
parent 145c64cdff5833ba07cca40dee204dcf83b1cba5
Author: fzanollo <fzanollo@users.noreply.github.com>
Date:   Tue,  3 Aug 2021 18:09:04 -0300

Show dependencies feature #1012 (#1165)

* 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

* divide identity report and installation disclaimer from actual installation

Co-authored-by: JohnMcPMS <johnmcp@microsoft.com>
Diffstat:
M.github/actions/spelling/expect.txt | 2++
Mdoc/Settings.md | 9+++++++++
Msrc/AppInstallerCLICore/AppInstallerCLICore.vcxproj | 2++
Msrc/AppInstallerCLICore/AppInstallerCLICore.vcxproj.filters | 4++++
Msrc/AppInstallerCLICore/Commands/UninstallCommand.cpp | 3+++
Msrc/AppInstallerCLICore/Commands/UpgradeCommand.cpp | 12++++++++++--
Msrc/AppInstallerCLICore/Commands/ValidateCommand.cpp | 9++++++++-
Msrc/AppInstallerCLICore/ExecutionContextData.h | 7+++++++
Msrc/AppInstallerCLICore/ExecutionReporter.cpp | 1+
Msrc/AppInstallerCLICore/ExecutionReporter.h | 1+
Msrc/AppInstallerCLICore/Resources.h | 10+++++++++-
Asrc/AppInstallerCLICore/Workflows/DependenciesFlow.cpp | 89+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Asrc/AppInstallerCLICore/Workflows/DependenciesFlow.h | 42++++++++++++++++++++++++++++++++++++++++++
Msrc/AppInstallerCLICore/Workflows/InstallFlow.cpp | 69++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++---
Msrc/AppInstallerCLICore/Workflows/InstallFlow.h | 6++++++
Msrc/AppInstallerCLICore/Workflows/ShowFlow.cpp | 66++++++++++++++++++++++++++++++++++++++++++++++++++++--------------
Msrc/AppInstallerCLICore/Workflows/UpdateFlow.cpp | 7++++++-
Msrc/AppInstallerCLIPackage/Shared/Strings/en-us/winget.resw | 28++++++++++++++++++++++++++++
Msrc/AppInstallerCLITests/AppInstallerCLITests.vcxproj | 21+++++++++++++++++++++
Msrc/AppInstallerCLITests/AppInstallerCLITests.vcxproj.filters | 21+++++++++++++++++++++
Msrc/AppInstallerCLITests/RestInterface_1_0.cpp | 9++++-----
Asrc/AppInstallerCLITests/TestData/ImportFile-Good-Dependencies.json | 26++++++++++++++++++++++++++
Asrc/AppInstallerCLITests/TestData/Installer_Exe_Dependencies.yaml | 21+++++++++++++++++++++
Asrc/AppInstallerCLITests/TestData/Installer_Exe_DependenciesMultideclaration.yaml | 24++++++++++++++++++++++++
Asrc/AppInstallerCLITests/TestData/Installer_Exe_DependenciesOnRoot.yaml | 21+++++++++++++++++++++
Asrc/AppInstallerCLITests/TestData/Installer_Msi_WFDependency.yaml | 20++++++++++++++++++++
Asrc/AppInstallerCLITests/TestData/Manifest-Good-AllDependencyTypes.yaml | 29+++++++++++++++++++++++++++++
Asrc/AppInstallerCLITests/TestData/UpdateFlowTest_ExeDependencies.yaml | 27+++++++++++++++++++++++++++
Msrc/AppInstallerCLITests/WorkFlow.cpp | 200+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Msrc/AppInstallerCLITests/YamlManifest.cpp | 21++++++++++-----------
Msrc/AppInstallerCommonCore/ExperimentalFeature.cpp | 4++++
Msrc/AppInstallerCommonCore/Manifest/ManifestYamlPopulator.cpp | 34+++++++++++++++++++++++++---------
Msrc/AppInstallerCommonCore/Public/winget/ExperimentalFeature.h | 1+
Msrc/AppInstallerCommonCore/Public/winget/ManifestCommon.h | 121+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++------
Msrc/AppInstallerCommonCore/Public/winget/ManifestInstaller.h | 2+-
Msrc/AppInstallerCommonCore/Public/winget/ManifestYamlPopulator.h | 7++++---
Msrc/AppInstallerCommonCore/Public/winget/UserSettings.h | 2++
Msrc/AppInstallerCommonCore/UserSettings.cpp | 1+
Msrc/AppInstallerRepositoryCore/Rest/Schema/1_0/Json/ManifestDeserializer.cpp | 36+++++++++++++++++++++++++-----------
Msrc/AppInstallerRepositoryCore/Rest/Schema/1_0/Json/ManifestDeserializer.h | 2+-
40 files changed, 946 insertions(+), 71 deletions(-)

diff --git a/.github/actions/spelling/expect.txt b/.github/actions/spelling/expect.txt @@ -138,6 +138,7 @@ IHelp IHost IID IInstalled +IISOn img IName inet @@ -202,6 +203,7 @@ msftrubengu MSIHASH MSIXHASH msstore +Multideclaration multimap mx mycustom diff --git a/doc/Settings.md b/doc/Settings.md @@ -146,3 +146,12 @@ Support in WinGet for packaged callers is currently implemented as an experiment "packagedAPI": true }, ``` +### Dependencies + +Experimental feature with the aim of managing dependencies, as of now it only shows package dependency information. You can enable the feature as shown below. + +```json + "experimentalFeatures": { + "dependencies": true + }, +``` diff --git a/src/AppInstallerCLICore/AppInstallerCLICore.vcxproj b/src/AppInstallerCLICore/AppInstallerCLICore.vcxproj @@ -249,6 +249,7 @@ <ClInclude Include="Commands\ValidateCommand.h" /> <ClInclude Include="Commands\SettingsCommand.h" /> <ClInclude Include="CompletionData.h" /> + <ClInclude Include="Workflows\DependenciesFlow.h" /> <ClInclude Include="ExecutionArgs.h" /> <ClInclude Include="ExecutionContextData.h" /> <ClInclude Include="ExecutionContext.h" /> @@ -278,6 +279,7 @@ <ClCompile Include="COMContext.cpp" /> <ClCompile Include="Commands\COMInstallCommand.cpp" /> <ClCompile Include="Commands\ImportCommand.cpp" /> + <ClCompile Include="Workflows\DependenciesFlow.cpp" /> <ClCompile Include="PackageCollection.cpp" /> <ClCompile Include="Argument.cpp" /> <ClCompile Include="ChannelStreams.cpp" /> diff --git a/src/AppInstallerCLICore/AppInstallerCLICore.vcxproj.filters b/src/AppInstallerCLICore/AppInstallerCLICore.vcxproj.filters @@ -155,6 +155,8 @@ <ClInclude Include="COMContext.h"> <Filter>Public</Filter> </ClInclude> + <ClInclude Include="Workflows\DependenciesFlow.h"> + <Filter>Workflows</Filter> <ClInclude Include="Commands\COMInstallCommand.h"> <Filter>Commands</Filter> </ClInclude> @@ -280,6 +282,8 @@ <ClCompile Include="COMContext.cpp"> <Filter>Source Files</Filter> </ClCompile> + <ClCompile Include="Workflows\DependenciesFlow.cpp"> + <Filter>Source Files</Filter> <ClCompile Include="Commands\COMInstallCommand.cpp"> <Filter>Commands</Filter> </ClCompile> diff --git a/src/AppInstallerCLICore/Commands/UninstallCommand.cpp b/src/AppInstallerCLICore/Commands/UninstallCommand.cpp @@ -6,6 +6,7 @@ #include "Workflows/InstallFlow.h" #include "Workflows/CompletionFlow.h" #include "Workflows/WorkflowBase.h" +#include "Workflows/DependenciesFlow.h" #include "Resources.h" using AppInstaller::CLI::Execution::Args; @@ -128,6 +129,8 @@ namespace AppInstaller::CLI context << Workflow::GetInstalledPackageVersion << Workflow::GetUninstallInfo << + Workflow::GetDependenciesInfoForUninstall << + Workflow::ReportDependencies(Resource::String::UninstallCommandReportDependencies) << Workflow::ReportExecutionStage(ExecutionStage::Execution) << Workflow::ExecuteUninstaller << Workflow::ReportExecutionStage(ExecutionStage::PostExecution); diff --git a/src/AppInstallerCLICore/Commands/UpgradeCommand.cpp b/src/AppInstallerCLICore/Commands/UpgradeCommand.cpp @@ -6,6 +6,7 @@ #include "Workflows/InstallFlow.h" #include "Workflows/UpdateFlow.h" #include "Workflows/WorkflowBase.h" +#include "Workflows/DependenciesFlow.h" #include "Resources.h" using namespace AppInstaller::CLI::Execution; @@ -148,7 +149,10 @@ namespace AppInstaller::CLI GetInstalledPackageVersion << EnsureUpdateVersionApplicable << SelectInstaller << - EnsureApplicableInstaller << + EnsureApplicableInstaller << + ReportIdentityAndInstallationDisclaimer << + GetDependenciesFromInstaller << + ReportDependencies(Resource::String::InstallAndUpgradeCommandsReportDependencies) << InstallPackageInstaller; } else @@ -175,7 +179,11 @@ namespace AppInstaller::CLI context << SelectLatestApplicableUpdate(true); } - context << InstallPackageInstaller; + context << + ReportIdentityAndInstallationDisclaimer << + GetDependenciesFromInstaller << + ReportDependencies(Resource::String::InstallAndUpgradeCommandsReportDependencies) << + InstallPackageInstaller; } } } diff --git a/src/AppInstallerCLICore/Commands/ValidateCommand.cpp b/src/AppInstallerCLICore/Commands/ValidateCommand.cpp @@ -3,6 +3,7 @@ #include "pch.h" #include "ValidateCommand.h" #include "Workflows/WorkflowBase.h" +#include "Workflows/DependenciesFlow.h" #include "Resources.h" namespace AppInstaller::CLI @@ -41,7 +42,13 @@ namespace AppInstaller::CLI try { - (void)Manifest::YamlParser::CreateFromPath(inputFile, true, true); + auto manifest = Manifest::YamlParser::CreateFromPath(inputFile, true, true); + + context.Add<Execution::Data::Manifest>(manifest); + context << + Workflow::GetInstallersDependenciesFromManifest << + Workflow::ReportDependencies(Resource::String::ValidateCommandReportDependencies); + context.Reporter.Info() << Resource::String::ManifestValidationSuccess << std::endl; } catch (const Manifest::ManifestException& e) diff --git a/src/AppInstallerCLICore/ExecutionContextData.h b/src/AppInstallerCLICore/ExecutionContextData.h @@ -47,6 +47,7 @@ namespace AppInstaller::CLI::Execution // On import: Sources for the imported packages Sources, ARPSnapshot, + Dependencies, Max }; @@ -184,5 +185,11 @@ namespace AppInstaller::CLI::Execution // Contains the { Id, Version, Channel } using value_t = std::vector<std::tuple<Utility::LocIndString, Utility::LocIndString, Utility::LocIndString>>; }; + + template <> + struct DataMapping<Data::Dependencies> + { + using value_t = Manifest::DependencyList; + }; } } diff --git a/src/AppInstallerCLICore/ExecutionReporter.cpp b/src/AppInstallerCLICore/ExecutionReporter.cpp @@ -11,6 +11,7 @@ namespace AppInstaller::CLI::Execution const Sequence& HelpCommandEmphasis = TextFormat::Foreground::Bright; const Sequence& HelpArgumentEmphasis = TextFormat::Foreground::Bright; + const Sequence& ManifestInfoEmphasis = TextFormat::Foreground::Bright; const Sequence& NameEmphasis = TextFormat::Foreground::BrightCyan; const Sequence& IdEmphasis = TextFormat::Foreground::BrightCyan; const Sequence& UrlEmphasis = TextFormat::Foreground::BrightBlue; diff --git a/src/AppInstallerCLICore/ExecutionReporter.h b/src/AppInstallerCLICore/ExecutionReporter.h @@ -142,6 +142,7 @@ namespace AppInstaller::CLI::Execution // Indirection to enable change without tracking down every place extern const VirtualTerminal::Sequence& HelpCommandEmphasis; extern const VirtualTerminal::Sequence& HelpArgumentEmphasis; + extern const VirtualTerminal::Sequence& ManifestInfoEmphasis; extern const VirtualTerminal::Sequence& NameEmphasis; extern const VirtualTerminal::Sequence& IdEmphasis; extern const VirtualTerminal::Sequence& UrlEmphasis; diff --git a/src/AppInstallerCLICore/Resources.h b/src/AppInstallerCLICore/Resources.h @@ -46,6 +46,7 @@ namespace AppInstaller::CLI::Resource WINGET_DEFINE_RESOURCE_STRINGID(ExportCommandShortDescription); WINGET_DEFINE_RESOURCE_STRINGID(ExportIncludeVersionsArgumentDescription); WINGET_DEFINE_RESOURCE_STRINGID(ExportSourceArgumentDescription); + WINGET_DEFINE_RESOURCE_STRINGID(ExternalDependencies); WINGET_DEFINE_RESOURCE_STRINGID(ExtraPositionalError); WINGET_DEFINE_RESOURCE_STRINGID(FeatureDisabledMessage); WINGET_DEFINE_RESOURCE_STRINGID(FeaturesCommandLongDescription); @@ -68,6 +69,7 @@ namespace AppInstaller::CLI::Resource WINGET_DEFINE_RESOURCE_STRINGID(HelpLinkPreamble); WINGET_DEFINE_RESOURCE_STRINGID(IdArgumentDescription); WINGET_DEFINE_RESOURCE_STRINGID(ImportCommandLongDescription); + WINGET_DEFINE_RESOURCE_STRINGID(ImportCommandReportDependencies); WINGET_DEFINE_RESOURCE_STRINGID(ImportCommandShortDescription); WINGET_DEFINE_RESOURCE_STRINGID(ImportFileArgumentDescription); WINGET_DEFINE_RESOURCE_STRINGID(ImportFileHasInvalidSchema); @@ -82,6 +84,7 @@ namespace AppInstaller::CLI::Resource WINGET_DEFINE_RESOURCE_STRINGID(InstallationDisclaimerMSStore); WINGET_DEFINE_RESOURCE_STRINGID(InstallationRequiresHigherWindows); WINGET_DEFINE_RESOURCE_STRINGID(InstallCommandLongDescription); + WINGET_DEFINE_RESOURCE_STRINGID(InstallAndUpgradeCommandsReportDependencies); WINGET_DEFINE_RESOURCE_STRINGID(InstallCommandShortDescription); WINGET_DEFINE_RESOURCE_STRINGID(InstalledPackageNotAvailable); WINGET_DEFINE_RESOURCE_STRINGID(InstalledPackageVersionNotAvailable); @@ -147,6 +150,7 @@ namespace AppInstaller::CLI::Resource WINGET_DEFINE_RESOURCE_STRINGID(OutputFileArgumentDescription); WINGET_DEFINE_RESOURCE_STRINGID(OverrideArgumentDescription); WINGET_DEFINE_RESOURCE_STRINGID(Package); + WINGET_DEFINE_RESOURCE_STRINGID(PackageDependencies); WINGET_DEFINE_RESOURCE_STRINGID(PendingWorkError); WINGET_DEFINE_RESOURCE_STRINGID(PoliciesDisabled); WINGET_DEFINE_RESOURCE_STRINGID(PoliciesEnabled); @@ -170,8 +174,8 @@ namespace AppInstaller::CLI::Resource WINGET_DEFINE_RESOURCE_STRINGID(SettingLoadFailure); WINGET_DEFINE_RESOURCE_STRINGID(SettingsCommandLongDescription); WINGET_DEFINE_RESOURCE_STRINGID(SettingsCommandShortDescription); - WINGET_DEFINE_RESOURCE_STRINGID(SettingsWarnings); WINGET_DEFINE_RESOURCE_STRINGID(SettingsWarningField); + WINGET_DEFINE_RESOURCE_STRINGID(SettingsWarnings); WINGET_DEFINE_RESOURCE_STRINGID(SettingsWarningValue); WINGET_DEFINE_RESOURCE_STRINGID(ShowChannel); WINGET_DEFINE_RESOURCE_STRINGID(ShowCommandLongDescription); @@ -235,6 +239,7 @@ namespace AppInstaller::CLI::Resource WINGET_DEFINE_RESOURCE_STRINGID(UnexpectedErrorExecutingCommand); WINGET_DEFINE_RESOURCE_STRINGID(UninstallAbandoned); WINGET_DEFINE_RESOURCE_STRINGID(UninstallCommandLongDescription); + WINGET_DEFINE_RESOURCE_STRINGID(UninstallCommandReportDependencies); WINGET_DEFINE_RESOURCE_STRINGID(UninstallCommandShortDescription); WINGET_DEFINE_RESOURCE_STRINGID(UninstallFailedWithCode); WINGET_DEFINE_RESOURCE_STRINGID(UninstallFlowStartingPackageUninstall); @@ -246,6 +251,7 @@ namespace AppInstaller::CLI::Resource WINGET_DEFINE_RESOURCE_STRINGID(UpgradeCommandShortDescription); WINGET_DEFINE_RESOURCE_STRINGID(Usage); WINGET_DEFINE_RESOURCE_STRINGID(ValidateCommandLongDescription); + WINGET_DEFINE_RESOURCE_STRINGID(ValidateCommandReportDependencies); WINGET_DEFINE_RESOURCE_STRINGID(ValidateCommandShortDescription); WINGET_DEFINE_RESOURCE_STRINGID(ValidateManifestArgumentDescription); WINGET_DEFINE_RESOURCE_STRINGID(VerboseLogsArgumentDescription); @@ -255,6 +261,8 @@ namespace AppInstaller::CLI::Resource WINGET_DEFINE_RESOURCE_STRINGID(VerifyPathFailedNotExist); WINGET_DEFINE_RESOURCE_STRINGID(VersionArgumentDescription); WINGET_DEFINE_RESOURCE_STRINGID(VersionsArgumentDescription); + WINGET_DEFINE_RESOURCE_STRINGID(WindowsFeaturesDependencies); + WINGET_DEFINE_RESOURCE_STRINGID(WindowsLibrariesDependencies); WINGET_DEFINE_RESOURCE_STRINGID(WordArgumentDescription); }; diff --git a/src/AppInstallerCLICore/Workflows/DependenciesFlow.cpp b/src/AppInstallerCLICore/Workflows/DependenciesFlow.cpp @@ -0,0 +1,88 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +#include "pch.h" +#include "DependenciesFlow.h" + +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; }); + } + } + } + + void GetInstallersDependenciesFromManifest(Execution::Context& context) { + if (Settings::ExperimentalFeature::IsEnabled(Settings::ExperimentalFeature::Feature::Dependencies)) + { + const auto& manifest = context.Get<Execution::Data::Manifest>(); + Manifest::DependencyList allDependencies; + + for (const auto& installer : manifest.Installers) + { + allDependencies.Add(installer.Dependencies); + } + + context.Add<Execution::Data::Dependencies>(allDependencies); + } + } + + void GetDependenciesFromInstaller(Execution::Context& context) + { + if (Settings::ExperimentalFeature::IsEnabled(Settings::ExperimentalFeature::Feature::Dependencies)) + { + const auto& installer = context.Get<Execution::Data::Installer>(); + if (installer) + { + context.Add<Execution::Data::Dependencies>(installer->Dependencies); + } + } + } + + void GetDependenciesInfoForUninstall(Execution::Context& context) + { + 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 + } + } +}+ \ No newline at end of file diff --git a/src/AppInstallerCLICore/Workflows/DependenciesFlow.h b/src/AppInstallerCLICore/Workflows/DependenciesFlow.h @@ -0,0 +1,41 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. +#pragma once +#include "ExecutionContext.h" + +namespace AppInstaller::CLI::Workflow +{ + // Shows information about dependencies. + // Required Args: message to use at the beginning, before outputting dependencies + // Inputs: Dependencies + // Outputs: None + struct ReportDependencies : public WorkflowTask + { + ReportDependencies(AppInstaller::StringResource::StringId messageId) : + WorkflowTask("ReportDependencies"), m_messageId(messageId) {} + + void operator()(Execution::Context& context) const override; + + private: + AppInstaller::StringResource::StringId m_messageId; + }; + + // Gathers all installers dependencies from manifest. + // Required Args: None + // Inputs: Manifest + // Outputs: Dependencies + void GetInstallersDependenciesFromManifest(Execution::Context& context); + + // Gathers package dependencies information from installer. + // Required Args: None + // Inputs: Installer + // Outputs: Dependencies + void GetDependenciesFromInstaller(Execution::Context& context); + + // TODO: + // Gathers dependencies information for the uninstall command. + // Required Args: None + // Inputs: None + // Outputs: Dependencies + void GetDependenciesInfoForUninstall(Execution::Context& context); +}+ \ No newline at end of file diff --git a/src/AppInstallerCLICore/Workflows/InstallFlow.cpp b/src/AppInstallerCLICore/Workflows/InstallFlow.cpp @@ -7,6 +7,7 @@ #include "ShellExecuteInstallerHandler.h" #include "MSStoreInstallerHandler.h" #include "WorkflowBase.h" +#include "Workflows/DependenciesFlow.h" namespace AppInstaller::CLI::Workflow { @@ -387,11 +388,16 @@ namespace AppInstaller::CLI::Workflow } } - void InstallPackageInstaller(Execution::Context& context) + void ReportIdentityAndInstallationDisclaimer(Execution::Context& context) { context << Workflow::ReportManifestIdentity << - Workflow::ShowInstallationDisclaimer << + Workflow::ShowInstallationDisclaimer; + } + + void InstallPackageInstaller(Execution::Context& context) + { + context << Workflow::ReportExecutionStage(ExecutionStage::Download) << Workflow::DownloadInstaller << Workflow::ReportExecutionStage(ExecutionStage::PreExecution) << @@ -408,12 +414,27 @@ namespace AppInstaller::CLI::Workflow context << Workflow::SelectInstaller << Workflow::EnsureApplicableInstaller << + Workflow::ReportIdentityAndInstallationDisclaimer << + Workflow::GetDependenciesFromInstaller << + Workflow::ReportDependencies(Resource::String::InstallAndUpgradeCommandsReportDependencies) << Workflow::InstallPackageInstaller; } + const struct PackagesAndInstallers + { + PackagesAndInstallers(std::optional<AppInstaller::Manifest::ManifestInstaller> inst, + AppInstaller::CLI::Execution::PackageToInstall pkg) : Installer(inst), Package(pkg) {} + + std::optional<AppInstaller::Manifest::ManifestInstaller> Installer; + AppInstaller::CLI::Execution::PackageToInstall Package; + }; + void InstallMultiple(Execution::Context& context) { bool allSucceeded = true; + DependencyList allDependencies; + std::vector<PackagesAndInstallers> installers; + for (auto package : context.Get<Execution::Data::PackagesToInstall>()) { Logging::SubExecutionTelemetryScope subExecution; @@ -429,7 +450,49 @@ namespace AppInstaller::CLI::Workflow // TODO: In the future, it would be better to not have to convert back and forth from a string installContext.Args.AddArg(Execution::Args::Type::InstallScope, ScopeToString(package.PackageRequest.Scope)); - installContext << InstallPackageVersion; + installContext << + Workflow::SelectInstaller << + Workflow::EnsureApplicableInstaller; + + if (installContext.IsTerminated()) + { + allSucceeded = false; + continue; + } + + const auto& installer = installContext.Get<Execution::Data::Installer>(); + installers.push_back(PackagesAndInstallers(installer, package)); + + if (Settings::ExperimentalFeature::IsEnabled(Settings::ExperimentalFeature::Feature::Dependencies)) + { + if (installer) allDependencies.Add(installer->Dependencies); + } + } + + if (Settings::ExperimentalFeature::IsEnabled(Settings::ExperimentalFeature::Feature::Dependencies)) + { + context.Add<Execution::Data::Dependencies>(allDependencies); + context << Workflow::ReportDependencies(Resource::String::ImportCommandReportDependencies); + } + + for (auto packageAndInstaller : installers) + { + auto package = packageAndInstaller.Package; + auto installer = packageAndInstaller.Installer; + + auto installContextPtr = context.Clone(); + Execution::Context& installContext = *installContextPtr; + + // set data needed for installing + installContext.Add<Execution::Data::PackageVersion>(package.PackageVersion); + installContext.Add<Execution::Data::Manifest>(package.PackageVersion->GetManifest()); + installContext.Args.AddArg(Execution::Args::Type::InstallScope, ScopeToString(package.PackageRequest.Scope)); + installContext.Add<Execution::Data::Installer>(installer); + + installContext << + ReportIdentityAndInstallationDisclaimer << + Workflow::InstallPackageInstaller; + if (installContext.IsTerminated()) { if (context.IsTerminated() && context.GetTerminationHR() == E_ABORT) diff --git a/src/AppInstallerCLICore/Workflows/InstallFlow.h b/src/AppInstallerCLICore/Workflows/InstallFlow.h @@ -77,6 +77,12 @@ namespace AppInstaller::CLI::Workflow // Outputs: None void RemoveInstaller(Execution::Context& context); + // Reports manifest identity and shows installation disclaimer + // Required Args: None + // Inputs: Manifest + // Outputs: None + void ReportIdentityAndInstallationDisclaimer(Execution::Context& context); + // Installs a specific package installer. // Required Args: None // Inputs: Manifest, Installer diff --git a/src/AppInstallerCLICore/Workflows/ShowFlow.cpp b/src/AppInstallerCLICore/Workflows/ShowFlow.cpp @@ -16,16 +16,16 @@ namespace AppInstaller::CLI::Workflow const auto& installer = context.Get<Execution::Data::Installer>(); // TODO: Come up with a prettier format - context.Reporter.Info() << "Version: " << manifest.Version << std::endl; - context.Reporter.Info() << "Publisher: " << manifest.CurrentLocalization.Get<Manifest::Localization::Publisher>() << std::endl; + context.Reporter.Info() << Execution::ManifestInfoEmphasis << "PackageVersion: " << manifest.Version << std::endl; + context.Reporter.Info() << Execution::ManifestInfoEmphasis << "Publisher: " << manifest.CurrentLocalization.Get<Manifest::Localization::Publisher>() << std::endl; auto author = manifest.CurrentLocalization.Get<Manifest::Localization::Author>(); if (!author.empty()) { - context.Reporter.Info() << "Author: " << author << std::endl; + context.Reporter.Info() << Execution::ManifestInfoEmphasis << "Author: " << author << std::endl; } if (!manifest.Moniker.empty()) { - context.Reporter.Info() << "Moniker: " << manifest.Moniker << std::endl; + context.Reporter.Info() << Execution::ManifestInfoEmphasis << "Moniker: " << manifest.Moniker << std::endl; } auto description = manifest.CurrentLocalization.Get<Manifest::Localization::Description>(); if (description.empty()) @@ -35,39 +35,77 @@ namespace AppInstaller::CLI::Workflow } if (!description.empty()) { - context.Reporter.Info() << "Description: " << description << std::endl; + context.Reporter.Info() << Execution::ManifestInfoEmphasis << "Description: " << description << std::endl; } auto homepage = manifest.CurrentLocalization.Get<Manifest::Localization::PackageUrl>(); if (!homepage.empty()) { - context.Reporter.Info() << "Homepage: " << homepage << std::endl; + context.Reporter.Info() << Execution::ManifestInfoEmphasis << "PackageUrl: " << homepage << std::endl; } - context.Reporter.Info() << "License: " << manifest.CurrentLocalization.Get<Manifest::Localization::License>() << std::endl; + context.Reporter.Info() << Execution::ManifestInfoEmphasis << "License: " << manifest.CurrentLocalization.Get<Manifest::Localization::License>() << std::endl; auto licenseUrl = manifest.CurrentLocalization.Get<Manifest::Localization::LicenseUrl>(); if (!licenseUrl.empty()) { - context.Reporter.Info() << "License Url: " << licenseUrl << std::endl; + context.Reporter.Info() << Execution::ManifestInfoEmphasis << "LicenseUrl: " << licenseUrl << std::endl; } - context.Reporter.Info() << "Installer:" << std::endl; + context.Reporter.Info() << Execution::ManifestInfoEmphasis << "Installer:" << std::endl; if (installer) { - context.Reporter.Info() << " Type: " << Manifest::InstallerTypeToString(installer->InstallerType) << std::endl; + context.Reporter.Info() << Execution::ManifestInfoEmphasis << " InstallerType: " << Manifest::InstallerTypeToString(installer->InstallerType) << std::endl; if (!installer->Locale.empty()) { - context.Reporter.Info() << " Locale: " << installer->Locale << std::endl; + context.Reporter.Info() << Execution::ManifestInfoEmphasis << " InstallerLocale: " << installer->Locale << std::endl; } if (!installer->Url.empty()) { - context.Reporter.Info() << " Download Url: " << installer->Url << std::endl; + context.Reporter.Info() << Execution::ManifestInfoEmphasis << " InstallerUrl: " << installer->Url << std::endl; } if (!installer->Sha256.empty()) { - context.Reporter.Info() << " SHA256: " << Utility::SHA256::ConvertToString(installer->Sha256) << std::endl; + context.Reporter.Info() << Execution::ManifestInfoEmphasis << " InstallerSha256: " << Utility::SHA256::ConvertToString(installer->Sha256) << std::endl; } if (!installer->ProductId.empty()) { - context.Reporter.Info() << " Store Product Id: " << installer->ProductId << std::endl; + context.Reporter.Info() << Execution::ManifestInfoEmphasis << " ProductId: " << installer->ProductId << std::endl; + } + + if (Settings::ExperimentalFeature::IsEnabled(Settings::ExperimentalFeature::Feature::Dependencies)) { + auto info = context.Reporter.Info(); + const auto& dependencies = installer->Dependencies; + + if (dependencies.HasAny()) + { + info << Execution::ManifestInfoEmphasis << " Dependencies: " << std::endl; + + if (dependencies.HasAnyOf(Manifest::DependencyType::WindowsFeature)) + { + info << " - WindowsFeatures: " << std::endl; + dependencies.ApplyToType(Manifest::DependencyType::WindowsFeature, [&info](Manifest::Dependency dependency) {info << " " << dependency.Id << std::endl; }); + } + + if (dependencies.HasAnyOf(Manifest::DependencyType::WindowsLibrary)) + { + info << " - WindowsLibraries: " << std::endl; + dependencies.ApplyToType(Manifest::DependencyType::WindowsLibrary, [&info](Manifest::Dependency dependency) {info << " " << dependency.Id << std::endl; }); + } + + if (dependencies.HasAnyOf(Manifest::DependencyType::Package)) + { + info << " - 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 << " - ExternalDependencies: " << std::endl; + dependencies.ApplyToType(Manifest::DependencyType::External, [&info](Manifest::Dependency dependency) {info << " " << dependency.Id << std::endl; }); + } + } } } else diff --git a/src/AppInstallerCLICore/Workflows/UpdateFlow.cpp b/src/AppInstallerCLICore/Workflows/UpdateFlow.cpp @@ -3,6 +3,7 @@ #include "pch.h" #include "WorkflowBase.h" +#include "DependenciesFlow.h" #include "InstallFlow.h" #include "UpdateFlow.h" #include "ManifestComparator.h" @@ -111,7 +112,11 @@ namespace AppInstaller::CLI::Workflow updateAllFoundUpdate = true; - updateContext << InstallPackageInstaller; + updateContext << + ReportIdentityAndInstallationDisclaimer << + GetDependenciesFromInstaller << + ReportDependencies(Resource::String::InstallAndUpgradeCommandsReportDependencies) << + InstallPackageInstaller; updateContext.Reporter.Info() << std::endl; diff --git a/src/AppInstallerCLIPackage/Shared/Strings/en-us/winget.resw b/src/AppInstallerCLIPackage/Shared/Strings/en-us/winget.resw @@ -921,4 +921,32 @@ Configuration is disabled due to Group Policy.</value> <data name="Cancelled" xml:space="preserve"> <value>Cancelled</value> </data> + <data name="ExternalDependencies" xml:space="preserve"> + <value>External</value> + </data> + <data name="ImportCommandReportDependencies" xml:space="preserve"> + <value>The packages found in this import have the following dependencies:</value> + <comment>Import command sentence showed before reporting dependencies</comment> + </data> + <data name="InstallAndUpgradeCommandsReportDependencies" xml:space="preserve"> + <value>This package requires the following dependencies:</value> + <comment>Install and Upgrade commands sentence showed before reporting dependencies</comment> + </data> + <data name="PackageDependencies" xml:space="preserve"> + <value>Packages</value> + </data> + <data name="UninstallCommandReportDependencies" xml:space="preserve"> + <value>This package had dependencies that may not be needed anymore:</value> + <comment>Uninstall command sentence showed before reporting dependencies</comment> + </data> + <data name="ValidateCommandReportDependencies" xml:space="preserve"> + <value>Manifest has the following dependencies that were not validated; ensure that they are valid:</value> + <comment>Validate command sentence showed before reporting dependencies</comment> + </data> + <data name="WindowsFeaturesDependencies" xml:space="preserve"> + <value>Windows Features</value> + </data> + <data name="WindowsLibrariesDependencies" xml:space="preserve"> + <value>Windows Libraries</value> + </data> </root> \ No newline at end of file diff --git a/src/AppInstallerCLITests/AppInstallerCLITests.vcxproj b/src/AppInstallerCLITests/AppInstallerCLITests.vcxproj @@ -511,6 +511,27 @@ <CopyFileToFolders Include="TestData\Manifest-Bad-Channel-NotSupported.yaml"> <DeploymentContent>true</DeploymentContent> </CopyFileToFolders> + <CopyFileToFolders Include="TestData\Manifest-Good-AllDependencyTypes.yaml"> + <DeploymentContent>true</DeploymentContent> + </CopyFileToFolders> + <CopyFileToFolders Include="TestData\ImportFile-Good-Dependencies.json"> + <DeploymentContent>true</DeploymentContent> + </CopyFileToFolders> + <CopyFileToFolders Include="TestData\Installer_Exe_Dependencies.yaml"> + <DeploymentContent>true</DeploymentContent> + </CopyFileToFolders> + <CopyFileToFolders Include="TestData\Installer_Msi_WFDependency.yaml"> + <DeploymentContent>true</DeploymentContent> + </CopyFileToFolders> + <CopyFileToFolders Include="TestData\UpdateFlowTest_ExeDependencies.yaml"> + <DeploymentContent>true</DeploymentContent> + </CopyFileToFolders> + <CopyFileToFolders Include="TestData\Installer_Exe_DependenciesOnRoot.yaml"> + <DeploymentContent>true</DeploymentContent> + </CopyFileToFolders> + <CopyFileToFolders Include="TestData\Installer_Exe_DependenciesMultideclaration.yaml"> + <DeploymentContent>true</DeploymentContent> + </CopyFileToFolders> </ItemGroup> <ItemGroup> <ProjectReference Include="..\AppInstallerCLICore\AppInstallerCLICore.vcxproj"> diff --git a/src/AppInstallerCLITests/AppInstallerCLITests.vcxproj.filters b/src/AppInstallerCLITests/AppInstallerCLITests.vcxproj.filters @@ -450,5 +450,26 @@ <CopyFileToFolders Include="TestData\ImportFile-Good-MachineScope.json"> <Filter>TestData</Filter> </CopyFileToFolders> + <CopyFileToFolders Include="TestData\Manifest-Good-AllDependencyTypes.yaml"> + <Filter>TestData</Filter> + </CopyFileToFolders> + <CopyFileToFolders Include="TestData\ImportFile-Good-Dependencies.json"> + <Filter>TestData</Filter> + </CopyFileToFolders> + <CopyFileToFolders Include="TestData\Installer_Exe_Dependencies.yaml"> + <Filter>TestData</Filter> + </CopyFileToFolders> + <CopyFileToFolders Include="TestData\UpdateFlowTest_ExeDependencies.yaml"> + <Filter>TestData</Filter> + </CopyFileToFolders> + <CopyFileToFolders Include="TestData\Installer_Msi_WFDependency.yaml"> + <Filter>TestData</Filter> + </CopyFileToFolders> + <CopyFileToFolders Include="TestData\Installer_Exe_DependenciesOnRoot.yaml"> + <Filter>TestData</Filter> + </CopyFileToFolders> + <CopyFileToFolders Include="TestData\Installer_Exe_DependenciesMultideclaration.yaml"> + <Filter>TestData</Filter> + </CopyFileToFolders> </ItemGroup> </Project> \ No newline at end of file diff --git a/src/AppInstallerCLITests/RestInterface_1_0.cpp b/src/AppInstallerCLITests/RestInterface_1_0.cpp @@ -259,11 +259,10 @@ namespace REQUIRE(actualInstaller.Commands.at(0) == "command1"); REQUIRE(actualInstaller.Protocols.at(0) == "protocol1"); REQUIRE(actualInstaller.FileExtensions.at(0) == ".file-extension"); - REQUIRE(actualInstaller.Dependencies.WindowsFeatures.at(0) == "feature1"); - REQUIRE(actualInstaller.Dependencies.WindowsLibraries.at(0) == "library1"); - REQUIRE(actualInstaller.Dependencies.PackageDependencies.at(0).Id == "Foo.Baz"); - REQUIRE(actualInstaller.Dependencies.PackageDependencies.at(0).MinVersion == "2.0.0"); - REQUIRE(actualInstaller.Dependencies.ExternalDependencies.at(0) == "FooBarBaz"); + REQUIRE(actualInstaller.Dependencies.HasExactDependency(DependencyType::WindowsFeature, "feature1")); + REQUIRE(actualInstaller.Dependencies.HasExactDependency(DependencyType::WindowsLibrary, "library1")); + REQUIRE(actualInstaller.Dependencies.HasExactDependency(DependencyType::Package, "Foo.Baz", "2.0.0")); + REQUIRE(actualInstaller.Dependencies.HasExactDependency(DependencyType::External, "FooBarBaz")); REQUIRE(actualInstaller.PackageFamilyName == "FooBar.PackageFamilyName"); REQUIRE(actualInstaller.ProductCode == ""); REQUIRE(actualInstaller.Capabilities.at(0) == "Bluetooth"); diff --git a/src/AppInstallerCLITests/TestData/ImportFile-Good-Dependencies.json b/src/AppInstallerCLITests/TestData/ImportFile-Good-Dependencies.json @@ -0,0 +1,25 @@ +{ + "$schema": "https://aka.ms/winget-packages.schema.1.0.json", + "CreationDate": "2021-01-01T12:00:00.000", + "Sources": [ + { + "Packages": [ + { + "Id": "AppInstallerCliTest.TestExeInstaller.Dependencies", + "Version": "2.0.0.0" + }, + { + "Id": "AppInstallerCliTest.TestMsixInstaller.WFDep", + "Version": "1.0.0.0" + } + ], + "SourceDetails": { + "Argument": "//arg", + "Identifier": "*TestSource", + "Name": "TestSource", + "Type": "Microsoft.TestSource" + } + } + ], + "WinGetVersion": "1.0.0" +}+ \ No newline at end of file diff --git a/src/AppInstallerCLITests/TestData/Installer_Exe_Dependencies.yaml b/src/AppInstallerCLITests/TestData/Installer_Exe_Dependencies.yaml @@ -0,0 +1,21 @@ +PackageIdentifier: AppInstallerCliTest.TestExeInstaller.Dependencies +PackageVersion: 1.0.0.0 +PackageLocale: en-US +PackageName: AppInstaller Test Exe Installer With Package Dep +Publisher: Microsoft Corporation +Moniker: AICLITestExePackageDep +License: Test +ShortDescription: AppInstaller Test Exe Installer With Package Dep +Installers: + - Architecture: x64 + InstallerUrl: https://ThisIsNotUsed + InstallerType: exe + InstallerSha256: 65DB2F2AC2686C7F2FD69D4A4C6683B888DC55BFA20A0E32CA9F838B51689A3B + InstallerSwitches: + SilentWithProgress: /silentwithprogress + Silent: /silence + Dependencies: + WindowsFeatures: + - PreviewIIS +ManifestType: singleton +ManifestVersion: 1.0.0 diff --git a/src/AppInstallerCLITests/TestData/Installer_Exe_DependenciesMultideclaration.yaml b/src/AppInstallerCLITests/TestData/Installer_Exe_DependenciesMultideclaration.yaml @@ -0,0 +1,24 @@ +PackageIdentifier: AppInstallerCliTest.TestExeInstaller.Dependencies +PackageVersion: 1.0.0.0 +PackageLocale: en-US +PackageName: AppInstaller Test Exe Installer With Package Dep +Publisher: Microsoft Corporation +Moniker: AICLITestExePackageDep +License: Test +ShortDescription: AppInstaller Test Exe Installer With Package Dep +Dependencies: + WindowsFeatures: + - PreviewIISOnRoot +Installers: + - Architecture: x64 + InstallerUrl: https://ThisIsNotUsed + InstallerType: exe + InstallerSha256: 65DB2F2AC2686C7F2FD69D4A4C6683B888DC55BFA20A0E32CA9F838B51689A3B + InstallerSwitches: + SilentWithProgress: /silentwithprogress + Silent: /silence + Dependencies: + WindowsFeatures: + - PreviewIIS +ManifestType: singleton +ManifestVersion: 1.0.0 diff --git a/src/AppInstallerCLITests/TestData/Installer_Exe_DependenciesOnRoot.yaml b/src/AppInstallerCLITests/TestData/Installer_Exe_DependenciesOnRoot.yaml @@ -0,0 +1,21 @@ +PackageIdentifier: AppInstallerCliTest.TestExeInstaller.Dependencies +PackageVersion: 1.0.0.0 +PackageLocale: en-US +PackageName: AppInstaller Test Exe Installer With Package Dep +Publisher: Microsoft Corporation +Moniker: AICLITestExePackageDep +License: Test +ShortDescription: AppInstaller Test Exe Installer With Package Dep +Dependencies: + WindowsFeatures: + - PreviewIISOnRoot +Installers: + - Architecture: x64 + InstallerUrl: https://ThisIsNotUsed + InstallerType: exe + InstallerSha256: 65DB2F2AC2686C7F2FD69D4A4C6683B888DC55BFA20A0E32CA9F838B51689A3B + InstallerSwitches: + SilentWithProgress: /silentwithprogress + Silent: /silence +ManifestType: singleton +ManifestVersion: 1.0.0 diff --git a/src/AppInstallerCLITests/TestData/Installer_Msi_WFDependency.yaml b/src/AppInstallerCLITests/TestData/Installer_Msi_WFDependency.yaml @@ -0,0 +1,19 @@ +PackageIdentifier: AppInstallerCliTest.TestMsixInstaller.WFDep +PackageVersion: 1.0.0.0 +PackageLocale: en-US +PackageName: AppInstaller Test MSIX Installer With Windows Feature Dep +Publisher: Microsoft Corporation +Moniker: AICLITestMsixWindowsFeatureDep +License: Test +ShortDescription: AppInstaller Test MSIX Installer With Windows Feature Dep +Installers: + - Architecture: x64 + InstallerUrl: https://github.com/microsoft/msix-packaging/blob/master/src/test/testData/unpack/TestAppxPackage_x64.appx?raw=true + InstallerType: msix + InstallerSha256: 6a2d3683fa19bf00e58e07d1313d20a5f5735ebbd6a999d33381d28740ee07ea + PackageFamilyName: 20477fca-282d-49fb-b03e-371dca074f0f_8wekyb3d8bbwe + Dependencies: + WindowsFeatures: + - Hyper-V +ManifestType: singleton +ManifestVersion: 1.0.0+ \ No newline at end of file diff --git a/src/AppInstallerCLITests/TestData/Manifest-Good-AllDependencyTypes.yaml b/src/AppInstallerCLITests/TestData/Manifest-Good-AllDependencyTypes.yaml @@ -0,0 +1,29 @@ +# Installer with all types of dependencies +PackageIdentifier: AppInstallerCliTest.TestMsixInstaller +PackageVersion: 1.0.0.0 +PackageLocale: en-US +PackageName: AppInstaller Test MSIX Installer +ShortDescription: AppInstaller Test MSIX Installer +Publisher: Microsoft Corporation +Moniker: AICLITestMsix +License: Test +Installers: + - Architecture: x64 + InstallerUrl: https://github.com/microsoft/msix-packaging/blob/master/src/test/testData/unpack/TestAppxPackage_x64.appx?raw=true + InstallerType: msix + InstallerSha256: 6a2d3683fa19bf00e58e07d1313d20a5f5735ebbd6a999d33381d28740ee07ea + PackageFamilyName: 20477fca-282d-49fb-b03e-371dca074f0f_8wekyb3d8bbwe + Dependencies: + WindowsFeatures: + - WindowsFeaturesDep1 + - WindowsFeaturesDep2 + WindowsLibraries: + - WindowsLibrariesDep + PackageDependencies: + - PackageIdentifier: Package.Dep1-x64 + MinimumVersion: 1.0 + - PackageIdentifier: Package.Dep2-x64 + ExternalDependencies: + - ExternalDep +ManifestType: singleton +ManifestVersion: 1.0.0 diff --git a/src/AppInstallerCLITests/TestData/UpdateFlowTest_ExeDependencies.yaml b/src/AppInstallerCLITests/TestData/UpdateFlowTest_ExeDependencies.yaml @@ -0,0 +1,26 @@ +# Same content with Installer_Exe_Dependencies but with higher version +PackageIdentifier: AppInstallerCliTest.TestExeInstaller.Dependencies +PackageVersion: 2.0.0.0 +PackageName: AppInstaller Test Installer +PackageLocale: en-US +Publisher: Microsoft Corporation +ShortDescription: Upgrade exe installer with dependencies +Moniker: AICLITestExe +License: Test +InstallerSwitches: + Custom: /custom + SilentWithProgress: /silentwithprogress + Silent: /silence + Upgrade: /upgrade +Installers: + - Architecture: x64 + InstallerUrl: https://ThisIsNotUsed + InstallerType: exe + InstallerSha256: 65DB2F2AC2686C7F2FD69D4A4C6683B888DC55BFA20A0E32CA9F838B51689A3B + Dependencies: + WindowsFeatures: + - PreviewIIS + WindowsLibraries: + - Preview VC Runtime +ManifestType: singleton +ManifestVersion: 1.0.0+ \ No newline at end of file diff --git a/src/AppInstallerCLITests/WorkFlow.cpp b/src/AppInstallerCLITests/WorkFlow.cpp @@ -27,6 +27,7 @@ #include <winget/ManifestYamlParser.h> #include <Resources.h> #include <AppInstallerFileLogger.h> +#include <Commands/ValidateCommand.h> using namespace winrt::Windows::Foundation; using namespace winrt::Windows::Management::Deployment; @@ -200,6 +201,38 @@ namespace PackageMatchFilter(PackageMatchField::Id, MatchType::Exact, "AppInstallerCliTest.TestExeInstaller"))); } + if (input == "AppInstallerCliTest.TestExeInstaller.Dependencies") + { + auto manifest = YamlParser::CreateFromPath(TestDataFile("Installer_Exe_Dependencies.yaml")); + auto manifest2 = YamlParser::CreateFromPath(TestDataFile("UpdateFlowTest_ExeDependencies.yaml")); + result.Matches.emplace_back( + ResultMatch( + TestPackage::Make( + manifest, + TestPackage::MetadataMap + { + { PackageVersionMetadata::InstalledType, "Exe" }, + { PackageVersionMetadata::StandardUninstallCommand, "C:\\uninstall.exe" }, + { PackageVersionMetadata::SilentUninstallCommand, "C:\\uninstall.exe /silence" }, + }, + std::vector<Manifest>{ manifest2, manifest }, + this->shared_from_this() + ), + PackageMatchFilter(PackageMatchField::Id, MatchType::Exact, "AppInstallerCliTest.TestExeInstaller.Dependencies"))); + } + + if (input == "AppInstallerCliTest.TestMsixInstaller.WFDep") + { + auto manifest = YamlParser::CreateFromPath(TestDataFile("Installer_Msi_WFDependency.yaml")); + result.Matches.emplace_back( + ResultMatch( + TestPackage::Make( + std::vector<Manifest>{ manifest }, + this->shared_from_this() + ), + PackageMatchFilter(PackageMatchField::Id, MatchType::Exact, "AppInstallerCliTest.TestMsixInstaller.WFDep"))); + } + return result; } }; @@ -782,6 +815,30 @@ TEST_CASE("ShowFlow_SearchAndShowAppVersion", "[ShowFlow][workflow]") REQUIRE(showOutput.str().find(" Download Url: https://ThisIsNotUsed") == std::string::npos); } +TEST_CASE("ShowFlow_Dependencies", "[ShowFlow][workflow][dependencies]") +{ + std::ostringstream showOutput; + TestContext context{ showOutput, std::cin }; + context.Args.AddArg(Execution::Args::Type::Manifest, TestDataFile("Manifest-Good-AllDependencyTypes.yaml").GetPath().u8string()); + + TestUserSettings settings; + settings.Set<AppInstaller::Settings::Setting::EFDependencies>({true}); + + ShowCommand show({}); + show.Execute(context); + INFO(showOutput.str()); + + // Verify all types of dependencies are printed + REQUIRE(showOutput.str().find("Dependencies") != std::string::npos); + REQUIRE(showOutput.str().find("WindowsFeaturesDep") != std::string::npos); + REQUIRE(showOutput.str().find("WindowsLibrariesDep") != std::string::npos); + // PackageDep1 has minimum version (1.0), PackageDep2 doesn't (shouldn't show [>=...]) + REQUIRE(showOutput.str().find("Package.Dep1-x64 [>= 1.0]") != std::string::npos); + REQUIRE(showOutput.str().find("Package.Dep2-x64") != std::string::npos); + REQUIRE(showOutput.str().find("Package.Dep2-x64 [") == std::string::npos); + REQUIRE(showOutput.str().find("ExternalDep") != std::string::npos); +} + TEST_CASE("UpdateFlow_UpdateWithManifest", "[UpdateFlow][workflow]") { TestCommon::TempFile updateResultPath("TestExeInstalled.txt"); @@ -1036,6 +1093,31 @@ TEST_CASE("UpdateFlow_UpdateAllApplicable", "[UpdateFlow][workflow]") REQUIRE(std::filesystem::exists(updateMSStoreResultPath.GetPath())); } +TEST_CASE("UpdateFlow_Dependencies", "[UpdateFlow][workflow][dependencies]") +{ + TestCommon::TempFile updateResultPath("TestExeInstalled.txt"); + + std::ostringstream updateOutput; + TestContext context{ updateOutput, std::cin }; + OverrideForCompositeInstalledSource(context); + OverrideForShellExecute(context); + context.Args.AddArg(Execution::Args::Type::Query, "AppInstallerCliTest.TestExeInstaller.Dependencies"sv); + + TestUserSettings settings; + settings.Set<AppInstaller::Settings::Setting::EFDependencies>({ true }); + + UpgradeCommand update({}); + update.Execute(context); + INFO(updateOutput.str()); + + std::string updateResultStr = updateOutput.str(); + + // Verify dependencies are informed + REQUIRE(updateResultStr.find(Resource::LocString(Resource::String::InstallAndUpgradeCommandsReportDependencies).get()) != std::string::npos); + REQUIRE(updateResultStr.find("PreviewIIS") != std::string::npos); + REQUIRE(updateResultStr.find("Preview VC Runtime") != std::string::npos); +} + TEST_CASE("UninstallFlow_UninstallExe", "[UninstallFlow][workflow]") { TestCommon::TempFile uninstallResultPath("TestExeUninstalled.txt"); @@ -1381,6 +1463,32 @@ TEST_CASE("ImportFlow_MachineScope", "[ImportFlow][workflow]") REQUIRE(installResultStr.find("/scope=machine") != std::string::npos); } +TEST_CASE("ImportFlow_Dependencies", "[ImportFlow][workflow][dependencies]") +{ + TestCommon::TempFile exeInstallResultPath("TestExeInstalled.txt"); + TestCommon::TempFile msixInstallResultPath("TestMsixInstalled.txt"); + + std::ostringstream importOutput; + TestContext context{ importOutput, std::cin }; + OverrideForImportSource(context); + OverrideForMSIX(context); + OverrideForShellExecute(context); + context.Args.AddArg(Execution::Args::Type::ImportFile, TestDataFile("ImportFile-Good-Dependencies.json").GetPath().string()); + + TestUserSettings settings; + settings.Set<AppInstaller::Settings::Setting::EFDependencies>({ true }); + + ImportCommand importCommand({}); + importCommand.Execute(context); + INFO(importOutput.str()); + + // Verify dependencies for all packages are informed + REQUIRE(importOutput.str().find(Resource::LocString(Resource::String::ImportCommandReportDependencies).get()) != std::string::npos); + REQUIRE(importOutput.str().find("PreviewIIS") != std::string::npos); + REQUIRE(importOutput.str().find("Preview VC Runtime") != std::string::npos); + REQUIRE(importOutput.str().find("Hyper-V") != std::string::npos); +} + void VerifyMotw(const std::filesystem::path& testFile, DWORD zone) { std::filesystem::path motwFile(testFile); @@ -1520,4 +1628,96 @@ TEST_CASE("InstallFlowMultiLocale_PreferenceWithBetterLocale", "[InstallFlow][wo std::string installResultStr; std::getline(installResultFile, installResultStr); REQUIRE(installResultStr.find("/en-GB") != std::string::npos); +} + +TEST_CASE("InstallFlow_Dependencies", "[InstallFlow][workflow][dependencies]") +{ + TestCommon::TempFile installResultPath("TestExeInstalled.txt"); + + std::ostringstream installOutput; + TestContext context{ installOutput, std::cin }; + OverrideForShellExecute(context); + + context.Args.AddArg(Execution::Args::Type::Manifest, TestDataFile("Installer_Exe_Dependencies.yaml").GetPath().u8string()); + + 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("ValidateCommand_Dependencies", "[workflow][dependencies]") +{ + std::ostringstream validateOutput; + TestContext context{ validateOutput, std::cin }; + context.Args.AddArg(Execution::Args::Type::ValidateManifest, TestDataFile("Manifest-Good-AllDependencyTypes.yaml").GetPath().u8string()); + + TestUserSettings settings; + settings.Set<AppInstaller::Settings::Setting::EFDependencies>({ true }); + + ValidateCommand validate({}); + validate.Execute(context); + INFO(validateOutput.str()); + + // Verify all types of dependencies are printed + REQUIRE(validateOutput.str().find(Resource::LocString(Resource::String::ValidateCommandReportDependencies).get()) != std::string::npos); + REQUIRE(validateOutput.str().find("WindowsFeaturesDep") != std::string::npos); + REQUIRE(validateOutput.str().find("WindowsLibrariesDep") != std::string::npos); + // PackageDep1 has minimum version (1.0), PackageDep2 doesn't (shouldn't show [>=...]) + REQUIRE(validateOutput.str().find("Package.Dep1-x64 [>= 1.0]") != std::string::npos); + REQUIRE(validateOutput.str().find("Package.Dep2-x64") != std::string::npos); + REQUIRE(validateOutput.str().find("Package.Dep2-x64 [") == std::string::npos); + REQUIRE(validateOutput.str().find("ExternalDep") != std::string::npos); +} + +TEST_CASE("DependenciesMultideclaration_InstallerDependenciesPreference", "[dependencies]") +{ + TestCommon::TempFile installResultPath("TestExeInstalled.txt"); + + std::ostringstream installOutput; + TestContext context{ installOutput, std::cin }; + OverrideForShellExecute(context); + + context.Args.AddArg(Execution::Args::Type::Manifest, TestDataFile("Installer_Exe_DependenciesMultideclaration.yaml").GetPath().u8string()); + + TestUserSettings settings; + settings.Set<AppInstaller::Settings::Setting::EFDependencies>({ true }); + + InstallCommand install({}); + 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); +} + +TEST_CASE("InstallerWithoutDependencies_RootDependenciesAreUsed", "[dependencies]") +{ + TestCommon::TempFile installResultPath("TestExeInstalled.txt"); + + std::ostringstream installOutput; + TestContext context{ installOutput, std::cin }; + OverrideForShellExecute(context); + + context.Args.AddArg(Execution::Args::Type::Manifest, TestDataFile("Installer_Exe_DependenciesOnRoot.yaml").GetPath().u8string()); + + TestUserSettings settings; + settings.Set<AppInstaller::Settings::Setting::EFDependencies>({ true }); + + InstallCommand install({}); + install.Execute(context); + INFO(installOutput.str()); + + // Verify root dependencies are shown + REQUIRE(installOutput.str().find(Resource::LocString(Resource::String::InstallAndUpgradeCommandsReportDependencies).get()) != std::string::npos); + REQUIRE(installOutput.str().find("PreviewIISOnRoot") != std::string::npos); } \ No newline at end of file diff --git a/src/AppInstallerCLITests/YamlManifest.cpp b/src/AppInstallerCLITests/YamlManifest.cpp @@ -382,12 +382,11 @@ void VerifyV1ManifestContent(const Manifest& manifest, bool isSingleton) REQUIRE(manifest.DefaultInstallerInfo.FileExtensions == MultiValue{ "appx", "msix", "appxbundle", "msixbundle" }); auto dependencies = manifest.DefaultInstallerInfo.Dependencies; - REQUIRE(dependencies.WindowsFeatures == MultiValue{ "IIS" }); - REQUIRE(dependencies.WindowsLibraries == MultiValue{ "VC Runtime" }); - REQUIRE(dependencies.PackageDependencies.size() == 1); - REQUIRE(dependencies.PackageDependencies[0].Id == "Microsoft.MsixSdkDep"); - REQUIRE(dependencies.PackageDependencies[0].MinVersion == "1.0.0"); - REQUIRE(dependencies.ExternalDependencies == MultiValue{ "Outside dependencies" }); + REQUIRE(dependencies.HasExactDependency(DependencyType::WindowsFeature, "IIS")); + REQUIRE(dependencies.HasExactDependency(DependencyType::WindowsLibrary, "VC Runtime")); + REQUIRE(dependencies.HasExactDependency(DependencyType::Package, "Microsoft.MsixSdkDep", "1.0.0")); + REQUIRE(dependencies.HasExactDependency(DependencyType::External, "Outside dependencies")); + REQUIRE(dependencies.Size() == 4); REQUIRE(manifest.DefaultInstallerInfo.Capabilities == MultiValue{ "internetClient" }); REQUIRE(manifest.DefaultInstallerInfo.RestrictedCapabilities == MultiValue{ "runFullTrust" }); @@ -430,11 +429,11 @@ void VerifyV1ManifestContent(const Manifest& manifest, bool isSingleton) REQUIRE(installer1.FileExtensions == MultiValue{ "appxbundle", "msixbundle", "appx", "msix" }); auto installer1Dependencies = installer1.Dependencies; - REQUIRE(installer1Dependencies.WindowsFeatures == MultiValue{ "PreviewIIS" }); - REQUIRE(installer1Dependencies.WindowsLibraries == MultiValue{ "Preview VC Runtime" }); - REQUIRE(installer1Dependencies.PackageDependencies.size() == 1); - REQUIRE(installer1Dependencies.PackageDependencies[0].Id == "Microsoft.MsixSdkDepPreview"); - REQUIRE(installer1Dependencies.ExternalDependencies == MultiValue{ "Preview Outside dependencies" }); + REQUIRE(installer1Dependencies.HasExactDependency(DependencyType::WindowsFeature, "PreviewIIS")); + REQUIRE(installer1Dependencies.HasExactDependency(DependencyType::WindowsLibrary, "Preview VC Runtime")); + REQUIRE(installer1Dependencies.HasExactDependency(DependencyType::Package, "Microsoft.MsixSdkDepPreview")); + REQUIRE(installer1Dependencies.HasExactDependency(DependencyType::External, "Preview Outside dependencies")); + REQUIRE(installer1Dependencies.Size() == 4); REQUIRE(installer1.Capabilities == MultiValue{ "internetClientPreview" }); REQUIRE(installer1.RestrictedCapabilities == MultiValue{ "runFullTrustPreview" }); diff --git a/src/AppInstallerCommonCore/ExperimentalFeature.cpp b/src/AppInstallerCommonCore/ExperimentalFeature.cpp @@ -46,6 +46,8 @@ namespace AppInstaller::Settings return userSettings.Get<Setting::EFExperimentalMSStore>(); case ExperimentalFeature::Feature::PackagedAPI: return userSettings.Get<Setting::EFPackagedAPI>(); + case ExperimentalFeature::Feature::Dependencies: + return userSettings.Get<Setting::EFDependencies>(); default: THROW_HR(E_UNEXPECTED); } @@ -76,6 +78,8 @@ namespace AppInstaller::Settings return ExperimentalFeature{ "Microsoft Store Support", "experimentalMSStore", "https://aka.ms/winget-settings", Feature::ExperimentalMSStore }; case Feature::PackagedAPI: return ExperimentalFeature{ "Packaged API Support", "packagedAPI", "https://aka.ms/winget-settings", Feature::PackagedAPI }; + case Feature::Dependencies: + return ExperimentalFeature{ "Show Dependencies Information", "dependencies", "https://aka.ms/winget-settings", Feature::Dependencies }; default: THROW_HR(E_UNEXPECTED); } diff --git a/src/AppInstallerCommonCore/Manifest/ManifestYamlPopulator.cpp b/src/AppInstallerCommonCore/Manifest/ManifestYamlPopulator.cpp @@ -228,7 +228,7 @@ namespace AppInstaller::Manifest { "Commands", [this](const YAML::Node& value)->ValidationErrors { m_p_installer->Commands = ProcessStringSequenceNode(value); return {}; } }, { "Protocols", [this](const YAML::Node& value)->ValidationErrors { m_p_installer->Protocols = ProcessStringSequenceNode(value); return {}; } }, { "FileExtensions", [this](const YAML::Node& value)->ValidationErrors { m_p_installer->FileExtensions = ProcessStringSequenceNode(value); return {}; } }, - { "Dependencies", [this](const YAML::Node& value)->ValidationErrors { m_p_dependency = &(m_p_installer->Dependencies); return ValidateAndProcessFields(value, DependenciesFieldInfos); } }, + { "Dependencies", [this](const YAML::Node& value)->ValidationErrors { m_p_dependencyList = &(m_p_installer->Dependencies); return ValidateAndProcessFields(value, DependenciesFieldInfos); } }, { "Capabilities", [this](const YAML::Node& value)->ValidationErrors { m_p_installer->Capabilities = ProcessStringSequenceNode(value); return {}; } }, { "RestrictedCapabilities", [this](const YAML::Node& value)->ValidationErrors { m_p_installer->RestrictedCapabilities = ProcessStringSequenceNode(value); return {}; } }, }; @@ -355,16 +355,25 @@ namespace AppInstaller::Manifest { result = { - { "WindowsFeatures", [this](const YAML::Node& value)->ValidationErrors { m_p_dependency->WindowsFeatures = ProcessStringSequenceNode(value); return {}; } }, - { "WindowsLibraries", [this](const YAML::Node& value)->ValidationErrors { m_p_dependency->WindowsLibraries = ProcessStringSequenceNode(value); return {}; } }, - { "PackageDependencies", [this](const YAML::Node& value)->ValidationErrors { return ProcessPackageDependenciesNode(value, m_p_dependency->PackageDependencies); } }, - { "ExternalDependencies", [this](const YAML::Node& value)->ValidationErrors { m_p_dependency->ExternalDependencies = ProcessStringSequenceNode(value); return {}; } }, + { "WindowsFeatures", [this](const YAML::Node& value)->ValidationErrors { ProcessDependenciesNode(DependencyType::WindowsFeature, value); return {}; } }, + { "WindowsLibraries", [this](const YAML::Node& value)->ValidationErrors { ProcessDependenciesNode(DependencyType::WindowsLibrary, value); return {}; } }, + { "PackageDependencies", [this](const YAML::Node& value)->ValidationErrors { ProcessPackageDependenciesNode(value); return {}; } }, + { "ExternalDependencies", [this](const YAML::Node& value)->ValidationErrors { ProcessDependenciesNode(DependencyType::External, value); return {}; } }, }; } return result; } + void ManifestYamlPopulator::ProcessDependenciesNode(DependencyType type, const YAML::Node& node) + { + const auto& ids = ProcessStringSequenceNode(node); + for (auto id : ids) + { + m_p_dependencyList->Add(Dependency(type, id)); + } + } + std::vector<ManifestYamlPopulator::FieldProcessInfo> ManifestYamlPopulator::GetPackageDependenciesFieldProcessInfo(const ManifestVer& manifestVersion) { std::vector<FieldProcessInfo> result = {}; @@ -450,18 +459,17 @@ namespace AppInstaller::Manifest return resultErrors; } - ValidationErrors ManifestYamlPopulator::ProcessPackageDependenciesNode(const YAML::Node& rootNode, std::vector<PackageDependency>& packageDependencies) + ValidationErrors ManifestYamlPopulator::ProcessPackageDependenciesNode(const YAML::Node& rootNode) { ValidationErrors resultErrors; - packageDependencies.clear(); for (auto const& entry : rootNode.Sequence()) { - PackageDependency packageDependency; + Dependency packageDependency = Dependency(DependencyType::Package); m_p_packageDependency = &packageDependency; auto errors = ValidateAndProcessFields(entry, PackageDependenciesFieldInfos); std::move(errors.begin(), errors.end(), std::inserter(resultErrors, resultErrors.end())); - packageDependencies.emplace_back(std::move(std::move(packageDependency))); + m_p_dependencyList->Add(std::move(std::move(packageDependency))); } return resultErrors; @@ -502,6 +510,8 @@ namespace AppInstaller::Manifest // Clear these defaults as PackageFamilyName and ProductCode needs to be copied based on InstallerType installer.PackageFamilyName.clear(); installer.ProductCode.clear(); + // Clear dependencies as installer overrides root dependencies + installer.Dependencies.Clear(); m_p_installer = &installer; auto errors = ValidateAndProcessFields(entry, InstallerFieldInfos); @@ -518,6 +528,12 @@ namespace AppInstaller::Manifest installer.ProductCode = manifest.DefaultInstallerInfo.ProductCode; } + // If there are no dependencies on installer use default ones + if (!installer.Dependencies.HasAny()) + { + installer.Dependencies = manifest.DefaultInstallerInfo.Dependencies; + } + // Populate installer default switches if not exists auto defaultSwitches = GetDefaultKnownSwitches(installer.InstallerType); for (auto const& defaultSwitch : defaultSwitches) diff --git a/src/AppInstallerCommonCore/Public/winget/ExperimentalFeature.h b/src/AppInstallerCommonCore/Public/winget/ExperimentalFeature.h @@ -22,6 +22,7 @@ namespace AppInstaller::Settings None = 0x0, ExperimentalMSStore = 0x1, PackagedAPI = 0x2, + Dependencies = 0x4, Max, // This MUST always be after all experimental features // Features listed after Max will not be shown with the features command diff --git a/src/AppInstallerCommonCore/Public/winget/ManifestCommon.h b/src/AppInstallerCommonCore/Public/winget/ManifestCommon.h @@ -3,7 +3,7 @@ #pragma once #include <AppInstallerStrings.h> #include <AppInstallerVersions.h> - +#include <functional> #include <map> #include <string_view> @@ -112,20 +112,125 @@ namespace AppInstaller::Manifest Preview, }; - struct PackageDependency + enum class DependencyType { - string_t Id; - string_t MinVersion; + WindowsFeature, + WindowsLibrary, + Package, + External }; struct Dependency { - std::vector<string_t> WindowsFeatures; - std::vector<string_t> WindowsLibraries; - std::vector<PackageDependency> PackageDependencies; - std::vector<string_t> ExternalDependencies; + DependencyType Type; + string_t Id; + std::optional<string_t> 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) : Type(type) {} }; + 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); diff --git a/src/AppInstallerCommonCore/Public/winget/ManifestInstaller.h b/src/AppInstallerCommonCore/Public/winget/ManifestInstaller.h @@ -71,6 +71,6 @@ namespace AppInstaller::Manifest // For msix only std::vector<string_t> RestrictedCapabilities; - Dependency Dependencies; + DependencyList Dependencies; }; } \ No newline at end of file diff --git a/src/AppInstallerCommonCore/Public/winget/ManifestYamlPopulator.h b/src/AppInstallerCommonCore/Public/winget/ManifestYamlPopulator.h @@ -37,8 +37,8 @@ namespace AppInstaller::Manifest AppInstaller::Manifest::Manifest* m_p_manifest = nullptr; AppInstaller::Manifest::ManifestInstaller* m_p_installer = nullptr; std::map<InstallerSwitchType, Utility::NormalizedString>* m_p_switches = nullptr; - AppInstaller::Manifest::Dependency* m_p_dependency = nullptr; - AppInstaller::Manifest::PackageDependency* m_p_packageDependency = nullptr; + AppInstaller::Manifest::DependencyList* m_p_dependencyList = nullptr; + AppInstaller::Manifest::Dependency* m_p_packageDependency = nullptr; AppInstaller::Manifest::ManifestLocalization* m_p_localization = nullptr; // Cache of Installers node and Localization node @@ -60,7 +60,8 @@ namespace AppInstaller::Manifest const YAML::Node& rootNode, const std::vector<FieldProcessInfo>& fieldInfos); - std::vector<ValidationError> ProcessPackageDependenciesNode(const YAML::Node& rootNode, std::vector<PackageDependency>& packageDependencies); + void ProcessDependenciesNode(DependencyType type, const YAML::Node& rootNode); + std::vector<ValidationError> ProcessPackageDependenciesNode(const YAML::Node& rootNode); std::vector<ValidationError> PopulateManifestInternal(const YAML::Node& rootNode, Manifest& manifest, const ManifestVer& manifestVersion, bool fullValidation); }; diff --git a/src/AppInstallerCommonCore/Public/winget/UserSettings.h b/src/AppInstallerCommonCore/Public/winget/UserSettings.h @@ -68,6 +68,7 @@ namespace AppInstaller::Settings EFExperimentalCmd, EFExperimentalArg, EFExperimentalMSStore, + EFDependencies, TelemetryDisable, InstallScopePreference, InstallScopeRequirement, @@ -114,6 +115,7 @@ namespace AppInstaller::Settings SETTINGMAPPING_SPECIALIZATION(Setting::EFExperimentalCmd, bool, bool, false, ".experimentalFeatures.experimentalCmd"sv); SETTINGMAPPING_SPECIALIZATION(Setting::EFExperimentalArg, bool, bool, false, ".experimentalFeatures.experimentalArg"sv); SETTINGMAPPING_SPECIALIZATION(Setting::EFExperimentalMSStore, bool, bool, false, ".experimentalFeatures.experimentalMSStore"sv); + SETTINGMAPPING_SPECIALIZATION(Setting::EFDependencies, bool, bool, false, ".experimentalFeatures.dependencies"sv); SETTINGMAPPING_SPECIALIZATION(Setting::TelemetryDisable, bool, bool, false, ".telemetry.disable"sv); SETTINGMAPPING_SPECIALIZATION(Setting::InstallScopePreference, std::string, ScopePreference, ScopePreference::User, ".installBehavior.preferences.scope"sv); SETTINGMAPPING_SPECIALIZATION(Setting::InstallScopeRequirement, std::string, ScopePreference, ScopePreference::None, ".installBehavior.requirements.scope"sv); diff --git a/src/AppInstallerCommonCore/UserSettings.cpp b/src/AppInstallerCommonCore/UserSettings.cpp @@ -225,6 +225,7 @@ namespace AppInstaller::Settings WINGET_VALIDATE_PASS_THROUGH(EFExperimentalCmd) WINGET_VALIDATE_PASS_THROUGH(EFExperimentalArg) WINGET_VALIDATE_PASS_THROUGH(EFExperimentalMSStore) + WINGET_VALIDATE_PASS_THROUGH(EFDependencies) WINGET_VALIDATE_PASS_THROUGH(TelemetryDisable) WINGET_VALIDATE_PASS_THROUGH(EFPackagedAPI) diff --git a/src/AppInstallerRepositoryCore/Rest/Schema/1_0/Json/ManifestDeserializer.cpp b/src/AppInstallerRepositoryCore/Rest/Schema/1_0/Json/ManifestDeserializer.cpp @@ -413,10 +413,10 @@ namespace AppInstaller::Repository::Rest::Schema::V1_0::Json JsonHelper::GetJsonValueFromNode(installerJsonObject, JsonHelper::GetUtilityString(Dependencies)); if (dependenciesObject) { - std::optional<Manifest::Dependency> dependency = DeserializeDependency(dependenciesObject.value().get()); - if (dependency) + std::optional<Manifest::DependencyList> dependencyList = DeserializeDependency(dependenciesObject.value().get()); + if (dependencyList) { - installer.Dependencies = std::move(dependency.value()); + installer.Dependencies = std::move(dependencyList.value()); } } @@ -428,18 +428,32 @@ namespace AppInstaller::Repository::Rest::Schema::V1_0::Json return installer; } - std::optional<Manifest::Dependency> ManifestDeserializer::DeserializeDependency(const web::json::value& dependenciesObject) const + std::optional<Manifest::DependencyList> ManifestDeserializer::DeserializeDependency(const web::json::value& dependenciesObject) const { if (dependenciesObject.is_null()) { return {}; } - Manifest::Dependency dependency; + Manifest::DependencyList dependencyList; - dependency.WindowsFeatures = ConvertToManifestStringArray(JsonHelper::GetRawStringArrayFromJsonNode(dependenciesObject, JsonHelper::GetUtilityString(WindowsFeatures))); - dependency.WindowsLibraries = ConvertToManifestStringArray(JsonHelper::GetRawStringArrayFromJsonNode(dependenciesObject, JsonHelper::GetUtilityString(WindowsLibraries))); - dependency.ExternalDependencies = ConvertToManifestStringArray(JsonHelper::GetRawStringArrayFromJsonNode(dependenciesObject, JsonHelper::GetUtilityString(ExternalDependencies))); + auto wfIds = ConvertToManifestStringArray(JsonHelper::GetRawStringArrayFromJsonNode(dependenciesObject, JsonHelper::GetUtilityString(WindowsFeatures))); + for (auto&& id : wfIds) + { + dependencyList.Add(Dependency(DependencyType::WindowsFeature, std::move(id))); + }; + + const auto& wlIds = ConvertToManifestStringArray(JsonHelper::GetRawStringArrayFromJsonNode(dependenciesObject, JsonHelper::GetUtilityString(WindowsLibraries))); + for (auto id : wlIds) + { + dependencyList.Add(Dependency(DependencyType::WindowsLibrary, id)); + }; + + const auto& extIds = ConvertToManifestStringArray(JsonHelper::GetRawStringArrayFromJsonNode(dependenciesObject, JsonHelper::GetUtilityString(ExternalDependencies))); + for (auto id : extIds) + { + dependencyList.Add(Dependency(DependencyType::External, id)); + }; // Package Dependencies std::optional<std::reference_wrapper<const web::json::array>> packageDependencies = JsonHelper::GetRawJsonArrayFromJsonNode(dependenciesObject, JsonHelper::GetUtilityString(PackageDependencies)); @@ -450,12 +464,12 @@ namespace AppInstaller::Repository::Rest::Schema::V1_0::Json std::optional<std::string> id = JsonHelper::GetRawStringValueFromJsonNode(packageDependency, JsonHelper::GetUtilityString(PackageIdentifier)); if (id) { - PackageDependency pkg{ std::move(id.value()) , JsonHelper::GetRawStringValueFromJsonNode(packageDependency, JsonHelper::GetUtilityString(MinimumVersion)).value_or("") }; - dependency.PackageDependencies.emplace_back(std::move(pkg)); + Dependency pkg{ DependencyType::Package, std::move(id.value()) , JsonHelper::GetRawStringValueFromJsonNode(packageDependency, JsonHelper::GetUtilityString(MinimumVersion)).value_or("") }; + dependencyList.Add(std::move(pkg)); } } } - return dependency; + return dependencyList; } } diff --git a/src/AppInstallerRepositoryCore/Rest/Schema/1_0/Json/ManifestDeserializer.h b/src/AppInstallerRepositoryCore/Rest/Schema/1_0/Json/ManifestDeserializer.h @@ -19,6 +19,6 @@ namespace AppInstaller::Repository::Rest::Schema::V1_0::Json std::optional<Manifest::ManifestInstaller> DeserializeInstaller(const web::json::value& installerJsonObject) const; - std::optional<Manifest::Dependency> DeserializeDependency(const web::json::value& dependenciesJsonObject) const; + std::optional<Manifest::DependencyList> DeserializeDependency(const web::json::value& dependenciesJsonObject) const; }; }