Public/Install-WindowsFromWim.ps1

function Install-WindowsFromWim {
    <#
    .SYNOPSIS
    Formats a disk and installs Windows from a WIM or ISO image.
 
    .DESCRIPTION
    This command formats the specified disk and installs Windows from a provided WIM or ISO image. You must supply the disk number and the path to a valid WIM or ISO file. You can specify the index number for the Windows edition to install, disk layout, partition sizes, features, drivers, packages, and more. The function supports overwriting existing data and skipping the recovery partition.
 
    .PARAMETER DiskNumber
    The disk number to format and install Windows to. Must exist and be visible to Get-Disk.
 
    .PARAMETER DiskLayout
    Specifies the disk layout: BIOS (MBR), UEFI (GPT), or WindowsToGo (MBR).
 
    .PARAMETER NoRecoveryTools
    If specified, skips creation of the recovery tools partition.
 
    .PARAMETER SystemSize
    Size of the system (boot loader) partition in MB. Default is 260MB.
 
    .PARAMETER ReservedSize
    Size of the MS Reserved partition in MB. Default is 128MB.
 
    .PARAMETER RecoverySize
    Size of the recovery tools partition in MB. Default is 905MB.
 
    .PARAMETER Force
    If specified, overwrites any existing partitions or data on the disk.
 
    .PARAMETER SourcePath
    The path to the WIM or ISO file used to install Windows.
 
    .PARAMETER Index
    The index of the image inside the WIM. Default is 1.
 
    .PARAMETER Unattend
    Path to an unattend.xml file to copy into the installed Windows image.
 
    .PARAMETER NativeBoot
    If specified, prepares the disk for native boot.
 
    .PARAMETER Feature
    Features to enable (in DISM format).
 
    .PARAMETER RemoveFeature
    Features to remove (in DISM format).
 
    .PARAMETER FeatureSource
    Path to feature source files or folders.
 
    .PARAMETER FeatureSourceIndex
    Index for feature source WIM. Default is 1.
 
    .PARAMETER Driver
    Paths to drivers to inject.
 
    .PARAMETER AddPayloadForRemovedFeature
    If specified, adds payload for all removed features.
 
    .PARAMETER Package
    Paths to packages to install via DISM.
 
    .PARAMETER filesToInject
    Files or folders to copy to the root of the Windows drive.
 
    .EXAMPLE
    Install-WindowsFromWim -DiskNumber 0 -SourcePath d:\Source\install.wim -NoRecoveryTools -DiskLayout UEFI
 
    Installs Windows to disk number 0 with no recovery partition from index 1.
 
    .EXAMPLE
    Install-WindowsFromWim -DiskNumber 0 -SourcePath d:\Source\install.wim -Index 3 -Force -DiskLayout UEFI
 
    Installs Windows to disk number 0 from index 3, overwriting any existing data.
 
    .NOTES
    Author: WindowsImageTools Team
    Requires: Administrator privileges
    #>

    [CmdletBinding(SupportsShouldProcess = $true,
        PositionalBinding = $false,
        ConfirmImpact = 'Medium')]
    Param
    (
        # Disk number, disk must exist
        [Parameter(Position = 0, Mandatory,
            HelpMessage = 'Disk Number based on Get-Disk')]
        [ValidateNotNullOrEmpty()]
        [ValidateScript( {
                if (Get-Disk -Number $_) {
                    $true
                } else {
                    Throw "Disk number $_ does not exist."
                }
            })]
        [string]$DiskNumber,

        # Specifies whether to build the image for BIOS (MBR), UEFI (GPT), or WindowsToGo (MBR).
        # Generation 1 VMs require BIOS (MBR) images. Generation 2 VMs require UEFI (GPT) images.
        # Windows To Go images will boot in UEFI or BIOS
        [Parameter(Mandatory = $true)]
        [Alias('Layout')]
        [string]
        [ValidateNotNullOrEmpty()]
        [ValidateSet('BIOS', 'UEFI', 'WindowsToGo')]
        $DiskLayout,

        # Skip creating the Recovery Environment Tools Partition.
        [switch]$NoRecoveryTools,

        # System (boot loader) Partition Size (Default : 260MB)
        [int]$SystemSize,

        # MS Reserved Partition Size (Default : 128MB)
        [int]$ReservedSize,

        # Recovery Tools Partition Size (Default : 905MB)
        [int]$RecoverySize,

        # Force the overwrite of existing files
        [switch]$force,

        # Path to WIM or ISO used to populate VHDX
        [parameter(Position = 1, Mandatory = $true,
            HelpMessage = 'Enter the path to the WIM/ISO file')]
        [ValidateScript( {
                Test-Path -Path (Get-FullFilePath -Path $_ )
            })]
        [string]$SourcePath,

        # Index of image inside of WIM (Default 1)
        [int]$Index = 1,

        # Path to file to copy inside of VHD(X) as C:\unattend.xml
        [ValidateScript( {
                if ($_) {
                    Test-Path -Path $_
                } else {
                    $true
                }
            })]
        [string]$Unattend,

        # Native Boot does not have the boot code inside the VHD(x) it must exist on the physical disk.
        [switch]$NativeBoot,

        # Features to turn on (in DISM format)
        [ValidateNotNullOrEmpty()]
        [string[]]$Feature,

        # Feature to remove (in DISM format)
        [ValidateNotNullOrEmpty()]
        [string[]]$RemoveFeature,

        # Feature Source path. If not provided, all ISO and WIM images in $sourcePath searched
        [ValidateNotNullOrEmpty()]
        [ValidateScript( {
                (Test-Path -Path $(Resolve-Path $_) -or ($_ -eq 'NONE') )
            })]
        [string]$FeatureSource,

        # Feature Source index. If the source is a .wim provide an index Default =1
        [int]$FeatureSourceIndex = 1,

        # Path to drivers to inject
        [ValidateNotNullOrEmpty()]
        [ValidateScript( {
                foreach ($Path in $_) {
                    Test-Path -Path $(Resolve-Path $Path)
                }
            })]
        [string[]]$Driver,

        # Add payload for all removed features
        [switch]$AddPayloadForRemovedFeature,

        # Path of packages to install via DISM
        [ValidateNotNullOrEmpty()]
        [ValidateScript( {
                foreach ($Path in $_) {
                    Test-Path -Path $(Resolve-Path $Path)
                }
            })]
        [string[]]$Package,
        # Files/Folders to copy to root of Windows Drive (to place files in directories mimic the directory structure off of C:\)
        [ValidateNotNullOrEmpty()]
        [ValidateScript( {
                foreach ($Path in $_) {
                    Test-Path -Path $(Resolve-Path $Path)
                }
            })]
        [string[]]$filesToInject,

        # Use DISM for expansion instead of native PowerShell
        [Parameter(HelpMessage = 'Use DISM for expansion instead of native PowerShell')]
        [switch]$UseDismExpansion

    )
    $SourcePath = $SourcePath | Get-FullFilePath

    if ($psCmdlet.ShouldProcess("[$($MyInvocation.MyCommand)] : Overwrite partitions on disk [$DiskNumber] with content of [$SourcePath]",
            "Overwrite partitions on disk [$DiskNumber] with content of [$SourcePath]? ",
            'Overwrite WARNING!')) {
        if ((Get-Disk -Number $DiskNumber | Get-Partition -ErrorAction SilentlyContinue) -Or $force -Or $psCmdlet.ShouldContinue('Are you sure? Any existing data will be lost!', 'Warning')) {
            $ParametersToPass = @{ }
            foreach ($key in ('WhatIf', 'Verbose', 'Debug')) {
                if ($PSBoundParameters.ContainsKey($key)) {
                    $ParametersToPass[$key] = $PSBoundParameters[$key]
                }
            }

            $InitializeDiskPartitionParam = @{
                'DiskNumber' = $DiskNumber
                'force'      = $true
                'DiskLayout' = $DiskLayout
            }
            if ($NoRecoveryTools) {
                $InitializeDiskPartitionParam.add('NoRecoveryTools', $true)
            }
            if ($Dynamic) {
                $InitializeDiskPartitionParam.add('Dynamic', $true)
            }
            if ($SystemSize) { $InitializeDiskPartitionParam.add('SystemSize', $SystemSize) }
            if ($ReservedSize) { $InitializeDiskPartitionParam.add('ReservedSize', $ReservedSize) }
            if ($RecoverySize) { $InitializeDiskPartitionParam.add('RecoverySize', $RecoverySize) }

            $SetDiskPartitionParam = @{
                'SourcePath' = $SourcePath
                'DiskNumber' = $DiskNumber
                'Index'      = $Index
                'force'      = $true
                'Confirm'    = $false
            }
            if ($Unattend) {
                $SetDiskPartitionParam.add('Unattend', $Unattend)
            }
            if ($NativeBoot) {
                $SetDiskPartitionParam.add('NativeBoot', $NativeBoot)
            }
            if ($Feature) {
                $SetDiskPartitionParam.add('Feature', $Feature)
            }
            if ($RemoveFeature) {
                $SetDiskPartitionParam.add('RemoveFeature', $RemoveFeature)
            }
            if ($FeatureSource) {
                $SetDiskPartitionParam.add('FeatureSource', $FeatureSource)
            }
            if ($FeatureSourceIndex) {
                $SetDiskPartitionParam.add('FeatureSourceIndex', $FeatureSourceIndex)
            }
            if ($AddPayloadForRemovedFeature) {
                $SetDiskPartitionParam.add('AddPayloadForRemovedFeature', $AddPayloadForRemovedFeature)
            }
            if ($Driver) {
                $SetDiskPartitionParam.add('Driver', $Driver)
            }
            if ($Package) {
                $SetDiskPartitionParam.add('Package', $Package)
            }
            if ($filesToInject) {
                $SetDiskPartitionParam.add('filesToInject', $filesToInject)
            }
            if ($PSBoundParameters.ContainsKey('UseDismExpansion')) {
                $SetDiskPartitionParam.add('UseDismExpansion', $UseDismExpansion.IsPresent)
            }
            Write-Verbose -Message "[$($MyInvocation.MyCommand)] : InitializeDiskPartitionParam"
            Write-Verbose -Message ($InitializeVHDPartitionParam | Out-String)
            Write-Verbose -Message "[$($MyInvocation.MyCommand)] : SetDiskPartitionParam"
            Write-Verbose -Message ($SetDiskPartitionParam | Out-String)
            Write-Verbose -Message "[$($MyInvocation.MyCommand)] : ParametersToPass"
            Write-Verbose -Message ($ParametersToPass | Out-String)

            Try {
                Initialize-DiskPartition @InitializeDiskPartitionParam @ParametersToPass
                Set-DiskPartition @SetDiskPartitionParam @ParametersToPass
            } Catch {
                throw "$($_.Exception.Message) at $($_.Exception.InvocationInfo.ScriptLineNumber)"
            }
        }
    }
}