commit 2537239ba22091803118de71ced0e5d4571be20b parent 1b816a3d31737ab703ce30c7da41f687ccf674a7 Author: Ryan <69221034+ryfu-msft@users.noreply.github.com> Date: Fri, 23 Jun 2023 09:28:06 -0700 Refresh process path variable when installing package dependencies (#3296) Diffstat:
36 files changed, 622 insertions(+), 42 deletions(-)
diff --git a/.github/actions/spelling/expect.txt b/.github/actions/spelling/expect.txt @@ -450,6 +450,7 @@ websites WERSJA wesome wfopen +wgetenv Whatif winapifamily windir @@ -462,6 +463,7 @@ wingetutil winreg winrtact withstarts +wputenv wsl wsv wto diff --git a/doc/Settings.md b/doc/Settings.md @@ -90,6 +90,15 @@ The `portablePackageMachineRoot` setting affects the default root directory wher }, ``` +### Skip Dependencies +The 'skipDependencies' behavior affects whether dependencies are installed for a given package. Defaults to 'false' if value is not set or is invalid. + +```json + "installBehavior": { + "skipDependencies": true + }, +``` + ### Preferences and Requirements Some of the settings are duplicated under `preferences` and `requirements`. `preferences` affect how the various available options are sorted when choosing the one to act on. For instance, the default scope of package installs is for the current user, but if that is not an option then a machine level installer will be chosen. `requirements` filter the options, potentially resulting in an empty list and a failure to install. In the previous example, a user scope requirement would result in no applicable installers and an error. diff --git a/schemas/JSON/settings/settings.schema.0.2.json b/schemas/JSON/settings/settings.schema.0.2.json @@ -102,15 +102,15 @@ "properties": { "preferences": { "$ref": "#/definitions/InstallPrefReq" }, "requirements": { "$ref": "#/definitions/InstallPrefReq" }, - "ignoreWarnings": { - "description": "Controls whether blocking warning messages shown to the user during an install or upgrade are ignored", + "skipDependencies": { + "description": "Controls whether package dependencies and Windows Features are skipped during installation", "type": "boolean", "default": false }, "disableInstallNotes": { "description": "Controls whether installation notes are shown after a successful install", "type": "boolean", - "default": false + "default": false }, "portablePackageUserRoot": { "description": "The default root directory where packages are installed to under User scope. Applies to the portable installer type.", diff --git a/src/AppInstallerCLICore/Argument.cpp b/src/AppInstallerCLICore/Argument.cpp @@ -87,6 +87,8 @@ namespace AppInstaller::CLI return { type, "rename"_liv, 'r' }; case Execution::Args::Type::NoUpgrade: return { type, "no-upgrade"_liv, ArgTypeCategory::CopyFlagToSubContext }; + case Execution::Args::Type::SkipDependencies: + return { type, "skip-dependencies"_liv, ArgTypeCategory::InstallerBehavior | ArgTypeCategory::CopyFlagToSubContext }; // Uninstall behavior case Execution::Args::Type::Purge: @@ -292,6 +294,8 @@ namespace AppInstaller::CLI return Argument{ type, Resource::String::VersionsArgumentDescription, ArgumentType::Flag }; case Args::Type::Help: return Argument{ type, Resource::String::HelpArgumentDescription, ArgumentType::Flag }; + case Args::Type::SkipDependencies: + return Argument{ type, Resource::String::SkipDependenciesArgumentDescription, ArgumentType::Flag, false }; case Args::Type::IgnoreLocalArchiveMalwareScan: return Argument{ type, Resource::String::IgnoreLocalArchiveMalwareScanArgumentDescription, ArgumentType::Flag, Settings::TogglePolicy::Policy::LocalArchiveMalwareScanOverride, Settings::AdminSetting::LocalArchiveMalwareScanOverride }; case Args::Type::SourceName: diff --git a/src/AppInstallerCLICore/Commands/COMCommand.cpp b/src/AppInstallerCLICore/Commands/COMCommand.cpp @@ -4,6 +4,7 @@ #include "COMCommand.h" #include "Workflows/DownloadFlow.h" #include "Workflows/InstallFlow.h" +#include "Workflows/PromptFlow.h" #include "Workflows/UninstallFlow.h" #include "Workflows/WorkflowBase.h" @@ -21,7 +22,10 @@ namespace AppInstaller::CLI Workflow::ReportExecutionStage(ExecutionStage::Discovery) << Workflow::SelectInstaller << Workflow::EnsureApplicableInstaller << - Workflow::DownloadSinglePackage; + Workflow::ReportIdentityAndInstallationDisclaimer << + Workflow::ShowPromptsForSinglePackage(/* ensureAcceptance */ true) << + Workflow::ManageDependencies << // TODO: Separate handling dependencies from download flow. + Workflow::DownloadInstaller; } // IMPORTANT: To use this command, the caller should have already executed the COMDownloadCommand 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::SkipDependencies), Argument::ForType(Args::Type::IgnoreLocalArchiveMalwareScan), Argument::ForType(Args::Type::DependencySource), Argument::ForType(Args::Type::AcceptPackageAgreements), diff --git a/src/AppInstallerCLICore/Commands/UpgradeCommand.cpp b/src/AppInstallerCLICore/Commands/UpgradeCommand.cpp @@ -59,6 +59,7 @@ namespace AppInstaller::CLI Argument::ForType(Args::Type::InstallArchitecture), // -a Argument::ForType(Args::Type::Locale), Argument::ForType(Args::Type::HashOverride), + Argument::ForType(Args::Type::SkipDependencies), Argument::ForType(Args::Type::IgnoreLocalArchiveMalwareScan), Argument::ForType(Args::Type::AcceptPackageAgreements), Argument::ForType(Args::Type::AcceptSourceAgreements), diff --git a/src/AppInstallerCLICore/ExecutionArgs.h b/src/AppInstallerCLICore/ExecutionArgs.h @@ -42,6 +42,7 @@ namespace AppInstaller::CLI::Execution InstallScope, InstallArchitecture, HashOverride, // Ignore hash mismatches + SkipDependencies, // Skip dependencies IgnoreLocalArchiveMalwareScan, // Ignore the local malware scan on archive files AcceptPackageAgreements, // Accept all license agreements for packages Rename, // Renames the file of the executable. Only applies to the portable installerType diff --git a/src/AppInstallerCLICore/Resources.h b/src/AppInstallerCLICore/Resources.h @@ -125,6 +125,7 @@ namespace AppInstaller::CLI::Resource WINGET_DEFINE_RESOURCE_STRINGID(DependenciesFlowNoSuitableInstallerFound); WINGET_DEFINE_RESOURCE_STRINGID(DependenciesFlowNoMatches); WINGET_DEFINE_RESOURCE_STRINGID(DependenciesFlowContainsLoop); + WINGET_DEFINE_RESOURCE_STRINGID(DependenciesSkippedMessage); WINGET_DEFINE_RESOURCE_STRINGID(DependenciesManagementError); WINGET_DEFINE_RESOURCE_STRINGID(DependenciesManagementExitMessage); WINGET_DEFINE_RESOURCE_STRINGID(DisabledByGroupPolicy); @@ -151,6 +152,7 @@ namespace AppInstaller::CLI::Resource WINGET_DEFINE_RESOURCE_STRINGID(FailedToEnableWindowsFeature); WINGET_DEFINE_RESOURCE_STRINGID(FailedToEnableWindowsFeatureOverridden); WINGET_DEFINE_RESOURCE_STRINGID(FailedToEnableWindowsFeatureOverrideRequired); + WINGET_DEFINE_RESOURCE_STRINGID(FailedToRefreshPathWarning); WINGET_DEFINE_RESOURCE_STRINGID(FeatureDisabledByAdminSettingMessage); WINGET_DEFINE_RESOURCE_STRINGID(FeatureDisabledMessage); WINGET_DEFINE_RESOURCE_STRINGID(FeaturesCommandLongDescription); @@ -438,6 +440,7 @@ namespace AppInstaller::CLI::Resource WINGET_DEFINE_RESOURCE_STRINGID(ShowVersion); WINGET_DEFINE_RESOURCE_STRINGID(SilentArgumentDescription); WINGET_DEFINE_RESOURCE_STRINGID(SingleCharAfterDashError); + WINGET_DEFINE_RESOURCE_STRINGID(SkipDependenciesArgumentDescription); WINGET_DEFINE_RESOURCE_STRINGID(SourceAddAlreadyExistsDifferentArg); WINGET_DEFINE_RESOURCE_STRINGID(SourceAddAlreadyExistsDifferentName); WINGET_DEFINE_RESOURCE_STRINGID(SourceAddAlreadyExistsMatch); diff --git a/src/AppInstallerCLICore/Workflows/DependenciesFlow.cpp b/src/AppInstallerCLICore/Workflows/DependenciesFlow.cpp @@ -343,6 +343,6 @@ namespace AppInstaller::CLI::Workflow // Install dependencies in the correct order context.Add<Execution::Data::PackageSubContexts>(std::move(dependencyPackageContexts)); - context << Workflow::InstallMultiplePackages(m_dependencyReportMessage, APPINSTALLER_CLI_ERROR_INSTALL_DEPENDENCIES, {}, false, true, true); + context << Workflow::InstallMultiplePackages(m_dependencyReportMessage, APPINSTALLER_CLI_ERROR_INSTALL_DEPENDENCIES, {}, false, true, true, true); } } \ No newline at end of file diff --git a/src/AppInstallerCLICore/Workflows/InstallFlow.cpp b/src/AppInstallerCLICore/Workflows/InstallFlow.cpp @@ -17,11 +17,12 @@ #include "PromptFlow.h" #include <AppInstallerMsixInfo.h> #include <AppInstallerDeployment.h> -#include <winget/ARPCorrelation.h> -#include <winget/Archive.h> +#include <AppInstallerSynchronization.h> #include <Argument.h> #include <Command.h> -#include <AppInstallerSynchronization.h> +#include <winget/ARPCorrelation.h> +#include <winget/Archive.h> +#include <winget/PathVariable.h> #include <winget/Runtime.h> using namespace winrt::Windows::Foundation; @@ -30,6 +31,7 @@ using namespace winrt::Windows::Management::Deployment; using namespace AppInstaller::CLI::Execution; using namespace AppInstaller::Manifest; using namespace AppInstaller::Repository; +using namespace AppInstaller::Registry::Environment; using namespace AppInstaller::Settings; using namespace AppInstaller::Utility; using namespace AppInstaller::Utility::literals; @@ -535,28 +537,33 @@ namespace AppInstaller::CLI::Workflow Workflow::ReportExecutionStage(ExecutionStage::PostExecution) << Workflow::ReportARPChanges << Workflow::RecordInstall << - Workflow::RemoveInstaller << + Workflow::RemoveInstaller << Workflow::DisplayInstallationNotes; } - void DownloadSinglePackage(Execution::Context& context) + void ManageDependencies(Execution::Context& context) { - // TODO: Split dependencies from download flow to prevent multiple installations. + if (Settings::User().Get<Settings::Setting::InstallSkipDependencies>() || context.Args.Contains(Execution::Args::Type::SkipDependencies)) + { + context.Reporter.Warn() << Resource::String::DependenciesSkippedMessage << std::endl; + return; + } + context << - Workflow::ReportIdentityAndInstallationDisclaimer << - Workflow::ShowPromptsForSinglePackage(/* ensureAcceptance */ true) << Workflow::GetDependenciesFromInstaller << Workflow::ReportDependencies(Resource::String::InstallAndUpgradeCommandsReportDependencies) << Workflow::EnableWindowsFeaturesDependencies << - Workflow::ManagePackageDependencies(Resource::String::InstallAndUpgradeCommandsReportDependencies) << - Workflow::DownloadInstaller; + Workflow::ManagePackageDependencies(Resource::String::InstallAndUpgradeCommandsReportDependencies); } void InstallSinglePackage(Execution::Context& context) { context << Workflow::CheckForUnsupportedArgs << - Workflow::DownloadSinglePackage << + Workflow::ReportIdentityAndInstallationDisclaimer << + Workflow::ShowPromptsForSinglePackage(/* ensureAcceptance */ true) << + Workflow::ManageDependencies << + Workflow::DownloadInstaller << Workflow::InstallPackageInstaller; } @@ -622,6 +629,19 @@ namespace AppInstaller::CLI::Workflow installContext.SetTerminationHR(Workflow::HandleException(installContext, std::current_exception())); } + if (m_refreshPathVariable) + { + if (RefreshPathVariableForCurrentProcess()) + { + AICLI_LOG(CLI, Info, << "Successfully refreshed process PATH environment variable."); + } + else + { + AICLI_LOG(CLI, Warning, << "Failed to refresh process PATH environment variable."); + context.Reporter.Warn() << Resource::String::FailedToRefreshPathWarning << std::endl; + } + } + installContext.Reporter.Info() << std::endl; if (installContext.IsTerminated()) diff --git a/src/AppInstallerCLICore/Workflows/InstallFlow.h b/src/AppInstallerCLICore/Workflows/InstallFlow.h @@ -142,11 +142,11 @@ namespace AppInstaller::CLI::Workflow // Outputs: None void InstallPackageInstaller(Execution::Context& context); - // Downloads the installer for a single package. This also does all the reporting and user interaction needed. + // Manages the dependencies for a single package. // Required Args: None // Inputs: Manifest, Installer // Outputs: InstallerPath - void DownloadSinglePackage(Execution::Context& context); + void ManageDependencies(Execution::Context& context); // Installs a single package. This also does the reporting, user interaction, and installer download // for single-package installation. @@ -167,14 +167,16 @@ namespace AppInstaller::CLI::Workflow std::vector<HRESULT>&& ignorableInstallResults = {}, bool ensurePackageAgreements = true, bool ignoreDependencies = false, - bool stopOnFailure = false) : + bool stopOnFailure = false, + bool refreshPathVariable = false) : WorkflowTask("InstallMultiplePackages"), m_dependenciesReportMessage(dependenciesReportMessage), m_resultOnFailure(resultOnFailure), m_ignorableInstallResults(std::move(ignorableInstallResults)), m_ignorePackageDependencies(ignoreDependencies), m_ensurePackageAgreements(ensurePackageAgreements), - m_stopOnFailure(stopOnFailure) {} + m_stopOnFailure(stopOnFailure), + m_refreshPathVariable(refreshPathVariable){} void operator()(Execution::Context& context) const override; @@ -185,6 +187,7 @@ namespace AppInstaller::CLI::Workflow bool m_ignorePackageDependencies; bool m_ensurePackageAgreements; bool m_stopOnFailure; + bool m_refreshPathVariable; }; // Stores the existing set of packages in ARP. diff --git a/src/AppInstallerCLIE2ETests/InstallCommand.cs b/src/AppInstallerCLIE2ETests/InstallCommand.cs @@ -20,6 +20,7 @@ namespace AppInstallerCLIE2ETests [OneTimeSetUp] public void OneTimeSetup() { + WinGetSettingsHelper.ConfigureFeature("dependencies", true); WinGetSettingsHelper.ConfigureFeature("windowsFeature", true); } @@ -647,5 +648,28 @@ namespace AppInstallerCLIE2ETests Assert.True(installResult.StdOut.Contains("Successfully installed")); Assert.True(TestCommon.VerifyTestExeInstalledAndCleanup(testDir)); } + + /// <summary> + /// Test install a package with a package dependency that requires the PATH environment variable to be refreshed between dependency installs. + /// </summary> + [Test] + public void InstallWithPackageDependency_RefreshPathVariable() + { + var testDir = TestCommon.GetRandomTestDir(); + string installDir = TestCommon.GetPortablePackagesDirectory(); + var installResult = TestCommon.RunAICLICommand("install", $"AppInstallerTest.PackageDependencyRequiresPathRefresh -l {testDir}"); + Assert.AreEqual(Constants.ErrorCode.S_OK, installResult.ExitCode); + Assert.True(installResult.StdOut.Contains("Successfully installed")); + + // Portable package is used as a dependency. Ensure that it is installed and cleaned up successfully. + string portablePackageId, commandAlias, fileName, packageDirName, productCode; + portablePackageId = "AppInstallerTest.TestPortableExeWithCommand"; + packageDirName = productCode = portablePackageId + "_" + Constants.TestSourceIdentifier; + fileName = "AppInstallerTestExeInstaller.exe"; + commandAlias = "testCommand.exe"; + + TestCommon.VerifyPortablePackage(Path.Combine(installDir, packageDirName), commandAlias, fileName, productCode, true); + Assert.True(TestCommon.VerifyTestExeInstalledAndCleanup(testDir)); + } } } \ No newline at end of file diff --git a/src/AppInstallerCLIE2ETests/Interop/InstallInterop.cs b/src/AppInstallerCLIE2ETests/Interop/InstallInterop.cs @@ -65,6 +65,7 @@ namespace AppInstallerCLIE2ETests.Interop // Assert Assert.AreEqual(InstallResultStatus.Ok, installResult.Status); + Assert.True(TestCommon.VerifyTestExeInstalledAndCleanup(this.installDir)); } /// <summary> @@ -565,6 +566,37 @@ namespace AppInstallerCLIE2ETests.Interop } /// <summary> + /// Test installing a package with a package dependency and passing in the 'skip-dependencies' install option. + /// </summary> + /// <returns>A <see cref="Task"/> representing the asynchronous unit test.</returns> + [Test] + public async Task InstallWithSkipDependencies() + { + // Find package + var searchResult = this.FindOnePackage(this.testSource, PackageMatchField.Id, PackageFieldMatchOption.Equals, "AppInstallerTest.PackageDependency"); + + // Configure installation + var installOptions = this.TestFactory.CreateInstallOptions(); + installOptions.PackageInstallMode = PackageInstallMode.Silent; + installOptions.PreferredInstallLocation = this.installDir; + installOptions.AcceptPackageAgreements = true; + installOptions.SkipDependencies = true; + + // Install + var installResult = await this.packageManager.InstallPackageAsync(searchResult.CatalogPackage, installOptions); + + // Assert that only the exe installer is installed and not the portable package dependency. + Assert.AreEqual(InstallResultStatus.Ok, installResult.Status); + Assert.True(TestCommon.VerifyTestExeInstalledAndCleanup(this.installDir)); + + string installDir = Path.Combine(Environment.GetEnvironmentVariable(Constants.LocalAppData), "Microsoft", "WinGet", "Packages"); + string productCode = Constants.PortableExePackageDirName; + string commandAlias = $"{Constants.ExeInstaller}.exe"; + string fileName = $"{Constants.ExeInstaller}.exe"; + TestCommon.VerifyPortablePackage(Path.Combine(installDir, Constants.PortableExePackageDirName), commandAlias, fileName, productCode, false); + } + + /// <summary> /// Test to verify the GetApplicableInstaller() COM call returns the correct manifest installer metadata. /// </summary> [Test] diff --git a/src/AppInstallerCLIE2ETests/TestData/Manifests/TestExeInstaller_PackageDependency.yaml b/src/AppInstallerCLIE2ETests/TestData/Manifests/TestExeInstaller_PackageDependency.yaml @@ -0,0 +1,22 @@ +PackageIdentifier: AppInstallerTest.PackageDependency +PackageVersion: 1.0.0.0 +PackageLocale: en-US +PackageName: TestPackageDependency +ShortDescription: Installs a package with a package dependency. +Publisher: AppInstallerTest +License: testLicense +Installers: + - Architecture: x64 + InstallerUrl: https://localhost:5001/TestKit/AppInstallerTestExeInstaller/AppInstallerTestExeInstaller.exe + InstallerType: exe + InstallerSha256: <EXEHASH> + InstallerSwitches: + Silent: /exesilent + SilentWithProgress: /exeswp + Log: /LogFile <LOGPATH> + InstallLocation: /InstallDir <INSTALLPATH> + Dependencies: + PackageDependencies: + - PackageIdentifier: AppInstallerTest.TestPortableExe +ManifestType: singleton +ManifestVersion: 1.4.0 diff --git a/src/AppInstallerCLIE2ETests/TestData/Manifests/TestExeInstaller_PackageDependencyRequiresPathRefresh.yaml b/src/AppInstallerCLIE2ETests/TestData/Manifests/TestExeInstaller_PackageDependencyRequiresPathRefresh.yaml @@ -0,0 +1,23 @@ +PackageIdentifier: AppInstallerTest.PackageDependencyRequiresPathRefresh +PackageVersion: 1.0.0.0 +PackageLocale: en-US +PackageName: TestPackageDependencyWithPathRefresh +ShortDescription: Installs a portable package dependency that modifies the PATH environment variable during installation, which is then invoked by the main installer. +Publisher: AppInstallerTest +License: testLicense +Installers: + - Architecture: x64 + InstallerUrl: https://localhost:5001/TestKit/AppInstallerTestExeInstaller/AppInstallerTestExeInstaller.exe + InstallerType: exe + InstallerSha256: <EXEHASH> + InstallerSwitches: + Custom: /AliasToExecute testCommand /AliasArguments /NoOperation + Silent: /exesilent + SilentWithProgress: /exeswp + Log: /LogFile <LOGPATH> + InstallLocation: /InstallDir <INSTALLPATH> + Dependencies: + PackageDependencies: + - PackageIdentifier: AppInstallerTest.TestPortableExeWithCommand +ManifestType: singleton +ManifestVersion: 1.4.0 diff --git a/src/AppInstallerCLIE2ETests/TestData/Manifests/TestExeInstaller_PathVariableRefresh.yaml b/src/AppInstallerCLIE2ETests/TestData/Manifests/TestExeInstaller_PathVariableRefresh.yaml @@ -0,0 +1,22 @@ +PackageIdentifier: AppInstallerTest.PathVariableRefresh +PackageVersion: 1.0.0.0 +PackageLocale: en-US +PackageName: TestPathVariableRefresh +ShortDescription: Emulates an installer that invokes a command that only becomes available once the PATH environment variable is refreshed. +Publisher: Microsoft Corporation +License: Test +Installers: + - Architecture: x64 + InstallerUrl: https://localhost:5001/TestKit/AppInstallerTestExeInstaller/AppInstallerTestExeInstaller.exe + InstallerType: exe + InstallerSha256: <EXEHASH> + InstallerSwitches: + Custom: /AliasToExecute testCommand.exe /AliasArguments /NoOperation + SilentWithProgress: /exeswp + Silent: /exesilent + Interactive: /exeinteractive + Language: /exeenus + Log: /LogFile <LOGPATH> + InstallLocation: /InstallDir <INSTALLPATH> +ManifestType: singleton +ManifestVersion: 1.4.0+ \ No newline at end of file diff --git a/src/AppInstallerCLIPackage/Shared/Strings/en-us/winget.resw b/src/AppInstallerCLIPackage/Shared/Strings/en-us/winget.resw @@ -1970,4 +1970,14 @@ Please specify one of them using the --source option to proceed.</value> <data name="ConfigurationDisabledMessage" xml:space="preserve"> <value>Configuration is disabled.</value> </data> + <data name="SkipDependenciesArgumentDescription" xml:space="preserve"> + <value>Skips processing package dependencies and Windows features</value> + </data> + <data name="DependenciesSkippedMessage" xml:space="preserve"> + <value>Dependencies skipped.</value> + </data> + <data name="FailedToRefreshPathWarning" xml:space="preserve"> + <value>Failed to refresh PATH variable for process. Subsequent installs that depend on changes to the PATH variable may fail.</value> + <comment>{Locked="PATH"}</comment> + </data> </root> \ No newline at end of file diff --git a/src/AppInstallerCLITests/AppInstallerCLITests.vcxproj b/src/AppInstallerCLITests/AppInstallerCLITests.vcxproj @@ -222,6 +222,7 @@ <ClCompile Include="PackageCollection.cpp" /> <ClCompile Include="PackageDependenciesValidationUtil.cpp" /> <ClCompile Include="PackageTrackingCatalog.cpp" /> + <ClCompile Include="PathVariable.cpp" /> <ClCompile Include="PinFlow.cpp" /> <ClCompile Include="PinningIndex.cpp" /> <ClCompile Include="PortableInstaller.cpp" /> diff --git a/src/AppInstallerCLITests/AppInstallerCLITests.vcxproj.filters b/src/AppInstallerCLITests/AppInstallerCLITests.vcxproj.filters @@ -302,6 +302,9 @@ <ClCompile Include="IconExtraction.cpp"> <Filter>Source Files\Repository</Filter> </ClCompile> + <ClCompile Include="PathVariable.cpp"> + <Filter>Source Files\CLI</Filter> + </ClCompile> </ItemGroup> <ItemGroup> <None Include="PropertySheet.props" /> diff --git a/src/AppInstallerCLITests/Dependencies.cpp b/src/AppInstallerCLITests/Dependencies.cpp @@ -5,11 +5,13 @@ #include <winget/DependenciesGraph.h> #include <Workflows/DependencyNodeProcessor.h> #include <AppInstallerErrors.h> +#include <AppInstallerRuntime.h> #include <AppInstallerStrings.h> #include <Workflows/DependenciesFlow.h> #include <Workflows/WorkflowBase.h> -#include <winget/RepositorySource.h> #include <winget/ManifestYamlParser.h> +#include <winget/PathVariable.h> +#include <winget/RepositorySource.h> #include <Resources.h> using namespace winrt::Windows::Foundation; @@ -206,4 +208,4 @@ TEST_CASE("DependencyNodeProcessor_NoMatches", "[dependencies]") 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/InstallDependenciesFlow.cpp b/src/AppInstallerCLITests/InstallDependenciesFlow.cpp @@ -217,6 +217,50 @@ TEST_CASE("InstallerWithoutDependencies_RootDependenciesAreUsed", "[dependencies REQUIRE(installOutput.str().find("PreviewIISOnRoot") != std::string::npos); } +TEST_CASE("InstallerWithDependencies_SkipDependencies", "[dependencies]") +{ + std::ostringstream installOutput; + TestContext context{ installOutput, std::cin }; + auto previousThreadGlobals = context.SetForCurrentThread(); + OverrideForShellExecute(context); + + context.Args.AddArg(Execution::Args::Type::Manifest, TestDataFile("Installer_Exe_Dependencies.yaml").GetPath().u8string()); + context.Args.AddArg(Execution::Args::Type::SkipDependencies); + + TestUserSettings settings; + settings.Set<AppInstaller::Settings::Setting::EFDependencies>({ true }); + + InstallCommand install({}); + install.Execute(context); + INFO(installOutput.str()); + + REQUIRE(installOutput.str().find(Resource::LocString(Resource::String::DependenciesSkippedMessage).get()) != std::string::npos); + REQUIRE_FALSE(installOutput.str().find(Resource::LocString(Resource::String::InstallAndUpgradeCommandsReportDependencies).get()) != std::string::npos); + REQUIRE_FALSE(installOutput.str().find("PreviewIIS") != std::string::npos); +} + +TEST_CASE("InstallerWithDependencies_IgnoreDependenciesSetting", "[dependencies]") +{ + std::ostringstream installOutput; + TestContext context{ installOutput, std::cin }; + auto previousThreadGlobals = context.SetForCurrentThread(); + 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 }); + settings.Set<AppInstaller::Settings::Setting::InstallSkipDependencies>({ true }); + + InstallCommand install({}); + install.Execute(context); + INFO(installOutput.str()); + + REQUIRE(installOutput.str().find(Resource::LocString(Resource::String::DependenciesSkippedMessage).get()) != std::string::npos); + REQUIRE_FALSE(installOutput.str().find(Resource::LocString(Resource::String::InstallAndUpgradeCommandsReportDependencies).get()) != std::string::npos); + REQUIRE_FALSE(installOutput.str().find("PreviewIIS") != std::string::npos); +} + TEST_CASE("DependenciesMultideclaration_InstallerDependenciesPreference", "[dependencies]") { std::ostringstream installOutput; diff --git a/src/AppInstallerCLITests/PathVariable.cpp b/src/AppInstallerCLITests/PathVariable.cpp @@ -0,0 +1,159 @@ +#include "pch.h" +#include "TestCommon.h" +#include <AppInstallerRuntime.h> +#include <Resources.h> +#include <winget/PathVariable.h> +#include <winget/Filesystem.h> + +using namespace AppInstaller::Manifest; +using namespace AppInstaller::Registry::Environment; + +TEST_CASE("PathVariable_EnforceReadOnly", "[pathVariable]") +{ + auto pathVariable = PathVariable(ScopeEnum::User, true); + REQUIRE_THROWS_HR(pathVariable.Append("testString"), E_ACCESSDENIED); + REQUIRE_THROWS_HR(pathVariable.Remove("testString"), E_ACCESSDENIED); +} + +TEST_CASE("PathVariable_Append_NoSemiColon", "[pathVariable]") +{ + if (!AppInstaller::Runtime::IsRunningAsAdmin()) + { + WARN("Test requires admin privilege. Skipped."); + return; + } + + auto pathVariable = PathVariable(ScopeEnum::User); + std::filesystem::path testPath{ "testString" }; + REQUIRE_FALSE(pathVariable.Contains(testPath)); + REQUIRE(pathVariable.Append(testPath)); + REQUIRE(pathVariable.Contains(testPath)); + + // Verify that the path value ends with a ';' and not include ";;" + std::string pathValue = pathVariable.GetPathValue(); + REQUIRE(pathValue.back() == ';'); + REQUIRE(pathValue.find(";;") == std::string::npos); + + REQUIRE(pathVariable.Remove(testPath)); + REQUIRE_FALSE(pathVariable.Contains(testPath)); +} + +TEST_CASE("PathVariable_Append_WithSemicolon", "[pathVariable]") +{ + if (!AppInstaller::Runtime::IsRunningAsAdmin()) + { + WARN("Test requires admin privilege. Skipped."); + return; + } + + auto pathVariable = PathVariable(ScopeEnum::User); + std::filesystem::path testPath{ "testString;" }; + REQUIRE_FALSE(pathVariable.Contains(testPath)); + REQUIRE(pathVariable.Append(testPath)); + REQUIRE(pathVariable.Contains(testPath)); + + // Verify that the path value ends with a ';' and does not include ";;" + std::string pathValue = pathVariable.GetPathValue(); + REQUIRE(pathValue.back() == ';'); + REQUIRE(pathValue.find(";;") == std::string::npos); + + REQUIRE(pathVariable.Remove(testPath)); + REQUIRE_FALSE(pathVariable.Contains(testPath)); +} + +std::wstring GetCurrentProcessPathVariable() +{ + size_t requiredSize; + _wgetenv_s(&requiredSize, nullptr, 0, L"PATH"); + + if (requiredSize > 0) + { + auto buffer = std::make_unique<wchar_t[]>(requiredSize); + errno_t errorResult = _wgetenv_s(&requiredSize, buffer.get(), requiredSize, L"PATH"); + if (errorResult == 0) + { + return std::wstring(buffer.get()); + } + } + return {}; +} + +TEST_CASE("RefreshEnvironmentVariable_User", "[pathVariable]") +{ + if (!AppInstaller::Runtime::IsRunningAsAdmin()) + { + WARN("Test requires admin privilege. Skipped."); + return; + } + + std::wstring testPathEntry = L"testUserPathEntry"; + auto pathVariable = AppInstaller::Registry::Environment::PathVariable(ScopeEnum::User); + pathVariable.Append(testPathEntry); + + std::wstring initialPathValue = GetCurrentProcessPathVariable(); + bool firstCheck = initialPathValue.find(testPathEntry) != std::string::npos; + + AppInstaller::Registry::Environment::RefreshPathVariableForCurrentProcess(); + + std::wstring updatedPathValue = GetCurrentProcessPathVariable(); + bool secondCheck = updatedPathValue.find(testPathEntry) != std::string::npos; + + pathVariable.Remove(testPathEntry); + + REQUIRE_FALSE(firstCheck); + REQUIRE(secondCheck); +} + +TEST_CASE("RefreshEnvironmentVariable_System", "[pathVariable]") +{ + if (!AppInstaller::Runtime::IsRunningAsAdmin()) + { + WARN("Test requires admin privilege. Skipped."); + return; + } + + std::wstring testPathEntry = L"testSystemPathEntry"; + auto pathVariable = AppInstaller::Registry::Environment::PathVariable(ScopeEnum::Machine); + pathVariable.Append(testPathEntry); + + std::wstring initialPathValue = GetCurrentProcessPathVariable(); + bool firstCheck = initialPathValue.find(testPathEntry) != std::string::npos; + + AppInstaller::Registry::Environment::RefreshPathVariableForCurrentProcess(); + + std::wstring updatedPathValue = GetCurrentProcessPathVariable(); + bool secondCheck = updatedPathValue.find(testPathEntry) != std::string::npos; + + pathVariable.Remove(testPathEntry); + + REQUIRE_FALSE(firstCheck); + REQUIRE(secondCheck); +} + +TEST_CASE("VerifyPathRefreshExpandsValues", "[pathVariable]") +{ + if (!AppInstaller::Runtime::IsRunningAsAdmin()) + { + WARN("Test requires admin privilege. Skipped."); + return; + } + + std::filesystem::path testEntry{ "%USERPROFILE%\\testPath" }; + auto pathVariable = AppInstaller::Registry::Environment::PathVariable(ScopeEnum::User); + pathVariable.Append(testEntry); + + std::wstring initialPathValue = GetCurrentProcessPathVariable(); + bool firstCheck = initialPathValue.find(testEntry) != std::string::npos; + + AppInstaller::Registry::Environment::RefreshPathVariableForCurrentProcess(); + + // %USERPROFILE% should be replaced with the actual path. + std::wstring updatedPathValue = GetCurrentProcessPathVariable(); + std::wstring expandedTestPath = AppInstaller::Filesystem::GetExpandedPath(testEntry.u8string()); + bool secondCheck = updatedPathValue.find(expandedTestPath) != std::string::npos; + + pathVariable.Remove(testEntry); + + REQUIRE_FALSE(firstCheck); + REQUIRE(secondCheck); +} diff --git a/src/AppInstallerCLITests/Strings.cpp b/src/AppInstallerCLITests/Strings.cpp @@ -260,3 +260,22 @@ TEST_CASE("SplitIntoLines", "[string]") "You want my treasure?\rYou can have it!\nI left everything I gathered in one place!\r\nYou just have to find it!") == std::vector<std::string>{ "You want my treasure?", "You can have it!", "I left everything I gathered in one place!", "You just have to find it!" }); } + +TEST_CASE("SplitWithSeparator", "[string]") +{ + std::vector<std::string> test1 = Split("first;second;third", ';'); + REQUIRE(test1.size() == 3); + REQUIRE(test1[0] == "first"); + REQUIRE(test1[1] == "second"); + REQUIRE(test1[2] == "third"); + + std::vector<std::string> test2 = Split("two spaces", ' '); + REQUIRE(test2.size() == 3); + REQUIRE(test2[0] == "two"); + REQUIRE(test2[1] == ""); + REQUIRE(test2[2] == "spaces"); + + std::vector<std::string> test3 = Split("test", '.'); + REQUIRE(test3.size() == 1); + REQUIRE(test3[0] == "test"); +} diff --git a/src/AppInstallerCommonCore/PathVariable.cpp b/src/AppInstallerCommonCore/PathVariable.cpp @@ -2,6 +2,7 @@ // Licensed under the MIT License. #include "pch.h" #include "winget/PathVariable.h" +#include <winget/Filesystem.h> using namespace AppInstaller::Utility; @@ -12,17 +13,54 @@ namespace AppInstaller::Registry::Environment constexpr std::wstring_view s_PathName = L"Path"; constexpr std::wstring_view s_PathSubkey_User = L"Environment"; constexpr std::wstring_view s_PathSubkey_Machine = L"SYSTEM\\CurrentControlSet\\Control\\Session Manager\\Environment"; + + void EnsurePathValueEndsWithSemicolon(std::string& value) + { + if (value.back() != ';') + { + value += ';'; + } + } + + std::string ExpandPathValue(const std::string& value) + { + std::string result; + std::vector<std::string> pathEntries = Split(value, ';'); + for (std::string& pathEntry : pathEntries) + { + if (!pathEntry.empty()) + { + result += AppInstaller::Filesystem::GetExpandedPath(pathEntry).u8string(); + result += ';'; + } + } + return result; + } } - PathVariable::PathVariable(Manifest::ScopeEnum scope) + PathVariable::PathVariable(Manifest::ScopeEnum scope, bool readOnly) : m_scope(scope), m_readOnly(readOnly) { - if (scope == Manifest::ScopeEnum::Machine) + if (m_readOnly) { - m_key = Registry::Key::Create(HKEY_LOCAL_MACHINE, std::wstring{ s_PathSubkey_Machine }); + if (m_scope == Manifest::ScopeEnum::Machine) + { + m_key = Registry::Key::OpenIfExists(HKEY_LOCAL_MACHINE, std::wstring{ s_PathSubkey_Machine }); + } + else + { + m_key = Registry::Key::OpenIfExists(HKEY_CURRENT_USER, std::wstring{ s_PathSubkey_User }); + } } else { - m_key = Registry::Key::Create(HKEY_CURRENT_USER, std::wstring{ s_PathSubkey_User }); + if (m_scope == Manifest::ScopeEnum::Machine) + { + m_key = Registry::Key::Create(HKEY_LOCAL_MACHINE, std::wstring{ s_PathSubkey_Machine }); + } + else + { + m_key = Registry::Key::Create(HKEY_CURRENT_USER, std::wstring{ s_PathSubkey_User }); + } } } @@ -40,6 +78,8 @@ namespace AppInstaller::Registry::Environment bool PathVariable::Remove(const std::filesystem::path& target) { + THROW_HR_IF(E_ACCESSDENIED, m_readOnly); + if (Contains(target)) { std::string targetString = Normalize(target.u8string()); @@ -57,16 +97,15 @@ namespace AppInstaller::Registry::Environment bool PathVariable::Append(const std::filesystem::path& target) { + THROW_HR_IF(E_ACCESSDENIED, m_readOnly); + if (!Contains(target)) { std::string targetString = Normalize(target.u8string()); std::string pathValue = GetPathValue(); - if (pathValue.back() != ';') - { - pathValue += ";"; - } - - pathValue += targetString + ";"; + EnsurePathValueEndsWithSemicolon(pathValue); + pathValue += targetString; + EnsurePathValueEndsWithSemicolon(pathValue); SetPathValue(pathValue); return true; } @@ -78,7 +117,18 @@ namespace AppInstaller::Registry::Environment void PathVariable::SetPathValue(const std::string& value) { + THROW_HR_IF(E_ACCESSDENIED, m_readOnly); + std::wstring pathName = std::wstring{ s_PathName }; m_key.SetValue(pathName, ConvertToUTF16(value), REG_EXPAND_SZ); } + + bool RefreshPathVariableForCurrentProcess() + { + // Path values must be expanded before assigning to process environment for proper refresh. + std::string systemPathValue = ExpandPathValue(PathVariable(Manifest::ScopeEnum::Machine, true).GetPathValue()); + std::string userPathValue = ExpandPathValue(PathVariable(Manifest::ScopeEnum::User, true).GetPathValue()); + std::wstring pathValue = ConvertToUTF16(systemPathValue + userPathValue); + return _wputenv_s(L"PATH", pathValue.c_str()) == 0; + } } \ No newline at end of file diff --git a/src/AppInstallerCommonCore/Public/winget/PathVariable.h b/src/AppInstallerCommonCore/Public/winget/PathVariable.h @@ -1,14 +1,16 @@ // Copyright (c) Microsoft Corporation. // Licensed under the MIT License. #pragma once -#include "winget/Registry.h" #include "winget/Manifest.h" +#include "winget/Registry.h" namespace AppInstaller::Registry::Environment { + bool RefreshPathVariableForCurrentProcess(); + struct PathVariable { - PathVariable(Manifest::ScopeEnum scope); + PathVariable(Manifest::ScopeEnum scope, bool readOnly = false); // Returns the PATH variable as a string. std::string GetPathValue(); @@ -25,7 +27,7 @@ namespace AppInstaller::Registry::Environment private: void SetPathValue(const std::string& value); Registry::Key m_key; - HKEY m_root; Manifest::ScopeEnum m_scope; + bool m_readOnly; }; } \ No newline at end of file diff --git a/src/AppInstallerCommonCore/Public/winget/UserSettings.h b/src/AppInstallerCommonCore/Public/winget/UserSettings.h @@ -83,7 +83,7 @@ namespace AppInstaller::Settings InstallLocalePreference, InstallLocaleRequirement, InstallDefaultRoot, - InstallIgnoreWarnings, + InstallSkipDependencies, DisableInstallNotes, PortablePackageUserRoot, PortablePackageMachineRoot, @@ -153,7 +153,7 @@ namespace AppInstaller::Settings SETTINGMAPPING_SPECIALIZATION(Setting::InstallScopeRequirement, std::string, Manifest::ScopeEnum, Manifest::ScopeEnum::Unknown, ".installBehavior.requirements.scope"sv); SETTINGMAPPING_SPECIALIZATION(Setting::InstallLocalePreference, std::vector<std::string>, std::vector<std::string>, {}, ".installBehavior.preferences.locale"sv); SETTINGMAPPING_SPECIALIZATION(Setting::InstallLocaleRequirement, std::vector<std::string>, std::vector<std::string>, {}, ".installBehavior.requirements.locale"sv); - SETTINGMAPPING_SPECIALIZATION(Setting::InstallIgnoreWarnings, bool, bool, false, ".installBehavior.ignoreWarnings"sv); + SETTINGMAPPING_SPECIALIZATION(Setting::InstallSkipDependencies, bool, bool, false, ".installBehavior.skipDependencies"sv); SETTINGMAPPING_SPECIALIZATION(Setting::DisableInstallNotes, bool, bool, false, ".installBehavior.disableInstallNotes"sv); SETTINGMAPPING_SPECIALIZATION(Setting::PortablePackageUserRoot, std::string, std::filesystem::path, {}, ".installBehavior.portablePackageUserRoot"sv); SETTINGMAPPING_SPECIALIZATION(Setting::PortablePackageMachineRoot, std::string, std::filesystem::path, {}, ".installBehavior.portablePackageMachineRoot"sv); diff --git a/src/AppInstallerCommonCore/UserSettings.cpp b/src/AppInstallerCommonCore/UserSettings.cpp @@ -266,7 +266,7 @@ namespace AppInstaller::Settings WINGET_VALIDATE_PASS_THROUGH(TelemetryDisable) WINGET_VALIDATE_PASS_THROUGH(InteractivityDisable) WINGET_VALIDATE_PASS_THROUGH(EnableSelfInitiatedMinidump) - WINGET_VALIDATE_PASS_THROUGH(InstallIgnoreWarnings) + WINGET_VALIDATE_PASS_THROUGH(InstallSkipDependencies) WINGET_VALIDATE_PASS_THROUGH(DisableInstallNotes) WINGET_VALIDATE_PASS_THROUGH(UninstallPurgePortablePackage) WINGET_VALIDATE_PASS_THROUGH(NetworkWingetAlternateSourceURL) diff --git a/src/AppInstallerSharedLib/AppInstallerStrings.cpp b/src/AppInstallerSharedLib/AppInstallerStrings.cpp @@ -838,4 +838,21 @@ namespace AppInstaller::Utility } return LocIndString{ ssJoin.str() }; } + + std::vector<std::string> Split(const std::string& input, char separator) + { + std::vector<std::string> result; + size_t startIndex = 0; + size_t endIndex = 0; + + while ((endIndex = input.find(separator, startIndex)) != std::string::npos) + { + std::string substring = input.substr(startIndex, endIndex - startIndex); + result.push_back(substring); + startIndex = endIndex + 1; + } + + result.push_back(input.substr(startIndex)); + return result; + } } diff --git a/src/AppInstallerSharedLib/Public/AppInstallerStrings.h b/src/AppInstallerSharedLib/Public/AppInstallerStrings.h @@ -250,6 +250,9 @@ namespace AppInstaller::Utility // Join a string vector using the provided separator. LocIndString Join(LocIndView separator, const std::vector<LocIndString>& vector); + // Splits the string using the provided separator. + std::vector<std::string> Split(const std::string& input, char separator); + // Format an input string by replacing placeholders {index} with provided values at corresponding indices. // Note: After upgrading to C++20, this function should be deprecated in favor of std::format. template <typename ... T> diff --git a/src/AppInstallerTestExeInstaller/main.cpp b/src/AppInstallerTestExeInstaller/main.cpp @@ -145,7 +145,10 @@ int wmain(int argc, const wchar_t** argv) std::wstring productCode; std::wstring displayName; std::wstring displayVersion; + std::wstring aliasToExecute; + std::wstring aliasArguments; bool useHKLM = false; + bool noOperation = false; int exitCode = 0; // Output to cout by default, but swap to a file if requested @@ -222,6 +225,56 @@ int wmain(int argc, const wchar_t** argv) { useHKLM = true; } + + // Executes a command alias during installation + else if (_wcsicmp(argv[i], L"/AliasToExecute") == 0) + { + if (++i < argc) + { + aliasToExecute = argv[i]; + outContent << argv[i] << ' '; + } + } + + // Additional arguments to include when executing the command alias during installation + else if (_wcsicmp(argv[i], L"/AliasArguments") == 0) + { + if (++i < argc) + { + aliasArguments = argv[i]; + outContent << argv[i] << ' '; + } + } + + // Returns the success exit code to emulate being invoked by another caller. + else if (_wcsicmp(argv[i], L"/NoOperation") == 0) + { + noOperation = true; + } + } + + if (noOperation) + { + return exitCode; + } + + if (!aliasToExecute.empty()) + { + SHELLEXECUTEINFOW execInfo = { 0 }; + execInfo.cbSize = sizeof(execInfo); + execInfo.fMask = SEE_MASK_NOCLOSEPROCESS; + execInfo.lpFile = aliasToExecute.c_str(); + + if (!aliasArguments.empty()) + { + execInfo.lpParameters = aliasArguments.c_str(); + } + execInfo.nShow = SW_SHOW; + + if (!ShellExecuteExW(&execInfo) || !execInfo.hProcess) + { + return -1; + } } if (displayName.empty()) diff --git a/src/IndexCreationTool/Program.cs b/src/IndexCreationTool/Program.cs @@ -62,11 +62,35 @@ namespace IndexCreationTool foreach (string includeDir in includeDirList) { var fullPath = Path.Combine(rootDir, includeDir); - foreach (string file in Directory.EnumerateFiles(fullPath, "*.yaml", SearchOption.AllDirectories)) + Queue<string> filesQueue = new(Directory.EnumerateFiles(fullPath, "*.yaml", SearchOption.AllDirectories)); + + while (filesQueue.Count > 0) { - indexHelper.AddManifest(file, Path.GetRelativePath(rootDir, file)); + int currentCount = filesQueue.Count; + + for (int i = 0; i < currentCount; i++) + { + string file = filesQueue.Dequeue(); + try + { + indexHelper.AddManifest(file, Path.GetRelativePath(rootDir, file)); + } + catch + { + // If adding manifest to index fails, add to queue and try again. + // This can occur if there is a package dependency that has not yet been added to the index. + filesQueue.Enqueue(file); + } + } + + if (filesQueue.Count == currentCount) + { + Console.WriteLine("Failed to add all manifests in directory to index."); + Environment.Exit(-1); + } } } + indexHelper.PrepareForPackaging(); } diff --git a/src/Microsoft.Management.Deployment/InstallOptions.cpp b/src/Microsoft.Management.Deployment/InstallOptions.cpp @@ -144,5 +144,13 @@ namespace winrt::Microsoft::Management::Deployment::implementation { return m_acceptPackageAgreements; } + void InstallOptions::SkipDependencies(bool value) + { + m_skipDependencies = value; + } + bool InstallOptions::SkipDependencies() + { + return m_skipDependencies; + } CoCreatableMicrosoftManagementDeploymentClass(InstallOptions); } diff --git a/src/Microsoft.Management.Deployment/InstallOptions.h b/src/Microsoft.Management.Deployment/InstallOptions.h @@ -40,6 +40,8 @@ namespace winrt::Microsoft::Management::Deployment::implementation void Force(bool value); bool AcceptPackageAgreements(); void AcceptPackageAgreements(bool value); + bool SkipDependencies(); + void SkipDependencies(bool value); #if !defined(INCLUDE_ONLY_INTERFACE_METHODS) private: @@ -59,6 +61,7 @@ namespace winrt::Microsoft::Management::Deployment::implementation bool m_allowUpgradeToUnknownVersion = false; bool m_force = false; bool m_acceptPackageAgreements = true; + bool m_skipDependencies = false; #endif }; } diff --git a/src/Microsoft.Management.Deployment/PackageManager.cpp b/src/Microsoft.Management.Deployment/PackageManager.cpp @@ -400,6 +400,11 @@ namespace winrt::Microsoft::Management::Deployment::implementation { context->Args.AddArg(Execution::Args::Type::AcceptPackageAgreements); } + + if (options.SkipDependencies()) + { + context->Args.AddArg(Execution::Args::Type::SkipDependencies); + } } else { diff --git a/src/Microsoft.Management.Deployment/PackageManager.idl b/src/Microsoft.Management.Deployment/PackageManager.idl @@ -2,7 +2,7 @@ // Licensed under the MIT License. namespace Microsoft.Management.Deployment { - [contractversion(6)] + [contractversion(7)] apicontract WindowsPackageManagerContract{}; /// State of the install @@ -821,6 +821,12 @@ namespace Microsoft.Management.Deployment /// Bypasses the Disabled Store Policy Boolean BypassIsStoreClientBlockedPolicyCheck; } + + [contract(Microsoft.Management.Deployment.WindowsPackageManagerContract, 7)] + { + // Skip installing the dependencies for the package. + Boolean SkipDependencies; + } } [contract(Microsoft.Management.Deployment.WindowsPackageManagerContract, 4)]