diff --git a/.github/actions/spelling/expect.txt b/.github/actions/spelling/expect.txt index dc79cbed72..6d917039fb 100644 --- a/.github/actions/spelling/expect.txt +++ b/.github/actions/spelling/expect.txt @@ -93,6 +93,7 @@ ecfr ecfrbrowse EFGH EQU +endregion errmsg ESRB etest @@ -200,6 +201,7 @@ minschema missingdependency MMmmbbbb monicka +MOF MPNS msdownload msft diff --git a/src/PowerShell/Microsoft.WinGet.Client/Crescendo/Crescendo.json b/src/PowerShell/Microsoft.WinGet.Client/Crescendo/Crescendo.json index 1ff08022ab..212d4e2e39 100644 --- a/src/PowerShell/Microsoft.WinGet.Client/Crescendo/Crescendo.json +++ b/src/PowerShell/Microsoft.WinGet.Client/Crescendo/Crescendo.json @@ -58,6 +58,18 @@ } ] }, + { + "Verb": "Get", + "Noun": "WinGetSettings", + "Platform": [ + "Windows" + ], + "OriginalName": "winget.exe", + "OriginalCommandElements": [ + "settings", + "export" + ] + }, { "Verb": "Add", "Noun": "WinGetSource", diff --git a/src/PowerShell/Microsoft.WinGet.Client/Module/Microsoft.WinGet.Client.psd1 b/src/PowerShell/Microsoft.WinGet.Client/Module/Microsoft.WinGet.Client.psd1 index a98556f207..badf730a06 100644 --- a/src/PowerShell/Microsoft.WinGet.Client/Module/Microsoft.WinGet.Client.psd1 +++ b/src/PowerShell/Microsoft.WinGet.Client/Module/Microsoft.WinGet.Client.psd1 @@ -80,7 +80,8 @@ FunctionsToExport = @( 'Disable-WinGetSetting', 'Add-WinGetSource', 'Remove-WinGetSource', - 'Reset-WinGetSource' + 'Reset-WinGetSource', + 'Get-WinGetSettings' ) # Cmdlets to export from this module, for best performance, do not use wildcards and do not delete the entry, use an empty array if there are no cmdlets to export. diff --git a/src/PowerShell/Microsoft.WinGet.Client/Module/Microsoft.WinGet.Client.psm1 b/src/PowerShell/Microsoft.WinGet.Client/Module/Microsoft.WinGet.Client.psm1 index 10c4e11645..497d9c09e1 100644 --- a/src/PowerShell/Microsoft.WinGet.Client/Module/Microsoft.WinGet.Client.psm1 +++ b/src/PowerShell/Microsoft.WinGet.Client/Module/Microsoft.WinGet.Client.psm1 @@ -9,8 +9,22 @@ class PowerShellCustomFunctionAttribute : System.Attribute { } } +<# + .SYNOPSIS + Displays the version of the tool. + .DESCRIPTION + Displays the version of the winget.exe tool. + .INPUTS + None. + + .OUTPUTS + None + + .EXAMPLE + PS> Get-WinGetVersion +#> function Get-WinGetVersion { [PowerShellCustomFunctionAttribute(RequiresElevation=$False)] @@ -79,13 +93,22 @@ PROCESS { } } } # end PROCESS +} <# .SYNOPSIS - Displays the version of the tool. + Enables the WinGet setting specified by the `Name` parameter. .DESCRIPTION - Displays the version of the winget.exe tool. + Enables the WinGet setting specified by the `Name` parameter. + Supported settings: + - LocalManifestFiles + - BypassCertificatePinningForMicrosoftStore + - InstallerHashOverride + - LocalArchiveMalwareScanOverride + + .PARAMETER Name + Specifies the name of the setting to be enabled. .INPUTS None. @@ -94,10 +117,8 @@ PROCESS { None .EXAMPLE - PS> Get-WinGetVersion + PS> Enable-WinGetSetting -name LocalManifestFiles #> -} - function Enable-WinGetSetting { [PowerShellCustomFunctionAttribute(RequiresElevation=$False)] @@ -180,17 +201,22 @@ PROCESS { } } } # end PROCESS +} <# .SYNOPSIS - Enables the WinGet setting specified by the `Name` parameter. + Disables the WinGet setting specified by the `Name` parameter. .DESCRIPTION - Enables the WinGet setting specified by the `Name` parameter. - Supported settings: `LocalManifestFiles` + Disables the WinGet setting specified by the `Name` parameter. + Supported settings: + - LocalManifestFiles + - BypassCertificatePinningForMicrosoftStore + - InstallerHashOverride + - LocalArchiveMalwareScanOverride .PARAMETER Name - Specifies the name of the setting to be enabled. + Specifies the name of the setting to be disabled. .INPUTS None. @@ -199,13 +225,8 @@ PROCESS { None .EXAMPLE - PS> Enable-WinGetSetting -name LocalManifestFiles + PS> Disable-WinGetSetting -name LocalManifestFiles #> -} - - - - function Disable-WinGetSetting { [PowerShellCustomFunctionAttribute(RequiresElevation=$False)] @@ -288,29 +309,125 @@ PROCESS { } } } # end PROCESS +} <# .SYNOPSIS - Disables the WinGet setting specified by the `Name` parameter. + Get winget settings. .DESCRIPTION - Disables the WinGet setting specified by the `Name` parameter. - Supported settings: `LocalManifestFiles` + Get the administrator settings values as well as the location of the user settings as json string .PARAMETER Name - Specifies the name of the setting to be disabled. + None .INPUTS None. .OUTPUTS - None + Prints the export settings json. .EXAMPLE - PS> Disable-WinGetSetting -name LocalManifestFiles + PS> Get-WinGetSettings #> +function Get-WinGetSettings +{ +[PowerShellCustomFunctionAttribute(RequiresElevation=$False)] +[CmdletBinding(SupportsShouldProcess)] + +param( ) + +BEGIN { + $__PARAMETERMAP = @{} + $__outputHandlers = @{ Default = @{ StreamOutput = $true; Handler = { $input } } } +} + +PROCESS { + $__boundParameters = $PSBoundParameters + $__defaultValueParameters = $PSCmdlet.MyInvocation.MyCommand.Parameters.Values.Where({$_.Attributes.Where({$_.TypeId.Name -eq "PSDefaultValueAttribute"})}).Name + $__defaultValueParameters.Where({ !$__boundParameters["$_"] }).ForEach({$__boundParameters["$_"] = get-variable -value $_}) + $__commandArgs = @() + $MyInvocation.MyCommand.Parameters.Values.Where({$_.SwitchParameter -and $_.Name -notmatch "Debug|Whatif|Confirm|Verbose" -and ! $__boundParameters[$_.Name]}).ForEach({$__boundParameters[$_.Name] = [switch]::new($false)}) + if ($__boundParameters["Debug"]){wait-debugger} + $__commandArgs += 'settings' + $__commandArgs += 'export' + foreach ($paramName in $__boundParameters.Keys| + Where-Object {!$__PARAMETERMAP[$_].ApplyToExecutable}| + Sort-Object {$__PARAMETERMAP[$_].OriginalPosition}) { + $value = $__boundParameters[$paramName] + $param = $__PARAMETERMAP[$paramName] + if ($param) { + if ($value -is [switch]) { + if ($value.IsPresent) { + if ($param.OriginalName) { $__commandArgs += $param.OriginalName } + } + elseif ($param.DefaultMissingValue) { $__commandArgs += $param.DefaultMissingValue } + } + elseif ( $param.NoGap ) { + $pFmt = "{0}{1}" + if($value -match "\s") { $pFmt = "{0}""{1}""" } + $__commandArgs += $pFmt -f $param.OriginalName, $value + } + else { + if($param.OriginalName) { $__commandArgs += $param.OriginalName } + $__commandArgs += $value | Foreach-Object {$_} + } + } + } + $__commandArgs = $__commandArgs | Where-Object {$_ -ne $null} + if ($__boundParameters["Debug"]){wait-debugger} + if ( $__boundParameters["Verbose"]) { + Write-Verbose -Verbose -Message winget.exe + $__commandArgs | Write-Verbose -Verbose + } + $__handlerInfo = $__outputHandlers[$PSCmdlet.ParameterSetName] + if (! $__handlerInfo ) { + $__handlerInfo = $__outputHandlers["Default"] # Guaranteed to be present + } + $__handler = $__handlerInfo.Handler + if ( $PSCmdlet.ShouldProcess("winget.exe $__commandArgs")) { + # check for the application and throw if it cannot be found + if ( -not (Get-Command -ErrorAction Ignore "winget.exe")) { + throw "Cannot find executable 'winget.exe'" + } + if ( $__handlerInfo.StreamOutput ) { + & "winget.exe" $__commandArgs | & $__handler + } + else { + $result = & "winget.exe" $__commandArgs + & $__handler $result + } + } + } # end PROCESS } +<# + .SYNOPSIS + Add a new source. + + .DESCRIPTION + Add a new source. A source provides the data for you to discover and install packages. + Only add a new source if you trust it as a secure location. + + .PARAMETER Name + Name of the source. + + .PARAMETER Argument + Argument to be given to the source. + + .PARAMETER Type + Type of the source. + + .INPUTS + None. + + .OUTPUTS + None. + + .EXAMPLE + PS> Add-WinGetSource -Name Contoso -Argument https://www.contoso.com/cache + +#> function Add-WinGetSource { [PowerShellCustomFunctionAttribute(RequiresElevation=$False)] @@ -413,24 +530,18 @@ PROCESS { } } } # end PROCESS +} <# .SYNOPSIS - Add a new source. + Remove a specific source. .DESCRIPTION - Add a new source. A source provides the data for you to discover and install packages. - Only add a new source if you trust it as a secure location. + Remove a specific source. The source must already exist to be removed. .PARAMETER Name Name of the source. - .PARAMETER Argument - Argument to be given to the source. - - .PARAMETER Type - Type of the source. - .INPUTS None. @@ -438,12 +549,9 @@ PROCESS { None. .EXAMPLE - PS> Add-WinGetSource -Name Contoso -Argument https://www.contoso.com/cache + PS> Remove-WinGetSource -Name Contoso #> -} - - function Remove-WinGetSource { [PowerShellCustomFunctionAttribute(RequiresElevation=$False)] @@ -526,13 +634,15 @@ PROCESS { } } } # end PROCESS +} <# .SYNOPSIS - Remove a specific source. + Drops existing sources. Without any argument, this command will drop all sources and add the defaults. .DESCRIPTION - Remove a specific source. The source must already exist to be removed. + Drops existing sources, potentially leaving any local data behind. Without any argument, it will drop all sources and add the defaults. + If a named source is provided, only that source will be dropped. .PARAMETER Name Name of the source. @@ -544,11 +654,12 @@ PROCESS { None. .EXAMPLE - PS> Remove-WinGetSource -Name Contoso + PS> Reset-WinGetSource -#> -} + .EXAMPLE + PS> Reset-WinGetSource -Name Contoso +#> function Reset-WinGetSource { [PowerShellCustomFunctionAttribute(RequiresElevation=$False)] @@ -632,31 +743,6 @@ PROCESS { } } } # end PROCESS - -<# - .SYNOPSIS - Drops existing sources. Without any argument, this command will drop all sources and add the defaults. - - .DESCRIPTION - Drops existing sources, potentially leaving any local data behind. Without any argument, it will drop all sources and add the defaults. - If a named source is provided, only that source will be dropped. - - .PARAMETER Name - Name of the source. - - .INPUTS - None. - - .OUTPUTS - None. - - .EXAMPLE - PS> Reset-WinGetSource - - .EXAMPLE - PS> Reset-WinGetSource -Name Contoso - -#> } diff --git a/src/PowerShell/Microsoft.WinGet.DSC/Microsoft.WinGet.DSC.psd1 b/src/PowerShell/Microsoft.WinGet.DSC/Microsoft.WinGet.DSC.psd1 new file mode 100644 index 0000000000..4fde00d764 --- /dev/null +++ b/src/PowerShell/Microsoft.WinGet.DSC/Microsoft.WinGet.DSC.psd1 @@ -0,0 +1,140 @@ +# +# Module manifest for module 'Microsoft.WinGet.DSC' +# +# Generated by: Microsoft Corporation +# +# Generated on: 11/1/2022 +# + +@{ + + # Script module or binary module file associated with this manifest. + RootModule = 'Microsoft.WinGet.DSC.psm1' + + # Version number of this module. + ModuleVersion = '0.0.1' + + # Supported PSEditions + CompatiblePSEditions = 'Core' + + # ID used to uniquely identify this module + GUID = '8c9326eb-595a-40eb-8696-b289e8085cad' + + # Author of this module + Author = 'Microsoft Corporation' + + # Company or vendor of this module + CompanyName = 'Microsoft Corporation' + + # Copyright statement for this module + Copyright = '(c) Microsoft Corporation. All rights reserved.' + + # Description of the functionality provided by this module + Description = 'PowerShell Module with DSC resources related to WinGet configurations' + + # Minimum version of the PowerShell engine required by this module + PowerShellVersion = '7.2' + + # Name of the PowerShell host required by this module + # PowerShellHostName = '' + + # Minimum version of the PowerShell host required by this module + # PowerShellHostVersion = '' + + # Minimum version of Microsoft .NET Framework required by this module. This prerequisite is valid for the PowerShell Desktop edition only. + # DotNetFrameworkVersion = '' + + # Minimum version of the common language runtime (CLR) required by this module. This prerequisite is valid for the PowerShell Desktop edition only. + # ClrVersion = '' + + # Processor architecture (None, X86, Amd64) required by this module + # ProcessorArchitecture = '' + + # Modules that must be imported into the global environment prior to importing this module + RequiredModules = @('Microsoft.WinGet.Client') + + # Assemblies that must be loaded prior to importing this module + # RequiredAssemblies = @() + + # Script files (.ps1) that are run in the caller's environment prior to importing this module. + # ScriptsToProcess = @() + + # Type files (.ps1xml) to be loaded when importing this module + # TypesToProcess = @() + + # Format files (.ps1xml) to be loaded when importing this module + # FormatsToProcess = @() + + # Modules to import as nested modules of the module specified in RootModule/ModuleToProcess + # NestedModules = @() + + # Functions to export from this module, for best performance, do not use wildcards and do not delete the entry, use an empty array if there are no functions to export. + # FunctionsToExport = @() + + # Cmdlets to export from this module, for best performance, do not use wildcards and do not delete the entry, use an empty array if there are no cmdlets to export. + # CmdletsToExport = @() + + # Variables to export from this module + # VariablesToExport = @() + + # Aliases to export from this module, for best performance, do not use wildcards and do not delete the entry, use an empty array if there are no aliases to export. + # AliasesToExport = @() + + # DSC resources to export from this module + DscResourcesToExport = @( + 'WinGetUserSettingsResource' + 'WinGetAdminSettings' + 'WinGetSourcesResource' + ) + + # List of all modules packaged with this module + # ModuleList = @() + + # List of all files packaged with this module + # FileList = @() + + # Private data to pass to the module specified in RootModule/ModuleToProcess. This may also contain a PSData hashtable with additional module metadata used by PowerShell. + PrivateData = @{ + + PSData = @{ + + # Tags applied to this module. These help with module discovery in online galleries. + Tags = @( + 'PSEdition_Core', + 'Windows', + 'WindowsPackageManager' + ) + + # A URL to the license for this module. + # LicenseUri = '' + + # A URL to the main website for this project. + ProjectUri = 'https://github.com/microsoft/winget-cli' + + # A URL to an icon representing this module. + # IconUri = '' + + # ReleaseNotes of this module + # ReleaseNotes = '' + + # Prerelease string of this module + Prerelease = 'alpha' + + # Flag to indicate whether the module requires explicit user acceptance for install/update/save + # RequireLicenseAcceptance = $false + + # External dependent modules of this module + # ExternalModuleDependencies = @() + + } # End of PSData hashtable + + } # End of PrivateData hashtable + + # HelpInfo URI of this module + # HelpInfoURI = '' + + # Default prefix for commands exported from this module. Override the default prefix using Import-Module -Prefix. + # DefaultCommandPrefix = '' + + } + \ No newline at end of file diff --git a/src/PowerShell/Microsoft.WinGet.DSC/Microsoft.WinGet.DSC.psm1 b/src/PowerShell/Microsoft.WinGet.DSC/Microsoft.WinGet.DSC.psm1 new file mode 100644 index 0000000000..0e1a23842c --- /dev/null +++ b/src/PowerShell/Microsoft.WinGet.DSC/Microsoft.WinGet.DSC.psm1 @@ -0,0 +1,307 @@ +# Copyright (c) Microsoft Corporation. All rights reserved. +# Licensed under the MIT License. + +using namespace System.Collections.Generic + +try +{ + # Load all non-test .ps1 files in the script's directory. + Get-ChildItem -Path $PSScriptRoot\* -Filter *.ps1 -Exclude *.Tests.ps1 -Recurse | ForEach-Object { Import-Module $_.FullName } +} catch +{ + $e = $_.Exception + while ($e.InnerException) + { + $e = $e.InnerException + } + + if (-not [string]::IsNullOrWhiteSpace($e.Message)) + { + Write-Host $e.Message -ForegroundColor Red -BackgroundColor Black + } +} + +#region enums +enum WinGetAction +{ + Partial + Full +} + +enum Ensure +{ + Absent + Present +} + +#endregion enums + +#region DscResources +# Author here all DSC Resources. +# DSC Powershell doesn't support binary DSC resources without the MOF schema. +# DSC Powershell classes aren't discoverable if placed outside of the psm1. + +# This resource is in charge of managing the settings.json file of winget. +[DSCResource()] +class WinGetUserSettingsResource +{ + # We need a key. Do not set. + [DscProperty(Key)] + [string]$SID + + # A hash table with the desired settings. + [DscProperty(Mandatory)] + [Hashtable]$Settings + + [DscProperty()] + [WinGetAction]$Action = [WinGetAction]::Full + + # Gets the current UserSettings by looking at the settings.json file for the current user. + [WinGetUserSettingsResource] Get() + { + Assert-WinGetCommand "Get-WinGetUserSettings" + + $userSettings = Get-WinGetUserSettings + $result = @{ + SID = '' + Settings = $userSettings + } + return $result + } + + # Tests if desired properties match. + [bool] Test() + { + Assert-WinGetCommand "Test-WinGetUserSettings" + + if ($this.Action -eq [WinGetAction]::Partial) + { + return Test-WinGetUserSettings -UserSettings $this.Settings -IgnoreNotSet + } + + return Test-WinGetUserSettings -UserSettings $this.Settings + } + + # Sets the desired properties. + [void] Set() + { + Assert-WinGetCommand "Set-WinGetUserSettings" + + if ($this.Action -eq [WinGetAction]::Partial) + { + Set-WinGetUserSettings -UserSettings $this.Settings -Merge | Out-Null + } + else + { + Set-WinGetUserSettings -UserSettings $this.Settings | Out-Null + } + } +} + +# Handles configuration of administrator settings. +[DSCResource()] +class WinGetAdminSettings +{ + # We need a key. Do not set. + [DscProperty(Key)] + [string]$SID + + # A hash table with the desired admin settings. + [DscProperty(Mandatory)] + [Hashtable]$Settings + + # Gets the administrator settings. + [WinGetAdminSettings] Get() + { + Assert-WinGetCommand "Get-WinGetSettings" + $settingsJson = Get-WinGetSettings | ConvertFrom-Json -AsHashtable + # Get admin setting values. + + $result = @{ + SID = '' + Settings = $settingsJson.adminSettings + } + return $result + } + + # Tests if administrator settings given are set as expected. + # This doesn't do a full comparison to allow users to don't have to update + # their resource every time a new admin setting is added on winget. + [bool] Test() + { + $adminSettings = $this.Get().Settings + foreach ($adminSetting in $adminSettings.GetEnumerator()) + { + if ($this.Settings.ContainsKey($adminSetting.Name)) + { + if ($this.Settings[$adminSetting.Name] -ne $adminSetting.Value) + { + return $false + } + } + } + + return $true + } + + # Sets the desired properties. + [void] Set() + { + Assert-IsAdministrator + Assert-WinGetCommand "Enable-WinGetSetting" + Assert-WinGetCommand "Disable-WinGetSetting" + + # It might be better to implement an internal Test with one value, or + # create a new instances with only one setting than calling Enable/Disable + # for all of them even if only one is different. + if (-not $this.Test()) + { + foreach ($adminSetting in $this.Settings.GetEnumerator()) + { + if ($adminSetting.Value) + { + Enable-WinGetSetting -Name $adminSetting.Name + } + else + { + Disable-WinGetSetting -Name $adminSetting.Name + } + } + } + } +} + +[DSCResource()] +class WinGetSourcesResource +{ + # We need a key. Do not set. + [DscProperty(Key)] + [string]$SID + + # An array of Hashtable with the key value properties that follows the source's group policy schema. + [DscProperty(Mandatory)] + [Hashtable[]]$Sources + + [DscProperty()] + [Ensure]$Ensure = [Ensure]::Present + + [DscProperty()] + [bool]$Reset = $false + + [DscProperty()] + [WinGetAction]$Action = [WinGetAction]::Full + + # Gets the current sources on winget. + [WinGetSourcesResource] Get() + { + Assert-WinGetCommand "Get-WinGetSource" + $packageCatalogReferences = Get-WinGetSource + $wingetSources = [List[Hashtable]]::new() + foreach ($packageCatalogReference in $packageCatalogReferences) + { + $source = @{ + Arg = $packageCatalogReference.Info.Argument + Identifier = $packageCatalogReference.Info.Id + Name = $packageCatalogReference.Info.Name + Type = $packageCatalogReference.Info.Type + } + $wingetSources.Add($source) + } + + $result = @{ + SID = '' + Sources = $wingetSources + } + return $result + } + + # Tests if desired properties match. + [bool] Test() + { + $currentSources = $this.Get().Sources + + # If this is a full match and the counts are different give up. + if (($this.Action -eq [WinGetAction]::Full) -and ($this.Sources.Count -ne $currentSources.Count)) + { + return $false + } + + # There's no need to differentiate between Partial and Full anymore. + foreach ($source in $this.Sources) + { + # Require Name and Arg. + if ((-not $source.ContainsKey("Name")) -or [string]::IsNullOrWhiteSpace($source.Name)) + { + throw "Invalid source input. Name is required." + } + + if ((-not $source.ContainsKey("Arg")) -or [string]::IsNullOrWhiteSpace($source.Arg)) + { + throw "Invalid source input. Arg is required." + } + + # Type has a default value. + $sourceType = "Microsoft.PreIndexed.Package" + if ($source.ContainsKey("Type") -and (-not([string]::IsNullOrWhiteSpace($source.Type)))) + { + $sourceType = $source.Type + } + + $result = $currentSources | Where-Object { $_.Name -eq $source.Name -and $_.Arg -eq $source.Arg -and $_.Type -eq $sourceType } + + # Source not found. + if ($null -eq $result) + { + return $false + } + } + + return $true + } + + # Sets the desired properties. + [void] Set() + { + Assert-IsAdministrator + Assert-WinGetCommand "Add-WinGetSource" + Assert-WinGetCommand "Reset-WinGetSource" + Assert-WinGetCommand "Remove-WinGetSource" + + foreach ($source in $this.Sources) + { + $sourceType = "Microsoft.PreIndexed.Package" + + # Require Name and Arg. + if ((-not $source.ContainsKey("Name")) -or [string]::IsNullOrWhiteSpace($source.Name)) + { + throw "Invalid source input. Name is required." + } + + if ((-not $source.ContainsKey("Arg")) -or [string]::IsNullOrWhiteSpace($source.Arg)) + { + throw "Invalid source input. Arg is required." + } + + if ($source.ContainsKey("Type") -and (-not([string]::IsNullOrWhiteSpace($source.Type)))) + { + $sourceType = $source.Type + } + + if ($this.Ensure -eq [Ensure]::Present) + { + Add-WinGetSource -Name $source.Name -Argument $source.Argument -Type $source.Type + + if ($this.Reset) + { + Reset-WinGetSource -Name $source.Name + } + } + else + { + Remove-WinGetSource -Name $source.Name + } + } + } +} + +#endregion DscResources diff --git a/src/PowerShell/Microsoft.WinGet.DSC/Private/HelperFunctions.ps1 b/src/PowerShell/Microsoft.WinGet.DSC/Private/HelperFunctions.ps1 new file mode 100644 index 0000000000..0af9fabfc3 --- /dev/null +++ b/src/PowerShell/Microsoft.WinGet.DSC/Private/HelperFunctions.ps1 @@ -0,0 +1,22 @@ +# Copyright (c) Microsoft Corporation. All rights reserved. +# Licensed under the MIT License. + +# Check that we are running as an administrator +function Assert-IsAdministrator +{ + $windowsIdentity = [System.Security.Principal.WindowsIdentity]::GetCurrent() + $windowsPrincipal = New-Object -TypeName 'System.Security.Principal.WindowsPrincipal' -ArgumentList @( $windowsIdentity ) + + $adminRole = [System.Security.Principal.WindowsBuiltInRole]::Administrator + + if (-not $windowsPrincipal.IsInRole($adminRole)) + { + New-InvalidOperationException -Message "This resource must run as an Administrator." + } +} + +# Verify the command is present in the Microsoft.WinGet.Client Module +function Assert-WinGetCommand([string]$cmdletName) +{ + $null = Get-Command -Module "Microsoft.WinGet.Client" -Name $cmdletName -ErrorAction Stop +} diff --git a/src/PowerShell/scripts/Initialize-LocalWinGetModules.ps1 b/src/PowerShell/scripts/Initialize-LocalWinGetModules.ps1 index 22dcea5fbd..d5500aaec9 100644 --- a/src/PowerShell/scripts/Initialize-LocalWinGetModules.ps1 +++ b/src/PowerShell/scripts/Initialize-LocalWinGetModules.ps1 @@ -33,12 +33,14 @@ class WinGetModule [string]$Name [string]$ModuleRoot [bool]$HasBinary + [bool]$ForceWinGetDev - WinGetModule([string]$n, [string]$m, [bool]$b) + WinGetModule([string]$n, [string]$m, [bool]$b, [bool]$d) { $this.Name = $n $this.ModuleRoot = $m $this.HasBinary = $b + $this.ForceWinGetDev = $d } } @@ -48,7 +50,8 @@ $moduleRootOutput = "$PSScriptRoot\Module\" # Add here new modules [WinGetModule[]]$modules = - [WinGetModule]::new("Microsoft.WinGet.Client", "$PSScriptRoot\..\Microsoft.WinGet.Client\Module\", $true) + [WinGetModule]::new("Microsoft.WinGet.DSC", "$PSScriptRoot\..\Microsoft.WinGet.DSC\", $false, $false), + [WinGetModule]::new("Microsoft.WinGet.Client", "$PSScriptRoot\..\Microsoft.WinGet.Client\Module\", $true, $true) foreach($module in $modules) { @@ -73,6 +76,16 @@ foreach($module in $modules) Write-Host "Coping module $($module.Name)" -ForegroundColor Green xcopy $module.ModuleRoot "$moduleRootOutput\$($module.Name)\" /d /s /f /y + if ($module.ForceWinGetDev) + { + # This is a terrible and shouldn't be used for real things. We must consider making something smarter and prettier. + # We could make the build system always take the crescendo json and generated the functions from it. The + # build system would know if the original name to be winget.exe or wingetdev.exe, set it on the json and produce + # the psm1 one. We could add a VS after build task that calls powershell and does it, or we could move away + # from crescendo and let the internal implementation knows which one to use based on the build preprocessor macro. + $psm1File = "$moduleRootOutput\$($module.Name)\$($module.Name).psm1" + (Get-Content $psm1File).replace("winget.exe", "wingetdev") | Set-Content $psm1File + } } # Add it to module path if not there. diff --git a/src/PowerShell/scripts/samples/WinGetAdminSettingsResourceSample.ps1 b/src/PowerShell/scripts/samples/WinGetAdminSettingsResourceSample.ps1 new file mode 100644 index 0000000000..21f6556ae9 --- /dev/null +++ b/src/PowerShell/scripts/samples/WinGetAdminSettingsResourceSample.ps1 @@ -0,0 +1,64 @@ +# Copyright (c) Microsoft Corporation. All rights reserved. +# Licensed under the MIT License. + +<# + .SYNOPSIS + Simple sample on how to use WinGetAdminSettings DSC resource. + Requires PSDesiredStateConfiguration v2 and enabling the + PSDesiredStateConfiguration.InvokeDscResource experimental feature + `Enable-ExperimentalFeature -Name PSDesiredStateConfiguration.InvokeDscResource` + IMPORTANT: This will leave LocalManifestFiles enabled + Run as admin for set. +#> + +#Requires -Modules Microsoft.WinGet.Client, Microsoft.WinGet.DSC + +using module Microsoft.WinGet.DSC +using namespace System.Collections.Generic + +$resource = @{ + Name = 'WinGetAdminSettings' + ModuleName = 'Microsoft.WinGet.DSC' + Property = @{ + } +} + +$getResult = Invoke-DscResource @resource -Method Get +Write-Host "Current sources" +$getResult.Settings + +$expectedSources = [List[Hashtable]]::new() +$expectedSources.Add(@{ + Name = "winget" + Arg = "https://cdn.winget.microsoft.com/cache" +}) + +# Lets see if LocalManifestFiles is enabled +$resource.Property = @{ + Settings = @{ + LocalManifestFiles = $true + } +} + +$testResult = Invoke-DscResource @resource -Method Test +if (-not $testResult.InDesiredState) +{ + Write-Host "LocalManifestFiles is disabled, enabling" + Invoke-DscResource @resource -Method Set | Out-Null + + # Now try again + $testResult2 = Invoke-DscResource @resource -Method Test + if (-not $testResult.InDesiredState) + { + Write-Host "LocalManifestFiles is now enabled" + } + else + { + Write-Host "Is there a bug somewhere?" + return + } +} +else +{ + Write-Host "LocalManifestFiles is already enabled" +} diff --git a/src/PowerShell/scripts/samples/WinGetSourcesResourceSample.ps1 b/src/PowerShell/scripts/samples/WinGetSourcesResourceSample.ps1 new file mode 100644 index 0000000000..16460a7fd4 --- /dev/null +++ b/src/PowerShell/scripts/samples/WinGetSourcesResourceSample.ps1 @@ -0,0 +1,113 @@ +# Copyright (c) Microsoft Corporation. All rights reserved. +# Licensed under the MIT License. + +<# + .SYNOPSIS + Simple sample on how to use WinGetSourcesResource DSC resource. + Requires PSDesiredStateConfiguration v2 and enabling the + PSDesiredStateConfiguration.InvokeDscResource experimental feature + `Enable-ExperimentalFeature -Name PSDesiredStateConfiguration.InvokeDscResource` + IMPORTANT: This deletes the main winget source and add it again. + Run as admin for set. +#> + +#Requires -Modules Microsoft.WinGet.Client, Microsoft.WinGet.DSC + +using module Microsoft.WinGet.DSC +using namespace System.Collections.Generic + +[CmdletBinding()] +param ( + [Parameter()] + [string] + $SourceName = "winget", + + [Parameter()] + [string] + $Argument = "https://cdn.winget.microsoft.com/cache", + + [Parameter()] + [string] + $Type = "" +) + +$resource = @{ + Name = 'WinGetSourcesResource' + ModuleName = 'Microsoft.WinGet.DSC' + Property = @{ + } +} + +$getResult = Invoke-DscResource @resource -Method Get +Write-Host "Current sources" + +foreach ($source in $getResult.Sources) +{ + Write-Host "Name '$($source.Name)' Arg '$($source.Arg)' Type '$($source.Type)'" +} + +$expectedSources = [List[Hashtable]]::new() +$expectedSources.Add(@{ + Name = "winget" + Arg = "https://cdn.winget.microsoft.com/cache" +}) + +$resource.Property = @{ + Sources = $expectedSources + Action = [WinGetAction]::Partial +} + +# The default value comparison for test is Partial, so if you have the winget source this should succeed. +$testResult = Invoke-DscResource @resource -Method Test +if ($testResult.InDesiredState) +{ + Write-Host "winget source is present" +} +else +{ + Write-Host "winget source is not present" + return +} + +# A full match will fail if there are more sources. +$resource.Property = @{ + Sources = $expectedSources + Action = [WinGetAction]::Full +} +$testResult = Invoke-DscResource @resource -Method Test +if (-not $testResult.InDesiredState) +{ + Write-Host "winget source is not the only source" +} +else +{ + Write-Host "winget source is the only source" +} + +# Breaking winget. Note this will fail if not run as admin. +$resource.Property = @{ + Sources = $expectedSources + Action = [WinGetAction]::Partial + Ensure = [Ensure]::Absent +} + +Invoke-DscResource @resource -Method Set | Out-Null +Write-Host "winget source removed" + +# Test again +$testResult = Invoke-DscResource @resource -Method Test +if (-not $testResult.InDesiredState) +{ + Write-Host "winget source is gone." + + # Add it again + $resource.Property.Command = [SourceCommand]::Add + Invoke-DscResource @resource -Method Set | Out-Null +} +else +{ + # TODO: debug. Basically when `winget source remove winget` happens if the + # commands prints the progress bar the source was not removed. I think that + # it was actually removed but readded updating the package. + Write-Host "winget was not removed." +} diff --git a/src/PowerShell/scripts/samples/WinGetUserSettingsResourceSample.ps1 b/src/PowerShell/scripts/samples/WinGetUserSettingsResourceSample.ps1 new file mode 100644 index 0000000000..daebd41549 --- /dev/null +++ b/src/PowerShell/scripts/samples/WinGetUserSettingsResourceSample.ps1 @@ -0,0 +1,74 @@ +# Copyright (c) Microsoft Corporation. All rights reserved. +# Licensed under the MIT License. + +<# + .SYNOPSIS + Simple sample on how to use WinGetUserSettings DSC resource. + Requires PSDesiredStateConfiguration v2 and enabling the + PSDesiredStateConfiguration.InvokeDscResource experimental feature + `Enable-ExperimentalFeature -Name PSDesiredStateConfiguration.InvokeDscResource` + + IMPORTANT: If you loaded the released modules this will modify your settings. + Use the -Restore to get back to your original settings + + .PARAMETER Restore + Restore back to the original user settings. +#> + +#Requires -Modules Microsoft.WinGet.Client, Microsoft.WinGet.DSC + +using module Microsoft.WinGet.DSC + +[CmdletBinding()] +param ( + [Parameter()] + [switch] + $Restore +) + +$resource = @{ + Name = 'WinGetUserSettingsResource' + ModuleName = 'Microsoft.WinGet.DSC' + Property = @{ + } +} + +# Get current settings +$getResult = Invoke-DscResource @resource -Method Get +Write-Host "Current Settings" +$settingsBackup = $getResult.Settings +$getResult.Settings | ConvertTo-Json + +# Test if telemetry is disabled +$resource.Property = @{ + Settings = @{ + telemetry = @{ + disable = $false + } + } + # If you want to check that this setting is the only setting set use [WinGetAction]::Full + Action = [WinGetAction]::Partial +} + +$testResult = Invoke-DscResource @resource -Method Test +if (-not $testResult.InDesiredState) +{ + Write-Host "Adding telemetry setting" + Invoke-DscResource @resource -Method Set | Out-Null + + Write-Host "New settings" + $getResult = Invoke-DscResource @resource -Method Get + $getResult.Settings | ConvertTo-Json +} +else +{ + Write-Host "Telemetry is already disabled" +} + +if ($Restore) +{ + $resource.Property.Settings = $settingsBackup + $resource.Property.Action = [WinGetAction]::Full + Invoke-DscResource @resource -Method Set | Out-Null + Write-Host "Settings restored." +}