WinGetExportToDsc.psm1
|
function ConvertTo-YamlString { param([Parameter(Mandatory)][string] $Value) ConvertTo-Json -InputObject $Value -Compress } function Convert-WinGetExportToDsc { <# .SYNOPSIS Converts a WinGet JSON export to a DSC v3 or WinGet DSC YAML configuration. .DESCRIPTION Reads the Sources and Packages arrays produced by winget export. Each package becomes a Microsoft.WinGet/Package resource for DSCv3 or a Microsoft.WinGet.DSC/WinGetPackage resource for WinGet. Resource names use the exact package IDs. Duplicate IDs are rejected. The legacy WinGet schema restricts resource names to letters, numbers, and spaces, so package IDs containing punctuation do not satisfy that restriction. Source names and known versions are preserved as strings. Missing, empty, or Unknown versions are omitted with a warning so DSC can select a version. JSON exports without Version fields are supported without additional options. WinGet resources include ensure: Present; DSCv3 resources do not. No YAML module is required. Existing output files are protected unless -Force is specified. Conversion does not install packages or run DSC. .PARAMETER InputPath Path to a JSON file produced by winget export, regardless of its extension. .PARAMETER OutputPath Destination YAML file. Defaults to <input-name>.dsc.yaml beside the input. .PARAMETER Author Author recorded in configuration metadata. Defaults to the current user name. .PARAMETER Description Description recorded in configuration metadata. .PARAMETER ContentVersion Version string written to contentVersion for DSCv3 (default "0.1.0") or configurationVersion for WinGet (default "0.1.0"). ConfigurationVersion is an alias for this parameter. .PARAMETER Schema Configuration format: DSCv3 (default) or WinGet. Selects the schema URL, resource type, version field, and default version. .PARAMETER Force Allows replacement of an existing output file, but never the input file. .EXAMPLE Convert-WinGetExportToDsc -InputPath D:\Git\winget\sys4ops.json -Author 'Michal' .EXAMPLE Convert-WinGetExportToDsc -InputPath .\packages.json -Schema WinGet -ConfigurationVersion '0.2.0' .OUTPUTS System.IO.FileInfo The generated configuration file. #> [CmdletBinding()] param( [Parameter(Mandatory, Position = 0)] [ValidateNotNullOrEmpty()] [string] $InputPath, [Parameter(Position = 1)] [ValidateNotNullOrEmpty()] [string] $OutputPath, [ValidateNotNullOrEmpty()] [string] $Author = [Environment]::UserName, [ValidateNotNullOrEmpty()] [string] $Description = 'WinGet packages converted from winget export', [Alias('ConfigurationVersion')] [ValidateNotNullOrEmpty()] [string] $ContentVersion, [ValidateSet('DSCv3', 'WinGet')] [string] $Schema = 'DSCv3', [switch] $Force ) $ErrorActionPreference = 'Stop' if ($Schema -eq 'WinGet') { $schemaUri = 'https://raw.githubusercontent.com/PowerShell/DSC/main/schemas/2023/08/config/document.json' $resourceType = 'Microsoft.WinGet.DSC/WinGetPackage' $versionField = 'configurationVersion' $defaultVersion = '0.1.0' } else { $schemaUri = 'https://aka.ms/dsc/schemas/v3/bundled/config/document.json' $resourceType = 'Microsoft.WinGet/Package' $versionField = 'contentVersion' $defaultVersion = '0.1.0' } if (-not $PSBoundParameters.ContainsKey('ContentVersion')) { $ContentVersion = $defaultVersion } $inputFile = Get-Item -LiteralPath $InputPath if ($inputFile -isnot [System.IO.FileInfo]) { throw "InputPath must refer to a file: $InputPath" } if (-not $PSBoundParameters.ContainsKey('OutputPath')) { $OutputPath = [System.IO.Path]::ChangeExtension($inputFile.FullName, '.dsc.yaml') } $outputFilePath = $ExecutionContext.SessionState.Path.GetUnresolvedProviderPathFromPSPath($OutputPath) if ([string]::Equals($inputFile.FullName, $outputFilePath, [StringComparison]::OrdinalIgnoreCase)) { throw 'InputPath and OutputPath must refer to different files.' } if ((Test-Path -LiteralPath $outputFilePath) -and -not $Force) { throw "Output file already exists. Use -Force to replace it: $outputFilePath" } $export = Get-Content -LiteralPath $inputFile.FullName -Raw -Encoding UTF8 | ConvertFrom-Json if ($null -eq $export -or $export -isnot [pscustomobject] -or $export.Sources -isnot [array]) { throw 'Invalid WinGet export: expected an object containing a Sources array.' } $lines = [System.Collections.Generic.List[string]]::new() $lines.Add('$schema: ' + $schemaUri) $lines.Add('') $lines.Add('metadata:') $lines.Add(' author: ' + (ConvertTo-YamlString $Author)) $lines.Add(' description: ' + (ConvertTo-YamlString $Description)) $lines.Add('') $lines.Add($versionField + ': ' + (ConvertTo-YamlString $ContentVersion)) $lines.Add('') $lines.Add('resources:') $packageIds = [System.Collections.Generic.HashSet[string]]::new([StringComparer]::OrdinalIgnoreCase) foreach ($source in $export.Sources) { if ($null -eq $source -or $source -isnot [pscustomobject] -or $source.SourceDetails.Name -isnot [string] -or [string]::IsNullOrWhiteSpace($source.SourceDetails.Name) -or $source.Packages -isnot [array]) { throw 'Invalid WinGet export: every source must contain SourceDetails.Name and a Packages array.' } foreach ($package in $source.Packages) { if ($null -eq $package -or $package -isnot [pscustomobject] -or $package.PackageIdentifier -isnot [string] -or [string]::IsNullOrWhiteSpace($package.PackageIdentifier)) { throw 'Invalid WinGet export: every package must contain a nonempty string PackageIdentifier.' } $id = $package.PackageIdentifier if (-not $packageIds.Add($id)) { throw "Duplicate package ID '$id'. Each package must appear only once across all sources." } $lines.Add(' - name: ' + (ConvertTo-YamlString $id)) $lines.Add(' type: ' + $resourceType) $lines.Add(' properties:') $lines.Add(' id: ' + (ConvertTo-YamlString $id)) $lines.Add(' source: ' + (ConvertTo-YamlString $source.SourceDetails.Name)) if ($Schema -eq 'WinGet') { $lines.Add(' ensure: Present') } $version = $package.Version if ($null -ne $version -and $version -isnot [string]) { throw "Invalid version for '$id': Version must be a string when provided." } if ([string]::IsNullOrWhiteSpace($version) -or $version.Trim() -ieq 'Unknown') { Write-Warning "Package '$id' has no known version; omitting the version property." } else { $lines.Add(' version: ' + (ConvertTo-YamlString $version)) } $lines.Add('') } } if ($packageIds.Count -eq 0) { throw 'The WinGet export contains no packages. No configuration was written.' } $encoding = [System.Text.UTF8Encoding]::new($false) $fileMode = [System.IO.FileMode]::CreateNew if ($Force) { $fileMode = [System.IO.FileMode]::Create } $stream = [System.IO.File]::Open($outputFilePath, $fileMode, [System.IO.FileAccess]::Write, [System.IO.FileShare]::None) try { $content = $lines -join [Environment]::NewLine $bytes = $encoding.GetBytes($content) $stream.Write($bytes, 0, $bytes.Length) } finally { $stream.Dispose() } Get-Item -LiteralPath $outputFilePath } Export-ModuleMember -Function Convert-WinGetExportToDsc |