winget-cli

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

commit b1979614e4e0e75bcaf04d082de4c19d442dc8ca
parent f269007baed40efb43d5a32d283e934827f53e6a
Author: Ruben Guerrero <rubengu@microsoft.com>
Date:   Wed, 26 Apr 2023 18:19:17 -0700

WinGetPackage DSC Resource (#2863)

This is a preview implementation of a DSC resource that installs packages on winget.

For now, ensures a package is installed or not. Its install behavior depends if the package is already installed for the user or not.
Diffstat:
Msrc/PowerShell/Microsoft.WinGet.Client.Cmdlets/Cmdlets/Common/FinderCmdlet.cs | 5+++--
Msrc/PowerShell/Microsoft.WinGet.Client.Cmdlets/Cmdlets/Common/InstallCmdlet.cs | 2+-
Msrc/PowerShell/Microsoft.WinGet.Client.Cmdlets/Cmdlets/FindPackageCmdlet.cs | 2+-
Msrc/PowerShell/Microsoft.WinGet.Client.Cmdlets/Cmdlets/GetPackageCmdlet.cs | 2+-
Msrc/PowerShell/Microsoft.WinGet.Client.Cmdlets/Cmdlets/InstallPackageCmdlet.cs | 11++++++-----
Msrc/PowerShell/Microsoft.WinGet.Client.Cmdlets/Cmdlets/UninstallPackageCmdlet.cs | 11++++++-----
Msrc/PowerShell/Microsoft.WinGet.Client.Cmdlets/Cmdlets/UpdatePackageCmdlet.cs | 2+-
Asrc/PowerShell/Microsoft.WinGet.Client.Cmdlets/PSObjects/PSPackageFieldMatchOption.cs | 34++++++++++++++++++++++++++++++++++
Msrc/PowerShell/Microsoft.WinGet.Client.Cmdlets/PSObjects/PSPackageInstallMode.cs | 2+-
Msrc/PowerShell/Microsoft.WinGet.Client.Cmdlets/PSObjects/PSPackageInstallScope.cs | 2+-
Msrc/PowerShell/Microsoft.WinGet.Client.Cmdlets/PSObjects/PSPackageUninstallMode.cs | 2+-
Msrc/PowerShell/Microsoft.WinGet.Client.Cmdlets/PSObjects/PSProcessorArchitecture.cs | 2+-
Msrc/PowerShell/Microsoft.WinGet.Client.Engine/Commands/Common/FinderCommand.cs | 19++++---------------
Msrc/PowerShell/Microsoft.WinGet.Client.Engine/Commands/FinderPackageCommand.cs | 8+++++---
Msrc/PowerShell/Microsoft.WinGet.Client.Engine/Commands/InstallerPackageCommand.cs | 6+++---
Msrc/PowerShell/Microsoft.WinGet.Client.Engine/Commands/UninstallPackageCommand.cs | 7+++----
Msrc/PowerShell/Microsoft.WinGet.Client.Engine/Helpers/PSEnumHelpers.cs | 19++++++++++++++++++-
Msrc/PowerShell/Microsoft.WinGet.Client.Engine/Microsoft.WinGet.Client.Engine.csproj | 6+++---
Msrc/PowerShell/Microsoft.WinGet.Client.Engine/PSObjects/PSCatalogPackage.cs | 1+
Asrc/PowerShell/Microsoft.WinGet.Client.Engine/PSObjects/PSCompareResult.cs | 34++++++++++++++++++++++++++++++++++
Msrc/PowerShell/Microsoft.WinGet.Client.Engine/PSObjects/PSInstallResult.cs | 19+++++++++++++++++++
Msrc/PowerShell/Microsoft.WinGet.Client.Engine/PSObjects/PSInstalledCatalogPackage.cs | 18++++++++++++++++++
Msrc/PowerShell/Microsoft.WinGet.Client.Engine/PSObjects/PSSourceResult.cs | 4+++-
Msrc/PowerShell/Microsoft.WinGet.Client.Engine/PSObjects/PSUninstallResult.cs | 22+++++++++++++++++++++-
Msrc/PowerShell/Microsoft.WinGet.DSC/Microsoft.WinGet.DSC.psd1 | 1+
Msrc/PowerShell/Microsoft.WinGet.DSC/Microsoft.WinGet.DSC.psm1 | 263++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++---
Asrc/PowerShell/scripts/samples/WinGetPackageResourceSample.ps1 | 76++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
27 files changed, 521 insertions(+), 59 deletions(-)

diff --git a/src/PowerShell/Microsoft.WinGet.Client.Cmdlets/Cmdlets/Common/FinderCmdlet.cs b/src/PowerShell/Microsoft.WinGet.Client.Cmdlets/Cmdlets/Common/FinderCmdlet.cs @@ -8,6 +8,7 @@ namespace Microsoft.WinGet.Client.Commands.Common { using System.Management.Automation; using Microsoft.WinGet.Client.Common; + using Microsoft.WinGet.Client.PSObjects; /// <summary> /// This is the base class for all commands that might need to search for a package. It contains an initial @@ -58,11 +59,11 @@ namespace Microsoft.WinGet.Client.Commands.Common public string[] Query { get; set; } /// <summary> - /// Gets or sets a value indicating whether to match exactly against package fields. + /// Gets or sets how to match against package fields. Default ContainsCaseInsensitive. /// </summary> [Parameter( ParameterSetName = Constants.FoundSet, ValueFromPipelineByPropertyName = true)] - public SwitchParameter Exact { get; set; } + public PSPackageFieldMatchOption MatchOption { get; set; } = PSPackageFieldMatchOption.ContainsCaseInsensitive; } } diff --git a/src/PowerShell/Microsoft.WinGet.Client.Cmdlets/Cmdlets/Common/InstallCmdlet.cs b/src/PowerShell/Microsoft.WinGet.Client.Cmdlets/Cmdlets/Common/InstallCmdlet.cs @@ -8,7 +8,7 @@ namespace Microsoft.WinGet.Client.Commands.Common { using System.IO; using System.Management.Automation; - using Microsoft.WinGet.Client.Engine.PSObjects; + using Microsoft.WinGet.Client.PSObjects; /// <summary> /// This is the base class for all commands that parse a FindPackagesOptions result diff --git a/src/PowerShell/Microsoft.WinGet.Client.Cmdlets/Cmdlets/FindPackageCmdlet.cs b/src/PowerShell/Microsoft.WinGet.Client.Cmdlets/Cmdlets/FindPackageCmdlet.cs @@ -31,7 +31,7 @@ namespace Microsoft.WinGet.Client.Commands this.Moniker, this.Source, this.Query, - this.Exact.ToBool(), + this.MatchOption.ToString(), this.Tag, this.Command, this.Count); diff --git a/src/PowerShell/Microsoft.WinGet.Client.Cmdlets/Cmdlets/GetPackageCmdlet.cs b/src/PowerShell/Microsoft.WinGet.Client.Cmdlets/Cmdlets/GetPackageCmdlet.cs @@ -31,7 +31,7 @@ namespace Microsoft.WinGet.Client.Commands this.Moniker, this.Source, this.Query, - this.Exact.ToBool(), + this.MatchOption.ToString(), this.Tag, this.Command, this.Count); diff --git a/src/PowerShell/Microsoft.WinGet.Client.Cmdlets/Cmdlets/InstallPackageCmdlet.cs b/src/PowerShell/Microsoft.WinGet.Client.Cmdlets/Cmdlets/InstallPackageCmdlet.cs @@ -6,11 +6,12 @@ namespace Microsoft.WinGet.Client.Commands { - using System.Management.Automation; - using Microsoft.WinGet.Client.Commands.Common; - using Microsoft.WinGet.Client.Common; - using Microsoft.WinGet.Client.Engine.Commands; + using System.Management.Automation; + using Microsoft.WinGet.Client.Commands.Common; + using Microsoft.WinGet.Client.Common; + using Microsoft.WinGet.Client.Engine.Commands; using Microsoft.WinGet.Client.Engine.PSObjects; + using Microsoft.WinGet.Client.PSObjects; /// <summary> /// Installs a package from the pipeline or from a configured source. @@ -57,7 +58,7 @@ namespace Microsoft.WinGet.Client.Commands this.Moniker, this.Source, this.Query, - this.Exact.ToBool()); + this.MatchOption.ToString()); command.Install(this.Scope.ToString(), this.Architecture.ToString()); } } diff --git a/src/PowerShell/Microsoft.WinGet.Client.Cmdlets/Cmdlets/UninstallPackageCmdlet.cs b/src/PowerShell/Microsoft.WinGet.Client.Cmdlets/Cmdlets/UninstallPackageCmdlet.cs @@ -6,11 +6,12 @@ namespace Microsoft.WinGet.Client.Commands { - using System.Management.Automation; - using Microsoft.WinGet.Client.Commands.Common; - using Microsoft.WinGet.Client.Common; - using Microsoft.WinGet.Client.Engine.Commands; + using System.Management.Automation; + using Microsoft.WinGet.Client.Commands.Common; + using Microsoft.WinGet.Client.Common; + using Microsoft.WinGet.Client.Engine.Commands; using Microsoft.WinGet.Client.Engine.PSObjects; + using Microsoft.WinGet.Client.PSObjects; /// <summary> /// Uninstalls a package from the local system. @@ -50,7 +51,7 @@ namespace Microsoft.WinGet.Client.Commands this.Moniker, this.Source, this.Query, - this.Exact.ToBool()); + this.MatchOption.ToString()); command.Uninstall(this.Mode.ToString(), this.Force.ToBool()); } } diff --git a/src/PowerShell/Microsoft.WinGet.Client.Cmdlets/Cmdlets/UpdatePackageCmdlet.cs b/src/PowerShell/Microsoft.WinGet.Client.Cmdlets/Cmdlets/UpdatePackageCmdlet.cs @@ -51,7 +51,7 @@ namespace Microsoft.WinGet.Client.Commands this.Moniker, this.Source, this.Query, - this.Exact.ToBool()); + this.MatchOption.ToString()); command.Update(this.IncludeUnknown.ToBool()); } } diff --git a/src/PowerShell/Microsoft.WinGet.Client.Cmdlets/PSObjects/PSPackageFieldMatchOption.cs b/src/PowerShell/Microsoft.WinGet.Client.Cmdlets/PSObjects/PSPackageFieldMatchOption.cs @@ -0,0 +1,34 @@ +// ----------------------------------------------------------------------------- +// <copyright file="PSPackageFieldMatchOption.cs" company="Microsoft Corporation"> +// Copyright (c) Microsoft Corporation. Licensed under the MIT License. +// </copyright> +// ----------------------------------------------------------------------------- + +namespace Microsoft.WinGet.Client.PSObjects +{ + /// <summary> + /// This should mimic Microsoft.Management.Deployment.PackageFileMatchOption. + /// </summary> + public enum PSPackageFieldMatchOption + { + /// <summary> + /// Equals. + /// </summary> + Equals, + + /// <summary> + /// EqualsCaseInsensitive. + /// </summary> + EqualsCaseInsensitive, + + /// <summary> + /// StartsWithCaseInsensitive. + /// </summary> + StartsWithCaseInsensitive, + + /// <summary> + /// ContainsCaseInsensitive + /// </summary> + ContainsCaseInsensitive, + } +} diff --git a/src/PowerShell/Microsoft.WinGet.Client.Cmdlets/PSObjects/PSPackageInstallMode.cs b/src/PowerShell/Microsoft.WinGet.Client.Cmdlets/PSObjects/PSPackageInstallMode.cs @@ -4,7 +4,7 @@ // </copyright> // ----------------------------------------------------------------------------- -namespace Microsoft.WinGet.Client.Engine.PSObjects +namespace Microsoft.WinGet.Client.PSObjects { /// <summary> /// This should mimic Microsoft.Management.Deployment.PackageInstallMode. diff --git a/src/PowerShell/Microsoft.WinGet.Client.Cmdlets/PSObjects/PSPackageInstallScope.cs b/src/PowerShell/Microsoft.WinGet.Client.Cmdlets/PSObjects/PSPackageInstallScope.cs @@ -4,7 +4,7 @@ // </copyright> // ----------------------------------------------------------------------------- -namespace Microsoft.WinGet.Client.Engine.PSObjects +namespace Microsoft.WinGet.Client.PSObjects { /// <summary> /// This must match Microsoft.Management.Deployment.PackageInstallScope. diff --git a/src/PowerShell/Microsoft.WinGet.Client.Cmdlets/PSObjects/PSPackageUninstallMode.cs b/src/PowerShell/Microsoft.WinGet.Client.Cmdlets/PSObjects/PSPackageUninstallMode.cs @@ -4,7 +4,7 @@ // </copyright> // ----------------------------------------------------------------------------- -namespace Microsoft.WinGet.Client.Engine.PSObjects +namespace Microsoft.WinGet.Client.PSObjects { /// <summary> /// Must match Microsoft.Management.Deployment.PackageUninstallMode. diff --git a/src/PowerShell/Microsoft.WinGet.Client.Cmdlets/PSObjects/PSProcessorArchitecture.cs b/src/PowerShell/Microsoft.WinGet.Client.Cmdlets/PSObjects/PSProcessorArchitecture.cs @@ -4,7 +4,7 @@ // </copyright> // ----------------------------------------------------------------------------- -namespace Microsoft.WinGet.Client.Engine.PSObjects +namespace Microsoft.WinGet.Client.PSObjects { /// <summary> /// The processor architecture of the package to install. diff --git a/src/PowerShell/Microsoft.WinGet.Client.Engine/Commands/Common/FinderCommand.cs b/src/PowerShell/Microsoft.WinGet.Client.Engine/Commands/Common/FinderCommand.cs @@ -54,14 +54,14 @@ namespace Microsoft.WinGet.Client.Engine.Commands.Common protected string Source { get; set; } /// <summary> - /// Gets or sets the strings that match against every field of a package. + /// Gets or sets how to match against package fields. /// </summary> protected string[] Query { get; set; } /// <summary> /// Gets or sets a value indicating whether to match exactly against package fields. /// </summary> - protected bool Exact { get; set; } + protected PackageFieldMatchOption MatchOption { get; set; } private string QueryAsJoinedString { @@ -74,17 +74,6 @@ namespace Microsoft.WinGet.Client.Engine.Commands.Common } /// <summary> - /// Returns a <see cref="PackageFieldMatchOption" /> based on a parameter. - /// </summary> - /// <returns>A <see cref="PackageFieldMatchOption" /> value.</returns> - protected virtual PackageFieldMatchOption GetExactAsMatchOption() - { - return this.Exact - ? PackageFieldMatchOption.Equals - : PackageFieldMatchOption.ContainsCaseInsensitive; - } - - /// <summary> /// Searches for packages based on the configured parameters. /// </summary> /// <param name="behavior">The <see cref="CompositeSearchBehavior" /> value.</param> @@ -172,8 +161,8 @@ namespace Microsoft.WinGet.Client.Engine.Commands.Common private FindPackagesOptions GetFindPackagesOptions(uint limit) { var options = ComObjectFactory.Value.CreateFindPackagesOptions(); - SetQueryInFindPackagesOptions(ref options, this.GetExactAsMatchOption(), this.QueryAsJoinedString); - this.AddAttributedFiltersToFindPackagesOptions(ref options, this.GetExactAsMatchOption()); + SetQueryInFindPackagesOptions(ref options, this.MatchOption, this.QueryAsJoinedString); + this.AddAttributedFiltersToFindPackagesOptions(ref options, this.MatchOption); options.ResultLimit = limit; return options; } diff --git a/src/PowerShell/Microsoft.WinGet.Client.Engine/Commands/FinderPackageCommand.cs b/src/PowerShell/Microsoft.WinGet.Client.Engine/Commands/FinderPackageCommand.cs @@ -9,6 +9,8 @@ namespace Microsoft.WinGet.Client.Engine.Commands using System.Management.Automation; using Microsoft.Management.Deployment; using Microsoft.WinGet.Client.Engine.Commands.Common; + using Microsoft.WinGet.Client.Engine.Extensions; + using Microsoft.WinGet.Client.Engine.Helpers; using Microsoft.WinGet.Client.Engine.PSObjects; /// <summary> @@ -25,7 +27,7 @@ namespace Microsoft.WinGet.Client.Engine.Commands /// <param name="moniker">Moniker of package.</param> /// <param name="source">Source to search. If null, all are searched.</param> /// <param name="query">Match against any field of a package.</param> - /// <param name="exact">Do exact match.</param> + /// <param name="matchOption">Match option.</param> /// <param name="tag">Tag of the package.</param> /// <param name="command">Command of the package.</param> /// <param name="count">Max results to return.</param> @@ -36,7 +38,7 @@ namespace Microsoft.WinGet.Client.Engine.Commands string moniker, string source, string[] query, - bool exact, + string matchOption, string tag, string command, uint count) @@ -48,7 +50,7 @@ namespace Microsoft.WinGet.Client.Engine.Commands this.Moniker = moniker; this.Source = source; this.Query = query; - this.Exact = exact; + this.MatchOption = PSEnumHelpers.ToPackageFieldMatchOption(matchOption); // FinderExtendedCommand this.Tag = tag; diff --git a/src/PowerShell/Microsoft.WinGet.Client.Engine/Commands/InstallerPackageCommand.cs b/src/PowerShell/Microsoft.WinGet.Client.Engine/Commands/InstallerPackageCommand.cs @@ -39,7 +39,7 @@ namespace Microsoft.WinGet.Client.Engine.Commands /// <param name="moniker">Moniker of package.</param> /// <param name="source">Source to search. If null, all are searched.</param> /// <param name="query">Match against any field of a package.</param> - /// <param name="exact">Do exact match.</param> + /// <param name="matchOption">Match option.</param> public InstallerPackageCommand( PSCmdlet psCmdlet, string psInstallMode, @@ -57,7 +57,7 @@ namespace Microsoft.WinGet.Client.Engine.Commands string moniker, string source, string[] query, - bool exact) + string matchOption) : base(psCmdlet) { // InstallCommand. @@ -84,7 +84,7 @@ namespace Microsoft.WinGet.Client.Engine.Commands this.Moniker = moniker; this.Source = source; this.Query = query; - this.Exact = exact; + this.MatchOption = PSEnumHelpers.ToPackageFieldMatchOption(matchOption); } /// <summary> diff --git a/src/PowerShell/Microsoft.WinGet.Client.Engine/Commands/UninstallPackageCommand.cs b/src/PowerShell/Microsoft.WinGet.Client.Engine/Commands/UninstallPackageCommand.cs @@ -10,7 +10,6 @@ namespace Microsoft.WinGet.Client.Engine.Commands using System.Management.Automation; using Microsoft.Management.Deployment; using Microsoft.WinGet.Client.Engine.Commands.Common; - using Microsoft.WinGet.Client.Engine.Extensions; using Microsoft.WinGet.Client.Engine.Helpers; using Microsoft.WinGet.Client.Engine.Properties; using Microsoft.WinGet.Client.Engine.PSObjects; @@ -32,7 +31,7 @@ namespace Microsoft.WinGet.Client.Engine.Commands /// <param name="moniker">Moniker of package.</param> /// <param name="source">Source to search. If null, all are searched.</param> /// <param name="query">Match against any field of a package.</param> - /// <param name="exact">Do exact match.</param> + /// <param name="matchOption">Match option.</param> public UninstallPackageCommand( PSCmdlet psCmdlet, PSCatalogPackage psCatalogPackage, @@ -43,7 +42,7 @@ namespace Microsoft.WinGet.Client.Engine.Commands string moniker, string source, string[] query, - bool exact) + string matchOption) : base(psCmdlet) { // PackageCommand. @@ -61,7 +60,7 @@ namespace Microsoft.WinGet.Client.Engine.Commands this.Moniker = moniker; this.Source = source; this.Query = query; - this.Exact = exact; + this.MatchOption = PSEnumHelpers.ToPackageFieldMatchOption(matchOption); } /// <summary> diff --git a/src/PowerShell/Microsoft.WinGet.Client.Engine/Helpers/PSEnumHelpers.cs b/src/PowerShell/Microsoft.WinGet.Client.Engine/Helpers/PSEnumHelpers.cs @@ -67,7 +67,7 @@ namespace Microsoft.WinGet.Client.Engine.Helpers } /// <summary> - /// Converts PSPackageUninstallMode to PackageUninstallMode. + /// Converts PSPackageUninstallMode string value to PackageUninstallMode. /// </summary> /// <param name="value">PSPackageUninstallMode string value.</param> /// <returns>PackageUninstallMode.</returns> @@ -81,5 +81,22 @@ namespace Microsoft.WinGet.Client.Engine.Helpers _ => throw new InvalidOperationException(), }; } + + /// <summary> + /// Converts PSPackageFieldMatchOption string value to PackageFieldMatchOption. + /// </summary> + /// <param name="value">PSPackageFieldMatchOption string value.</param> + /// <returns>PackageFieldMatchOption.</returns> + public static PackageFieldMatchOption ToPackageFieldMatchOption(string value) + { + return value switch + { + "Equals" => PackageFieldMatchOption.Equals, + "EqualsCaseInsensitive" => PackageFieldMatchOption.EqualsCaseInsensitive, + "StartsWithCaseInsensitive" => PackageFieldMatchOption.StartsWithCaseInsensitive, + "ContainsCaseInsensitive" => PackageFieldMatchOption.ContainsCaseInsensitive, + _ => throw new InvalidOperationException(), + }; + } } } diff --git a/src/PowerShell/Microsoft.WinGet.Client.Engine/Microsoft.WinGet.Client.Engine.csproj b/src/PowerShell/Microsoft.WinGet.Client.Engine/Microsoft.WinGet.Client.Engine.csproj @@ -136,7 +136,7 @@ </PropertyGroup> <!-- Copy unmanaged dlls that are not copied in the build server --> - <Target Name="CopyWinRTAct" AfterTargets="AfterBuild" > + <Target Name="CopyWinRTAct" AfterTargets="AfterBuild"> <PropertyGroup> <WinRTActDir>$(OutputPath)..\..\..\UndockedRegFreeWinRT\</WinRTActDir> <!-- The output path for this project is different on some builds where the RIDs are not used in the output. --> @@ -149,7 +149,7 @@ <Copy SourceFiles="@(WinRTActDll)" DestinationFolder="$(OutputPath)" /> </Target> - <Target Name="CopyDeploymentInproc" AfterTargets="AfterBuild" > + <Target Name="CopyDeploymentInproc" AfterTargets="AfterBuild"> <PropertyGroup> <DeploymentInprocDir>$(OutputPath)..\..\..\Microsoft.Management.Deployment.InProc\</DeploymentInprocDir> <!-- The output path for this project is different on some builds where the RIDs are not used in the output. --> @@ -162,7 +162,7 @@ <Copy SourceFiles="@(DeploymentInprocDll)" DestinationFolder="$(OutputPath)" /> </Target> - <Target Name="CopyWindowsPackageManager" AfterTargets="AfterBuild" > + <Target Name="CopyWindowsPackageManager" AfterTargets="AfterBuild"> <PropertyGroup> <WindowsPackageManagerDir>$(OutputPath)..\..\..\WindowsPackageManager\</WindowsPackageManagerDir> <!-- The output path for this project is different on some builds where the RIDs are not used in the output. --> diff --git a/src/PowerShell/Microsoft.WinGet.Client.Engine/PSObjects/PSCatalogPackage.cs b/src/PowerShell/Microsoft.WinGet.Client.Engine/PSObjects/PSCatalogPackage.cs @@ -7,6 +7,7 @@ namespace Microsoft.WinGet.Client.Engine.PSObjects { using System.Linq; + using System.Management.Automation; using Microsoft.Management.Deployment; using Microsoft.WinGet.Client.Engine.Exceptions; diff --git a/src/PowerShell/Microsoft.WinGet.Client.Engine/PSObjects/PSCompareResult.cs b/src/PowerShell/Microsoft.WinGet.Client.Engine/PSObjects/PSCompareResult.cs @@ -0,0 +1,34 @@ +// ----------------------------------------------------------------------------- +// <copyright file="PSCompareResult.cs" company="Microsoft Corporation"> +// Copyright (c) Microsoft Corporation. Licensed under the MIT License. +// </copyright> +// ----------------------------------------------------------------------------- + +namespace Microsoft.WinGet.Client.Engine.PSObjects +{ + /// <summary> + /// Must match Microsoft.Management.Deployment.CompareResult. + /// </summary> + public enum PSCompareResult + { + /// <summary> + /// Unknown, + /// </summary> + Unknown, + + /// <summary> + /// Lesser. + /// </summary> + Lesser, + + /// <summary> + /// Equal, + /// </summary> + Equal, + + /// <summary> + /// Greater, + /// </summary> + Greater, + } +} diff --git a/src/PowerShell/Microsoft.WinGet.Client.Engine/PSObjects/PSInstallResult.cs b/src/PowerShell/Microsoft.WinGet.Client.Engine/PSObjects/PSInstallResult.cs @@ -7,6 +7,7 @@ namespace Microsoft.WinGet.Client.Engine.PSObjects { using System; + using System.Management.Automation; using Microsoft.Management.Deployment; /// <summary> @@ -79,5 +80,23 @@ namespace Microsoft.WinGet.Client.Engine.PSObjects return this.installResult.Status.ToString(); } } + + /// <summary> + /// If the installation succeeded. + /// </summary> + /// <returns>True if installation succeeded.</returns> + public bool Succeeded() + { + return this.installResult.Status == InstallResultStatus.Ok; + } + + /// <summary> + /// Message with error information. + /// </summary> + /// <returns>Error message.</returns> + public string ErrorMessage() + { + return $"InstallStatus '{this.Status}' InstallerErrorCode '{this.InstallerErrorCode}' ExtendedError '{this.ExtendedErrorCode.HResult}'"; + } } } diff --git a/src/PowerShell/Microsoft.WinGet.Client.Engine/PSObjects/PSInstalledCatalogPackage.cs b/src/PowerShell/Microsoft.WinGet.Client.Engine/PSObjects/PSInstalledCatalogPackage.cs @@ -6,6 +6,7 @@ namespace Microsoft.WinGet.Client.Engine.PSObjects { + using System; using Microsoft.Management.Deployment; /// <summary> @@ -29,5 +30,22 @@ namespace Microsoft.WinGet.Client.Engine.PSObjects { get { return this.CatalogPackageCOM.InstalledVersion.Version; } } + + /// <summary> + /// Compares versions. + /// </summary> + /// <param name="version">Version.</param> + /// <returns>PSCompareResult.</returns> + public PSCompareResult CompareToVersion(string version) + { + return this.CatalogPackageCOM.InstalledVersion.CompareToVersion(version) switch + { + CompareResult.Unknown => PSCompareResult.Unknown, + CompareResult.Lesser => PSCompareResult.Lesser, + CompareResult.Equal => PSCompareResult.Equal, + CompareResult.Greater => PSCompareResult.Greater, + _ => throw new InvalidOperationException(), + }; + } } } diff --git a/src/PowerShell/Microsoft.WinGet.Client.Engine/PSObjects/PSSourceResult.cs b/src/PowerShell/Microsoft.WinGet.Client.Engine/PSObjects/PSSourceResult.cs @@ -6,10 +6,12 @@ namespace Microsoft.WinGet.Client.Engine.PSObjects { + using System.Management.Automation; + /// <summary> /// SourceResult wrapper object for displaying to PowerShell. /// </summary> - public class PSSourceResult + public sealed class PSSourceResult { /// <summary> /// Initializes a new instance of the <see cref="PSSourceResult"/> class. diff --git a/src/PowerShell/Microsoft.WinGet.Client.Engine/PSObjects/PSUninstallResult.cs b/src/PowerShell/Microsoft.WinGet.Client.Engine/PSObjects/PSUninstallResult.cs @@ -7,13 +7,15 @@ namespace Microsoft.WinGet.Client.Engine.PSObjects { using System; + using System.Management.Automation; + using Microsoft.Management.Deployment; /// <summary> /// UninstallResult wrapper object for displaying to PowerShell. /// </summary> public sealed class PSUninstallResult { - private Management.Deployment.UninstallResult uninstallResult; + private readonly Management.Deployment.UninstallResult uninstallResult; /// <summary> /// Initializes a new instance of the <see cref="PSUninstallResult"/> class. @@ -78,5 +80,23 @@ namespace Microsoft.WinGet.Client.Engine.PSObjects return this.uninstallResult.UninstallerErrorCode; } } + + /// <summary> + /// If the uninstall succeeded. + /// </summary> + /// <returns>True if uninstall succeeded.</returns> + public bool Succeeded() + { + return this.uninstallResult.Status == UninstallResultStatus.Ok; + } + + /// <summary> + /// Message with error information. + /// </summary> + /// <returns>Error message.</returns> + public string ErrorMessage() + { + return $"UninstallStatus '{this.Status}' UninstallerErrorCode '{this.UninstallerErrorCode}' ExtendedError '{this.ExtendedErrorCode.HResult}'"; + } } } diff --git a/src/PowerShell/Microsoft.WinGet.DSC/Microsoft.WinGet.DSC.psd1 b/src/PowerShell/Microsoft.WinGet.DSC/Microsoft.WinGet.DSC.psd1 @@ -86,6 +86,7 @@ 'WinGetAdminSettings' 'WinGetSources' 'WinGetPackageManager' + 'WinGetPackage' ) # List of all modules packaged with this module diff --git a/src/PowerShell/Microsoft.WinGet.DSC/Microsoft.WinGet.DSC.psm1 b/src/PowerShell/Microsoft.WinGet.DSC/Microsoft.WinGet.DSC.psm1 @@ -34,6 +34,21 @@ enum Ensure Present } +enum MatchOption +{ + Equals + EqualsCaseInsensitive + StartsWithCaseInsensitive + ContainsCaseInsensitive +} + +enum InstallMode +{ + Default + Silent + Interactive +} + #endregion enums #region DscResources @@ -74,12 +89,16 @@ class WinGetUserSettings { Assert-WinGetCommand "Test-WinGetUserSettings" + $hashArgs = @{ + UserSettings = $this.Settings + } + if ($this.Action -eq [WinGetAction]::Partial) { - return Test-WinGetUserSettings -UserSettings $this.Settings -IgnoreNotSet + $hashArgs.Add('IgnoreNotSet', $true) } - return Test-WinGetUserSettings -UserSettings $this.Settings + return Test-WinGetUserSettings @hashArgs } # Sets the desired properties. @@ -87,14 +106,16 @@ class WinGetUserSettings { Assert-WinGetCommand "Set-WinGetUserSettings" - if ($this.Action -eq [WinGetAction]::Partial) - { - Set-WinGetUserSettings -UserSettings $this.Settings -Merge | Out-Null + $hashArgs = @{ + UserSettings = $this.Settings } - else + + if ($this.Action -eq [WinGetAction]::Partial) { - Set-WinGetUserSettings -UserSettings $this.Settings | Out-Null + $hashArgs.Add('Merge', $true) } + + Set-WinGetUserSettings @hashArgs } } @@ -232,11 +253,13 @@ class WinGetSources # Require Name and Arg. if ((-not $source.ContainsKey("Name")) -or [string]::IsNullOrWhiteSpace($source.Name)) { + # TODO: Localize. throw "Invalid source input. Name is required." } if ((-not $source.ContainsKey("Arg")) -or [string]::IsNullOrWhiteSpace($source.Arg)) { + # TODO: Localize. throw "Invalid source input. Arg is required." } @@ -274,11 +297,13 @@ class WinGetSources # Require Name and Arg. if ((-not $source.ContainsKey("Name")) -or [string]::IsNullOrWhiteSpace($source.Name)) { + # TODO: Localize. throw "Invalid source input. Name is required." } if ((-not $source.ContainsKey("Arg")) -or [string]::IsNullOrWhiteSpace($source.Arg)) { + # TODO: Localize. throw "Invalid source input. Arg is required." } @@ -289,7 +314,7 @@ class WinGetSources if ($this.Ensure -eq [Ensure]::Present) { - Add-WinGetSource -Name $source.Name -Argument $source.Argument -Type $source.Type + Add-WinGetSource -Name $source.Name -Argument $source.Argument -Type $sourceType if ($this.Reset) { @@ -392,10 +417,232 @@ class WinGetPackageManager if ($result -ne 0) { + # TODO: Localize. throw "Failed to repair winget. Result $result" } } } } +[DSCResource()] +class WinGetPackage +{ + [DscProperty(Key, Mandatory)] + [string]$Id + + [DscProperty()] + [string]$Version + + [DscProperty()] + [string]$Source + + [DscProperty()] + [Ensure]$Ensure = [Ensure]::Present + + [DscProperty()] + [MatchOption]$MatchOption = [MatchOption]::EqualsCaseInsensitive + + [DscProperty()] + [bool]$UseLatest = $false + + [DSCProperty()] + [InstallMode]$InstallMode = [InstallMode]::Silent + + [DscProperty(NotConfigurable)] + [string]$InstalledVersion + + [DscProperty(NotConfigurable)] + [bool]$IsInstalled = $false + + [DscProperty(NotConfigurable)] + [bool]$IsUpdateAvailable = $false + + [PSObject] hidden $CatalogPackage = $null + + hidden Initialize() + { + # DSC only validates keys and mandatories in a Set call. + if ([string]::IsNullOrWhiteSpace($this.Id)) + { + # TODO: Localize. + throw "WinGetPackage: Id is required" + } + + if (($this.UseLatest -eq $true) -and (-not[string]::IsNullOrWhiteSpace($this.Version))) + { + # TODO: Localize. + throw "WinGetPackage: Version and UseLatest cannot be set at the same time" + } + + # This has to use MatchOption equals. Otherwise, it might find other package where the + # id starts with. + $this.CatalogPackage = Get-WinGetPackage -Id $this.Id -MatchOption $this.MatchOption + if ($null -ne $this.CatalogPackage) + { + $this.InstalledVersion = $this.CatalogPackage.InstalledVersion + $this.IsInstalled = $true + $this.IsUpdateAvailable = $this.CatalogPackage.IsUpdateAvailable + } + } + + # Get. + [WinGetPackage] Get() + { + Assert-WinGetCommand "Get-WinGetPackage" + $this.Initialize() + return $this + } + + # Test. + [bool] Test() + { + $this.Initialize() + $ensureInstalled = $this.Ensure -eq [Ensure]::Present + + # Not installed, doesn't have to. + if (-not($this.IsInstalled -or $ensureInstalled)) + { + return $true + } + + # Not install, need to ensure installed. + # Installed, need to ensure not installed. + if ($this.IsInstalled -ne $ensureInstalled) + { + return $false + } + + # At this point we know is installed. + # If asked for latests, but there are updates available. + if ($this.UseLatest -and + $this.CatalogPackage.IsUpdateAvailable) + { + return $false + } + + # If there is an specific version, compare with the current installed version. + if (-not ([string]::IsNullOrWhiteSpace($this.Version))) + { + $compareResult = $this.CatalogPackage.CompareToVersion($this.Version) + if ($compareResult -ne 'Equal') + { + return $false + } + } + + # For now this is all. + return $true + } + + # Set. + [void] Set() + { + Assert-WinGetCommand "Install-WinGetPackage" + Assert-WinGetCommand "Uninstall-WinGetPackage" + + if (-not $this.Test()) + { + $hashArgs = @{ + Id = $this.Id + MatchOption = $this.MatchOption + Mode = $this.InstallMode + } + + if ($this.Ensure -eq [Ensure]::Present) + { + if (-not([string]::IsNullOrWhiteSpace($this.Source))) + { + $hashArgs.Add("Source", $this.Source) + } + + if ($this.IsInstalled) + { + if ($this.UseLatest) + { + $this.TryUpdate($hashArgs) + } + elseif (-not([string]::IsNullOrWhiteSpace($this.Version))) + { + $hashArgs.Add("Version", $this.Version) + + $compareResult = $this.CatalogPackage.CompareToVersion($this.Version) + switch ($compareResult) + { + 'Lesser' + { + $this.TryUpdate($hashArgs) + break + } + {'Greater' -or 'Unknown'} + { + # The installed package has a greater version or unknown. Uninstall and install. + $this.Uninstall() + $this.Install($hashArgs) + break + } + } + } + } + else + { + if (-not([string]::IsNullOrWhiteSpace($this.Version))) + { + $hashArgs.Add("Version", $this.Version) + } + + $this.Install($hashArgs) + } + } + else + { + $this.Uninstall() + } + } + } + + hidden Install([Hashtable]$hashArgs) + { + $installResult = Install-WinGetPackage @hashArgs + if (-not $installResult.Succeeded()) + { + # TODO: Localize. + throw "WinGetPackage Failed installing $($this.Id). $($installResult.ErrorMessage())" + } + } + + hidden Uninstall() + { + $uninstallResult = Uninstall-WinGetPackage -PSCatalogPackage $this.CatalogPackage + if (-not $uninstallResult.Succeeded()) + { + # TODO: Localize. + throw "WinGetPackage Failed uninstalling $($this.Id). $($uninstallResult.ErrorMessage())" + } + } + + hidden Update([Hashtable]$hashArgs) + { + $updateResult = Update-WinGetPackage @hashArgs + if (-not $updateResult.Succeeded()) + { + # TODO: Localize. + throw "WinGetPackage Failed updating $($this.Id). $($updateResult.ErrorMessage())" + } + } + + # Tries to update, if not, uninstall and install. + hidden TryUpdate([Hashtable]$hashArgs) + { + try + { + $this.Update($hashArgs) + } + catch + { + $this.Uninstall() + $this.Install($hashArgs) + } + } +} + #endregion DscResources diff --git a/src/PowerShell/scripts/samples/WinGetPackageResourceSample.ps1 b/src/PowerShell/scripts/samples/WinGetPackageResourceSample.ps1 @@ -0,0 +1,75 @@ +# Copyright (c) Microsoft Corporation. All rights reserved. +# Licensed under the MIT License. + +<# + .SYNOPSIS + Simple sample on how to use WinGetPackage DSC resource. + Requires PSDesiredStateConfiguration v2 and enabling the + PSDesiredStateConfiguration.InvokeDscResource experimental feature + `Enable-ExperimentalFeature -Name PSDesiredStateConfiguration.InvokeDscResource` + IMPORTANT: This will install Microsoft.PowerToys. +#> + +#Requires -Modules Microsoft.WinGet.Client, Microsoft.WinGet.DSC + +using module Microsoft.WinGet.DSC + +$resource = @{ + Name = 'WinGetPackage' + ModuleName = 'Microsoft.WinGet.DSC' + Property = @{ + Id = 'Microsoft.PowerToys' + Ensure = "Absent" + } +} + +$testResult = Invoke-DscResource @resource -Method Test +if ($testResult.InDesiredState) +{ + Write-Host "PowerToys is installed." +} +else +{ + Write-Host "PowerToys is not installed." +} + +# Default value of Ensure is present. +$resource = @{ + Name = 'WinGetPackage' + ModuleName = 'Microsoft.WinGet.DSC' + Property = @{ + Id = 'Microsoft.PowerToys' + Version = '0.65.0' + } +} + +$testResult = Invoke-DscResource @resource -Method Test +if ($testResult.InDesiredState) +{ + Write-Host "PowerToys 0.65.0 is installed." +} +else +{ + Write-Host "PowerToys 0.65.0 is not installed." + + Invoke-DscResource @resource -Method Set +} + +$resource = @{ + Name = 'WinGetPackage' + ModuleName = 'Microsoft.WinGet.DSC' + Property = @{ + Id = 'Microsoft.PowerToys' + } +} +$testResult = Invoke-DscResource @resource -Method Test +if ($testResult.InDesiredState) +{ + Write-Host "PowerToys latest version is installed." +} +else +{ + Write-Host "PowerToys latest version is no installed." + + Invoke-DscResource @resource -Method Set +}+ \ No newline at end of file