Wsl-Image/Wsl-Image.Cmdlets.ps1
|
function New-WslImage { <# .SYNOPSIS Creates a WslImage object. .DESCRIPTION WslImage object retrieve and provide information about available root filesystems. .PARAMETER Source A WslImageSource object representing the image source to create a local image from. .PARAMETER Name The identifier of the image. It can be an already known name: - Arch - Alpine - Ubuntu - Debian It also can be the URL (https://...) of an existing filesystem or a image name saved through Export-WslInstance. It can also be a URL in the form: incus://<os>#<release> (ex: incus://rockylinux#9) In this case, it will fetch the last version the specified image in https://images.linuxcontainers.org/images. .PARAMETER Uri A URI object representing the location of the root filesystem. .PARAMETER File A FileInfo object of the compressed root filesystem. .INPUTS WslImageSource[] You can pipe WslImageSource objects to this cmdlet. .OUTPUTS WslImage The cmdlet returns WslImage objects that represent the WSL root filesystems. .EXAMPLE New-WslImage -Name "incus://alpine#3.19" Type Os Release State Name ---- -- ------- ----- ---- Incus alpine 3.19 Synced incus.alpine_3.19.rootfs.tar.gz Creates a WSL root filesystem from the incus alpine 3.19 image. .EXAMPLE New-WslImage -Name "alpine" Type Os Release State Name ---- -- ------- ----- ---- Builtin Alpine 3.19 Synced alpine.rootfs.tar.gz Creates a WSL root filesystem from the builtin Alpine image. .EXAMPLE New-WslImage -File (Get-Item "C:\temp\test.rootfs.tar.gz") Type Os Release State Name ---- -- ------- ----- ---- Local Alpine 3.21.3 Synced test.rootfs.tar.gz Creates a WSL root filesystem from a local file. .EXAMPLE New-WslImage -Name "C:\temp\test.rootfs.tar.gz" Type Os Release State Name ---- -- ------- ----- ---- Local Alpine 3.21.3 Synced test.rootfs.tar.gz Creates a WSL root filesystem from a local file without requiring a FileInfo object. .EXAMPLE Get-WslImageSource | New-WslImage Creates WslImage objects from all available image sources. .LINK Get-WslImage Get-WslImageSource #> [Diagnostics.CodeAnalysis.SuppressMessageAttribute('PSUseShouldProcessForStateChangingFunctions', '')] [CmdletBinding()] [OutputType([WslImage])] param ( [Parameter(Position=0, Mandatory = $true, ParameterSetName = 'Source', ValueFromPipeline = $true)] [WslImageSource[]]$Source, [Parameter(ParameterSetName = 'Name', ValueFromPipeline = $true, Mandatory = $true)] [string]$Name, [Parameter(ParameterSetName = 'Uri', ValueFromPipeline = $true, Mandatory = $true)] [Uri]$Uri, [Parameter(ParameterSetName = 'File', ValueFromPipeline = $true, Mandatory = $true)] [FileInfo]$File ) process { if ($PSCmdlet.ParameterSetName -eq "Name") { $Source = New-WslImageSource -Name $Name } elseif ($PSCmdlet.ParameterSetName -eq "File") { $Source = New-WslImageSource -File $File } elseif ($PSCmdlet.ParameterSetName -eq "Uri") { $Source = New-WslImageSource -Uri $Uri } [WslImageDatabase] $imageDb = Get-WslImageDatabase $Source | ForEach-Object { $imageSource = $_ if (-not $imageSource.IsCached) { $imageSource.Id = [Guid]::NewGuid() $imageDb.SaveImageSource($imageSource.ToObject()) } Write-Verbose "Creating local image from source Id $($imageSource.Id)..." $imageDb.CreateLocalImageFromImageSource($imageSource.Id) | ForEach-Object { $result = [WslImage]::new($_, $imageSource) if ($result.RefreshState()) { $imageDb.SaveLocalImage($result.ToObject()) } $result } } } } function Sync-WslImage { <# .SYNOPSIS Synchronize locally the specified WSL root filesystem. .DESCRIPTION If the root filesystem is not already present locally, downloads it from its original URL. .PARAMETER Name The identifier of the image. It can be an already known name: - Arch - Alpine - Ubuntu - Debian It also can be the URL (https://...) of an existing filesystem or a image name saved through Export-WslInstance. It can also be a name in the form: incus://<os>#<release> (ex: incus://rockylinux#9) In this case, it will fetch the last version the specified image in https://images.linuxcontainers.org/images. It can also designate a docker image in the form: docker://<registry>/<image>#<tag> (ex: docker://ghcr.io/antoinemartin/yawsldocker/yawsldocker-alpine:latest) NOTE: Currently, only images with a single layer are supported. .PARAMETER Image The WslImage object to process. .PARAMETER Force Force the synchronization even if the root filesystem is already present locally. .INPUTS WslImage[] The WslImage Objects to process. .OUTPUTS WslImage[] The WslImage objects. .EXAMPLE Sync-WslImage -Name "Alpine" Syncs the builtin Alpine root filesystem. .EXAMPLE Sync-WslImage -Name "Alpine" -Force Re-download the Alpine builtin root filesystem. .EXAMPLE Get-WslImage -State NotDownloaded -Distribution Alpine | Sync-WslImage Synchronize the Alpine root filesystems not already synced .EXAMPLE New-WslImage -Name "alpine" | Sync-WslImage | ForEach-Object { &wsl --import test $env:LOCALAPPDATA\Wsl\test $_.File.FullName } Create a WSL distro from a synchronized root filesystem. .LINK New-WslImage Get-WslImage #> [CmdletBinding(SupportsShouldProcess = $true)] [OutputType([WslImage])] param ( [Parameter(Position = 0, ParameterSetName = 'Name', ValueFromPipeline = $true, Mandatory = $true)] [ValidateNotNullOrEmpty()] [string[]]$Name, [Parameter(Mandatory = $true, ValueFromPipeline = $true, ParameterSetName = "Image")] [WslImage[]]$Image, [Parameter(Mandatory = $false)] [switch]$Force ) process { if ($PSCmdlet.ParameterSetName -eq "Name") { $Image = $Name | ForEach-Object { $existing = Get-WslImage -Name $_ if ($existing.Count -eq 0) { Write-Verbose "Image '$_' not found locally. Creating new image." $existing = New-WslImage -Name $_ } $existing } } if ($null -ne $Image) { If (!([WslImage]::BasePath.Exists)) { # nocov if ($PSCmdlet.ShouldProcess([WslImage]::BasePath.Create(), "Create base path")) { [WslImage]::BasePath.Create() } } [WslImageDatabase] $imageDb = Get-WslImageDatabase $Image | ForEach-Object { $fs = $_ if ($true -eq $Force -and $null -ne $fs.Source) { $null = Update-WslImageSource -ImageSource $fs.Source | Save-WslImageSource $fs = Get-WslImage -Id $fs.Id } # Check if we need to download something $oldFile = $null $oldFileName = $null if ($fs.State -eq [WslImageState]::Outdated) { $oldFileName = $fs.LocalFilename $oldFile = $fs.File Write-Verbose "Image [$($fs.DistributionName)] is outdated. Old file: [$($oldFileName)]. New file: [$($fs.Source.LocalFilename)]." Write-Verbose "Update metadata from source." $fs.UpdateFromSource() } [FileInfo] $dest = $fs.File if (!$dest.Exists -or $true -eq $Force -or $null -ne $oldFile) { if ($PSCmdlet.ShouldProcess($fs.Url, "Sync locally")) { try { $fs.DownloadAndCheckFile() $fs.State = [WslImageState]::Synced $imageDb.SaveLocalImage($fs.ToObject()) # Remove old file if needed if ($null -ne $oldFile -and $oldFileName -ne $fs.LocalFilename) { $existing = $imageDb.GetLocalImages("LocalFilename = @LocalFilename", @{ LocalFilename = $oldFileName }) if ($existing.Count -eq 0) { Write-Verbose "Removing old file [$($oldFile.FullName)]." try { $oldFile.Delete() } catch { # nocov Warning "Unable to delete old file [$($oldFile.FullName)]: $($_.Exception.Message)" } } } Success "[$($fs.DistributionName)] Synced at [$($dest.FullName)]." } catch [Exception] { throw [WslManagerException]::new("Error while loading distro [$($fs.DistributionName)] on $($fs.Url): $($_.Exception.Message)", $_.Exception) } } } else { Information "[$($fs.DistributionName)] Root FS already at [$($dest.FullName)]." } return $fs } } } } function Get-WslImage { <# .SYNOPSIS Gets the WSL root filesystems installed on the computer and the ones available. .DESCRIPTION The Get-WslImage cmdlet gets objects that represent the WSL root filesystems available on the computer. This can be the ones already synchronized as well as the Builtin filesystems available. .PARAMETER Name Specifies the name of the filesystem. Supports wildcards. .PARAMETER Distribution Specifies the linux distribution of the image. .PARAMETER Type Specifies the type of the filesystem source (All, Builtin, Local, Incus, Docker). .PARAMETER State Specifies the state of the image (NotDownloaded, Synced, Outdated). .PARAMETER Configured Return only configured builtin images when present, or unconfigured when not present. .PARAMETER Outdated Return the list of outdated images. Works mainly on Builtin images. .PARAMETER Source Filters by a specific WslImageSource object. .PARAMETER Id Specifies one or more image IDs (GUIDs) to retrieve. This parameter is used in a separate parameter set to get images by their unique identifiers. .INPUTS System.String You can pipe image names to this cmdlet. .OUTPUTS WslImage The cmdlet returns objects that represent the WSL root filesystems on the computer. .EXAMPLE Get-WslImage Name Type Os Release Configured State Length ---- ---- -- ------- ---------- ----- ------ opensuse Docker Opensuse-... 20250813 True Synced 107,3 MB docker Local arch 3.22.1 True Synced 511,9 MB iknite Local Alpine 3.21.3 False Synced 802,2 MB kaweezle Local Alpine 3.21.3 False Synced 802,2 MB python Local debian 13 True Synced 113,7 MB alpine Builtin Alpine 3.23.2 True Synced 36,1 MB opensuse-tumb... Builtin Opensuse-... 20251217 False Synced 72,3 MB yawsldocker-a... Docker Alpine 3.22.1 True Synced 148,5 MB archlinux Uri Archlinux latest False Synced 131,1 MB alpine Docker alpine edge False Synced 3,5 MB debian-base Builtin Debian 13 False Synced 48,1 MB arch Builtin Arch 2025.12.01 True Synced 379,5 MB jekyll Local Alpine 3.22.1 True Synced 159,0 MB opensuse Uri Opensuse tumbleweed False Synced 46,4 MB Get all WSL root filesystem. .EXAMPLE Get-WslImage -Distribution alpine Name Type Os Release Configured State Length ---- ---- -- ------- ---------- ----- ------ iknite Local Alpine 3.21.3 False Synced 802,2 MB kaweezle Local Alpine 3.21.3 False Synced 802,2 MB alpine Builtin Alpine 3.23.2 True Synced 36,1 MB yawsldocker-a... Docker Alpine 3.22.1 True Synced 148,5 MB jekyll Local Alpine 3.22.1 True Synced 159,0 MB Get All Alpine root filesystems. .EXAMPLE Get-WslImage -Type Incus Name Type Os Release Configured State Length ---- ---- -- ------- ---------- ----- ------ almalinux Incus Almalinux 8 False Synced 110,0 MB almalinux Incus Almalinux 9 False Synced 102,0 MB alpine Incus Alpine 3.19 False Synced 2,9 MB alpine Incus Alpine 3.20 False Synced 3,0 MB alpine Incus Alpine 3.20 False Synced 3,0 MB Get All downloaded Incus root filesystems. .EXAMPLE Get-WslImage -State NotDownloaded Get all images that are not yet downloaded. .EXAMPLE Get-WslImage -Configured Get all configured builtin images. .EXAMPLE Get-WslImage -Outdated Get all outdated images that need updating. #> [CmdletBinding()] [OutputType([WslImage])] param( [Parameter(Mandatory = $false, Position = 0, ValueFromPipeline = $true, ParameterSetName = 'Name')] [ValidateNotNullOrEmpty()] [SupportsWildcards()] [string[]]$Name, [Parameter(Mandatory = $false, ParameterSetName = 'Name')] [Alias("Os")] [string]$Distribution, [Parameter(Mandatory = $false, ParameterSetName = 'Name')] [WslImageSourceType]$Type = [WslImageSourceType]::All, [Parameter(Mandatory = $false, ParameterSetName = 'Name')] [WslImageState]$State, [Parameter(Mandatory = $false, ParameterSetName = 'Name')] [switch]$Configured, [Parameter(Mandatory = $false, ParameterSetName = 'Name')] [switch]$Outdated, [Parameter(Mandatory = $false, ParameterSetName = 'Name')] [WslImageSource]$Source, [Parameter(Mandatory = $true, ParameterSetName = 'Id')] [Guid[]]$Id ) process { $operators = @() $parameters = @{} $typesInUse = @() if ($PSCmdlet.ParameterSetName -eq 'Id') { $operators += "Id IN (@Ids)" $parameters["Ids"] = $Id | ForEach-Object { $_.ToString() } } else { if ($Type -ne [WslImageSourceType]::All) { foreach ($sourceType in [WslImageSourceType].GetEnumNames()) { if ('All' -eq $sourceType) { continue } if ($Type -band [WslImageSourceType]::$sourceType) { $typesInUse += $sourceType } } } if ($typesInUse.Count -gt 0) { $operators += "Type IN (" + (($typesInUse | ForEach-Object { "'$_'" }) -join ", ") + ")" } if ($PSBoundParameters.ContainsKey("Distribution")) { $operators += "Distribution = @Distribution" $parameters["Distribution"] = $Distribution } if ($PSBoundParameters.ContainsKey("State") -or $PSBoundParameters.ContainsKey("Outdated")) { $operators += "State = @State" if ($PSBoundParameters.ContainsKey("State")) { $parameters["State"] = $State.ToString() } else { $parameters["State"] = [WslImageState]::Outdated.ToString() } } if ($PSBoundParameters.ContainsKey("Configured")) { $operators += "Configured = @Configured" $parameters["Configured"] = if ($Configured.IsPresent) { 'TRUE' } else { 'FALSE' } } if ($PSBoundParameters.ContainsKey("Source")) { $operators += "ImageSourceId = @ImageSourceId" $parameters["ImageSourceId"] = $Source.Id.ToString() } if ($Name.Length -gt 0) { $operators += ($Name | ForEach-Object { "(Name GLOB '$($_)')" }) -join " OR " } } $whereClause = $operators -join " AND " Write-Verbose "Get-WslImage: WHERE $whereClause with parameters $($parameters | ConvertTo-Json -Compress)" [WslImageDatabase] $imageDb = Get-WslImageDatabase $fileSystems = $imageDb.GetLocalImages($whereClause, $parameters) # Retrieve related image sources $sourceIds = $fileSystems | Where-Object { $null -ne $_.ImageSourceId } | Select-Object -ExpandProperty ImageSourceId -Unique | ForEach-Object { "'$_'" } $query = "Id IN ($($sourceIds -join ','))" $sources = $imageDb.GetImageSources($query, @{}) | ForEach-Object { [WslImageSource]::new($_) } | Group-Object -Property Id -AsHashTable -AsString if ($null -eq $sources) { Write-Verbose "No image sources found." $sources = @{} } # else { # Write-Verbose "Found $($sources.Count) image sources.$($sources.Keys | ForEach-Object { "`n - $_" })" #} $result = $fileSystems | ForEach-Object { if ($null -eq $_.ImageSourceId -or -not $sources.ContainsKey($_.ImageSourceId)) { # nocov # Write-Verbose "No image source found for image [$($_.Id)] ($($_.ImageSourceId)). Creating without source." [WslImage]::new($_) } else { $imageSources = $sources[$_.ImageSourceId] # Write-Verbose "Linking image source [$($_.ImageSourceId)] to image [$($_.Id)]" [WslImage]::new($_, $imageSources[0]) } } return $result } } <# .SYNOPSIS Remove a WSL root filesystem from the local disk. .DESCRIPTION If the WSL root filesystem is synced, it will remove the tar file and its meta data from the disk. Builtin root filesystems will still appear as output of `Get-WslImage`, but their state will be `NotDownloaded`. .PARAMETER Name The identifier of the image. It can be an already known name: - Arch - Alpine - Ubuntu - Debian It also can be the URL (https://...) of an existing filesystem or a image name saved through Export-WslInstance. It can also be a name in the form: incus://<os>#<release> (ex: incus://rockylinux#9) In this case, it will refer to the specified image from https://images.linuxcontainers.org/images. Supports wildcards. .PARAMETER Image The WslImage object representing the WSL root filesystem to delete. .PARAMETER Force Force removal of the image even if it is the source file. By default, images that serve as source files cannot be removed without this flag. .INPUTS WslImage[] One or more WslImage objects representing the WSL root filesystem to delete. .OUTPUTS WslImage[] The WslImage objects updated. .EXAMPLE Remove-WslImage -Name "alpine" Removes the alpine root filesystem. .EXAMPLE New-WslImage -Name "incus://alpine#3.19" | Remove-WslImage Removes the Incus alpine 3.19 root filesystem. .EXAMPLE Get-WslImage -Type Incus | Remove-WslImage Removes all the Incus root filesystems present locally. .EXAMPLE Remove-WslImage -Name "*alpine*" Removes all root filesystems with 'alpine' in their name. .Link Get-WslImage New-WslImage #> Function Remove-WslImage { [CmdletBinding(SupportsShouldProcess = $true)] [OutputType([WslImage])] param ( [Parameter(Position=0, ParameterSetName = 'Name', Mandatory = $true, ValueFromPipeline = $false)] [ValidateNotNullOrEmpty()] [SupportsWildcards()] [string[]]$Name, [Parameter(Position=0, Mandatory = $true, ValueFromPipeline = $true, ParameterSetName = "Image")] [WslImage[]]$Image, [switch]$Force ) process { if ($PSCmdlet.ParameterSetName -eq "Name") { $Image = Get-WslImage -Name $Name } if ($null -ne $Image) { $db = Get-WslImageDatabase $Image | ForEach-Object { if ($PSCmdlet.ShouldProcess($_.Name, "Remove WSL image")) { $ImageIsSource = ($_.Type -eq [WslImageType]::Local) -and ($_.SourceId -ne [Guid]::Empty) -and ($_.Url -eq $_.Source.Url) Write-Verbose "Removing image [$($_.Name)] (id=$($_.Id), sourceId=$($_.SourceId), url=$($_.Url.AbsoluteUri), type=$($_.Type), isSource=$ImageIsSource)..." if (-not $Force -and $ImageIsSource) { throw [WslImageException]::new("$($_.Name) file is the source file. Use -Force to remove both.") } Write-Verbose "Removing image file [$($_.File.FullName)]..." $_.Delete() | Out-Null $db.RemoveLocalImage($_.Id) if ($ImageIsSource) { Write-Verbose "Removing image source [$($_.SourceId)]..." $db.RemoveImageSource($_.SourceId) $_.SourceId = [Guid]::Empty } $_.Id = [Guid]::Empty $_ } } } } } #region Set-WslImageProperty <# .SYNOPSIS Sets a property of a WSL image. .DESCRIPTION The Set-WslImageProperty cmdlet changes the value of a specified property on a WSL image. The image is identified either by its name or by passing a WslImage object. Standard properties that can be changed without -Force: - Name - Distribution - Release - Username - Uid - Configured Advanced properties requiring -Force: - Type - SourceId - Url - LocalFilename - DigestUrl - DigestAlgorithm - DigestType - FileHash - State .PARAMETER ImageName The name of the image to modify. Use this parameter when specifying the image by name. .PARAMETER Image The WslImage object to modify. Can be piped to this cmdlet. .PARAMETER PropertyName The name of the property to change. .PARAMETER Value The new value for the property. .PARAMETER Source A WslImageSource object. When specified with PropertyName 'SourceId', the SourceId of the image will be set to the Id of this source. .PARAMETER Force Required when changing advanced properties (Type, SourceId, Url, etc.). .INPUTS WslImage You can pipe WslImage objects to this cmdlet. .OUTPUTS WslImage The cmdlet returns the modified WslImage object. .EXAMPLE Set-WslImageProperty -ImageName "MyImage" -PropertyName "Name" -Value "NewName" Changes the name of the image "MyImage" to "NewName". .EXAMPLE Set-WslImageProperty -ImageName "MyImage" -PropertyName "Distribution" -Value "Ubuntu" Changes the distribution of "MyImage" to "Ubuntu". .EXAMPLE $image = Get-WslImage -Name "MyImage" $source = Get-WslImageSource -Name "alpine" Set-WslImageProperty -Image $image -PropertyName "SourceId" -Source $source -Force Changes the source of the image to the alpine image source. .EXAMPLE Get-WslImage -Name "MyImage" | Set-WslImageProperty -PropertyName "State" -Value "NotDownloaded" -Force Changes the state of "MyImage" to NotDownloaded using pipeline input. .LINK Get-WslImage New-WslImage #> function Set-WslImageProperty { [CmdletBinding(SupportsShouldProcess = $true, DefaultParameterSetName = 'ByName')] [OutputType([WslImage])] param ( [Parameter(Mandatory = $true, ParameterSetName = 'ByName', Position = 0)] [ValidateNotNullOrEmpty()] [string]$ImageName, [Parameter(Mandatory = $true, ParameterSetName = 'ByImage', ValueFromPipeline = $true)] [ValidateNotNull()] [WslImage]$Image, [Parameter(Mandatory = $true)] [Alias("Name")] [ValidateSet( 'Name', 'Distribution', 'Release', 'Username', 'Uid', 'Configured', 'Type', 'SourceId', 'Url', 'LocalFilename', 'DigestUrl', 'DigestAlgorithm', 'DigestType', 'FileHash', 'State' )] [string]$PropertyName, [Parameter(Mandatory = $false)] [object]$Value, [Parameter(Mandatory = $false)] [WslImageSource]$Source, [Parameter(Mandatory = $false)] [switch]$Force ) process { # Advanced properties that require -Force $advancedProperties = @('Type', 'SourceId', 'Url', 'LocalFilename', 'DigestUrl', 'DigestAlgorithm', 'DigestType', 'FileHash', 'State') # Validate that advanced properties require -Force if ($PropertyName -in $advancedProperties -and -not $Force) { throw [WslImageException]::new("Property '$PropertyName' requires the -Force switch to modify.") } # Retrieve the image if specified by name if ($PSCmdlet.ParameterSetName -eq 'ByName') { $foundImages = @(Get-WslImage -Name $ImageName) if ($null -eq $foundImages -or $foundImages.Count -eq 0) { throw [WslImageException]::new("Image '$ImageName' not found.") } if ($foundImages.Count -gt 1) { throw [WslImageException]::new("Multiple images found with name '$ImageName'. Please specify a unique name.") } $Image = $foundImages[0] } # Handle SourceId property specially when -Source is provided if ($PropertyName -eq 'SourceId' -and $null -ne $Source) { if ($Source.Id -eq [Guid]::Empty) { throw [WslImageException]::new("The provided Source has an empty or null Id.") } $Value = $Source.Id } elseif ($PropertyName -eq 'SourceId' -and $null -eq $Source -and $null -ne $Value) { # Validate that the SourceId exists $db = Get-WslImageDatabase $existingSource = $db.GetImageSources("Id = @Id", @{ Id = $Value.ToString() }) if ($null -eq $existingSource -or $existingSource.Count -eq 0) { throw [WslImageException]::new("Image source with Id '$Value' not found.") } } # Convert value to appropriate type based on property $convertedValue = switch ($PropertyName) { 'Configured' { [bool]$Value } 'Uid' { [int]$Value } 'Type' { [WslImageType]$Value } 'State' { [WslImageState]$Value } 'Url' { [System.Uri]$Value } 'DigestUrl' { if ($null -ne $Value) { [System.Uri]$Value } else { $null } } 'SourceId' { [Guid]$Value } default { $Value } } $oldValue = $Image.$PropertyName $action = "Set $PropertyName from '$oldValue' to '$convertedValue'" if ($PSCmdlet.ShouldProcess($Image.Name, $action)) { Write-Verbose "Setting property '$PropertyName' on image '$($Image.Name)' from '$oldValue' to '$convertedValue'..." # Set the property $Image.$PropertyName = $convertedValue # Save to database $db = Get-WslImageDatabase $db.SaveLocalImage($Image.ToObject()) Write-Verbose "Property '$PropertyName' updated successfully." return $Image } } } #endregion Set-WslImageProperty |