Private/Set-DiskPartition.ps1

function Set-DiskPartition {
    <#
    .SYNOPSIS
    Sets the content of a disk using a source WIM or ISO file.
 
    .DESCRIPTION
    Copies the content from a specified WIM or ISO file to the partitions on the target disk. You must provide a valid WIM/ISO path and the index number for the Windows edition to install. If a recovery partition is present, the source WIM will be copied to it. Optionally, you can specify an unattend XML file, drivers, Windows Update (MSU) packages, optional features, and additional files to inject. CAUTION: This command will replace the content of partitions.
 
    .EXAMPLE
    Set-DiskPartition -DiskNumber 0 -SourcePath D:\wim\Win2012R2-Install.wim -Index 1
 
    Copies the specified WIM image to disk 0, applying index 1.
 
    .EXAMPLE
    Set-DiskPartition -DiskNumber 0 -SourcePath D:\wim\Win2012R2-Install.wim -Index 1 -Confirm:$false -Force -Verbose
 
    Copies the specified WIM image to disk 0, applying index 1, without confirmation and with verbose output.
 
    .NOTES
    Author: BladeFireLight
    #>

    [CmdletBinding(SupportsShouldProcess = $true,
        PositionalBinding = $true,
        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."
                }
            })]
        [int]$DiskNumber,

        # 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 on the disk. Only useful for VHD(X).
        [switch]$NativeBoot,

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

        # Feature 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,

        # 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,

        # Bypass the warning and about lost data
        [switch]$Force,

        # Use DISM for image expansion instead of Expand-WindowsImage
        [Parameter(HelpMessage = 'If true, use Expand-DpWindowsImage for image expansion. If false, use Expand-WindowsImage.')]
        [switch]$UseDismExpansion = $false
    )


    Process {
        $SourcePath = $SourcePath | Get-FullFilePath

        if ($psCmdlet.ShouldProcess("[$($MyInvocation.MyCommand)] : Overwrite partitions inside [$Path] with content of [$SourcePath]",
                "Overwrite partitions inside [$Path] with content of [$SourcePath]? ",
                'Overwrite WARNING!')) {
            if ($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]
                    }
                }
                #region ISO detection
                # If we're using an ISO, mount it and get the path to the WIM file.
                if (([IO.FileInfo]$SourcePath).Extension -ilike '.ISO') {

                    $isoPath = (Resolve-Path $SourcePath).Path

                    Write-Verbose -Message "[$($MyInvocation.MyCommand)] : Opening ISO [$(Split-Path -Path $isoPath -Leaf)]"
                    $openIso = Mount-DiskImage -ImagePath $isoPath -StorageType ISO -PassThru
                    # Workaround for new drive letters in script modules
                    $null = Get-PSDrive
                    # Refresh the DiskImage object so we can get the real information about it. I assume this is a bug.
                    $openIso = Get-DiskImage -ImagePath $isoPath
                    $driveLetter = ($openIso | Get-Volume).DriveLetter

                    $SourcePath = "$($driveLetter):\sources\install.wim"

                    # Check to see if there's a WIM file.
                    Write-Verbose -Message "[$($MyInvocation.MyCommand)] : Looking for $($SourcePath)"
                    if (!(Test-Path $SourcePath)) {
                        throw 'The specified ISO does not appear to be valid Windows installation media.'
                    }
                }
                #endregion ISO detection

                try {
                    #! Workaround for new drive letters in script modules
                    $null = Get-PSDrive

                    #region Assign Drive Letters (disable explorer popup and reset afterwords)
                    $DisableAutoPlayOldValue = (Get-ItemProperty -path hkcu:\Software\Microsoft\Windows\CurrentVersion\Explorer\AutoplayHandlers -name DisableAutoplay).DisableAutoplay
                    Set-ItemProperty -Path hkcu:\Software\Microsoft\Windows\CurrentVersion\Explorer\AutoplayHandlers -Name DisableAutoplay -Value 1
                    foreach ($partition in (Get-Partition -DiskNumber $DiskNumber |
                            Where-Object -Property DriveLetter -EQ 0x00 |
                            Where-Object -Property Type -NE -Value Reserved)) {
                        $partition | Add-PartitionAccessPath -AssignDriveLetter -ErrorAction Stop
                    }
                    #! Workaround for new drive letters in script modules
                    $null = Get-PSDrive
                    Set-ItemProperty -Path hkcu:\Software\Microsoft\Windows\CurrentVersion\Explorer\AutoplayHandlers -Name DisableAutoplay -Value $DisableAutoPlayOldValue

                    Write-Verbose -Message "[$($MyInvocation.MyCommand)] [$DiskNumber] : Partition Table"
                    Write-Verbose -Message (Get-Partition -DiskNumber $DiskNumber |
                        Select-Object -Property PartitionNumber, DriveLetter, Size, Type |
                        Out-String)
                    #endregion

                    #region get partitions
                    $RecoveryToolsPartition = Get-Partition -DiskNumber $DiskNumber |
                    Where-Object -Property Type -EQ -Value Recovery |
                    Select-Object -First 1
                    $WindowsPartition = Get-Partition -DiskNumber $DiskNumber |
                    Where-Object -Property Type -EQ -Value Basic |
                    Select-Object -First 1
                    $SystemPartition = Get-Partition -DiskNumber $DiskNumber |
                    Where-Object -Property Type -EQ -Value System |
                    Select-Object -First 1

                    $DiskLayout = 'UEFI'
                    if (-not ($WindowsPartition -and $SystemPartition)) {
                        $WindowsPartition = Get-Partition -DiskNumber $DiskNumber |
                        Where-Object -Property Type -EQ -Value IFS |
                        Sort-Object -Descending -Property Size |
                        Select-Object -First 1
                        $SystemPartition = Get-Partition -DiskNumber $DiskNumber |
                        Where-Object -Property Type -EQ -Value IFS |
                        Sort-Object -Property Size |
                        Select-Object -First 1
                        $DiskLayout = 'BIOS'
                    }

                    if (Get-Partition -DiskNumber $DiskNumber |
                        Where-Object -Property Type -Like -Value 'FAT32*' ) {
                        $WindowsPartition = Get-Partition -DiskNumber $DiskNumber |
                        Where-Object -Property Type -EQ -Value IFS |
                        Sort-Object -Descending -Property Size |
                        Select-Object -First 1
                        $SystemPartition = Get-Partition -DiskNumber $DiskNumber |
                        Where-Object -Property Type -Like -Value 'FAT32*' |
                        Sort-Object -Property Size |
                        Select-Object -First 1
                        $DiskLayout = 'WindowsToGo'
                    }
                    #endregion get partitions

                    #region Windows partition
                    if ($WindowsPartition) {
                        $WinDrive = Join-Path -Path "$($WindowsPartition.DriveLetter):" -ChildPath '\'
                        $windir = Join-Path -Path $WinDrive -ChildPath Windows
                        Write-Verbose -Message "[$($MyInvocation.MyCommand)] [$DiskNumber] Windows Partition [$($WindowsPartition.partitionNumber)] : Applying image from [$SourcePath] to [$WinDrive] using Index [$Index]"
                        if ($UseDismExpansion) {
                            $null = Expand-DpWindowsImage -ImagePath $SourcePath -Index $Index -ApplyPath $WinDrive -ErrorAction Stop
                        } else {
                            $null = Expand-WindowsImage -ImagePath $SourcePath -Index $Index -ApplyPath $WinDrive -ErrorAction Stop
                        }

                        #region Modify the OS with Drivers, Active Features and Packages
                        if ($Driver) {
                            Write-Verbose -Message "[$($MyInvocation.MyCommand)] [$DiskNumber] : Adding Windows Drivers to the Image"

                            $Driver | ForEach-Object -Process
                            {
                                Write-Verbose -Message "[$($MyInvocation.MyCommand)] [$DiskNumber] : Driver path: [$PSItem]"
                                $Null = Add-WindowsDriver -Path $WinDrive -Recurse -Driver $PSItem
                            }
                        }
                        if ($filesToInject) {
                            # TODO: verify consistency for folder path IE: dest exists or not.
                            foreach ($filePath in $filesToInject) {
                                Write-Verbose -Message "[$($MyInvocation.MyCommand)] [$DiskNumber] Windows Partition [$($WindowsPartition.partitionNumber)] : Adding files from $filePath"
                                $recurse = $false
                                if (Test-Path $filePath -PathType Container) {
                                    $recurse = $true
                                }
                                Copy-Item -Path $filePath -Destination $WinDrive -Recurse:$recurse
                            }
                        }


                        if ($Unattend) {
                            try {
                                Write-Verbose -Message "[$($MyInvocation.MyCommand)] [$DiskNumber] Windows Partition [$($WindowsPartition.partitionNumber)] : Adding Unattend.xml ($Unattend)"
                                Copy-Item $Unattend -Destination "$WinDrive\unattend.xml"
                            } catch {
                                Write-Error -Message "[$($MyInvocation.MyCommand)] [$DiskNumber] : Error Adding Unattend.xml "
                                throw $_.Exception.Message
                            }
                        }
                        if ($AddPayloadForRemovedFeature) {
                            $Feature = $Feature + (Get-WindowsOptionalFeature -Path $WinDrive | Where-Object -Property state -EQ -Value 'DisabledWithPayloadRemoved' ).FeatureName
                        }

                        If ($Feature) {
                            try {
                                Write-Verbose -Message "[$($MyInvocation.MyCommand)] [$DiskNumber] : Installing Windows Feature(s) : Collecting possible source paths"
                                $FeatureSourcePath = @()
                                $MountFolderList = @()

                                # ISO
                                if ($driveLetter) {
                                    $FeatureSourcePath += Join-Path -Path "$($driveLetter):" -ChildPath 'sources\sxs'
                                }

                                $notWinPE = $true
                                if ((Resolve-Path -Path $env:temp).drive.name -eq 'X') {
                                    $notWinPE = $false
                                    Write-Warning "WinPE does not support Mounting WIM, Feature sources must be present in the image OR -FeatureSource must be a Folder"
                                }
                                if (($FeatureSource) -and ($FeatureSource -ne 'NONE')) {
                                    Write-Verbose -Message "[$($MyInvocation.MyCommand)] [$DiskNumber] : Installing Windows Feature(s) : Source Path provided [$FeatureSource]"
                                    if (($FeatureSource |
                                            Resolve-Path |
                                            Get-Item ).PSIsContainer -eq $true ) {
                                        Write-Verbose -Message "[$($MyInvocation.MyCommand)] [$DiskNumber] : Installing Windows Feature(s) : Source Path [$FeatureSource] in folder"
                                        $FeatureSourcePath += $FeatureSource
                                    } elseif ((($FeatureSource |
                                                Resolve-Path |
                                                Get-Item ).extension -like '.wim') -and $notWinPE) {
                                        #$FeatureSourcePath += Convert-Path $FeatureSource
                                        $MountFolder = [System.IO.Directory]::CreateDirectory((Join-Path -Path $env:temp -ChildPath ([System.IO.Path]::GetRandomFileName().split('.')[0])))
                                        $MountFolderList += $MountFolder.FullName
                                        Write-Verbose -Message "[$($MyInvocation.MyCommand)] [$DiskNumber] : Installing Windows Feature(s) : Mounting Source [$FeatureSource] Index [$FeatureSourceIndex]"
                                        $null = Mount-WindowsImage -ImagePath $FeatureSource -Index $FeatureSourceIndex -Path  $MountFolder.FullName -ReadOnly
                                        $FeatureSourcePath += Join-Path -Path $MountFolder.FullName -ChildPath 'Windows\WinSxS'
                                    } else {
                                        if ($FeatureSource -ne 'NONE') {
                                            Write-Warning -Message "$FeatureSource is not a .wim or folder"
                                        }
                                    }
                                } elseif ($notWinPE) {
                                    #NO $FeatureSource

                                    $images = Get-WindowsImage -ImagePath $SourcePath

                                    foreach ($image in $images) {
                                        $MountFolder = [System.IO.Directory]::CreateDirectory((Join-Path -Path $env:temp -ChildPath ([System.IO.Path]::GetRandomFileName().split('.')[0])))
                                        $MountFolderList += $MountFolder.FullName
                                        Write-Verbose -Message "[$($MyInvocation.MyCommand)] [$DiskNumber] : Installing Windows Feature(s) : Mounting Source [$SourcePath] [$($image.ImageIndex)] [$($image.ImageName)] to [$($MountFolder.FullName)] "
                                        $null = Mount-WindowsImage -ImagePath $SourcePath -Index $image.ImageIndex -Path  $MountFolder.FullName -ReadOnly
                                        $FeatureSourcePath += Join-Path -Path $MountFolder.FullName -ChildPath 'Windows\WinSxS'
                                    }
                                } #end if FeatureSource

                                if ($FeatureSourcePath.count -gt 0) {
                                    Write-Verbose -Message "[$($MyInvocation.MyCommand)] [$DiskNumber] : Installing Windows Feature(s) [$Feature] to the Image [$WinDrive] : Search Source Path [$FeatureSourcePath]"
                                    $null = Enable-WindowsOptionalFeature -Path $WinDrive -All -FeatureName $Feature -Source $FeatureSourcePath
                                } else {
                                    Write-Verbose -Message "[$($MyInvocation.MyCommand)] [$DiskNumber] : Installing Windows Feature(s) [$Feature] to the Image [$WinDrive] : No Source Path"
                                    $null = Enable-WindowsOptionalFeature -Path $WinDrive -All -FeatureName $Feature
                                }
                            } catch {
                                Write-Error -Message "[$($MyInvocation.MyCommand)] [$DiskNumber] : Error Installing Windows Feature "
                                throw $_.Exception.Message
                            } finally {
                                foreach ($MountFolder in $MountFolderList) {
                                    $null = Dismount-WindowsImage -Path $MountFolder -Discard
                                    Remove-Item $MountFolder
                                }
                            }
                        }

                        if ($Package) {
                            Write-Verbose -Message "[$($MyInvocation.MyCommand)] [$DiskNumber] : Adding Windows Packages to the Image"

                            $Package | ForEach-Object -Process {
                                Write-Verbose -Message "[$($MyInvocation.MyCommand)] [$DiskNumber] : Package path: [$PSItem]"
                                $Null = Add-WindowsPackage -Path $WinDrive -PackagePath $PSItem
                            }
                        }
                        if ($RemoveFeature) {
                            Write-Verbose -Message "[$($MyInvocation.MyCommand)] [$DiskNumber] : Removing Windows Features from the Image [$WinDrive]"

                            try {
                                $null = Disable-WindowsOptionalFeature -Path $WinDrive -FeatureName $RemoveFeature @ParametersToPass
                            } catch {
                                Write-Error -Message "[$($MyInvocation.MyCommand)] [$DiskNumber] : Error Removing Windows Feature [$RemoveFeature] "
                                throw $_.Exception.Message
                            }
                        }

                        #endregion
                    } else {
                        throw 'Unable to find OS partition'
                    }
                    #endregion

                    #region System partition
                    if ($SystemPartition -and (-not ($NativeBoot))) {
                        $systemDrive = "$($SystemPartition.driveLetter):"


                        $bcdBootArgs = @(
                            "$windir", # Path to the \Windows on the Disk
                            "/s $systemDrive", # Specifies the volume letter of the drive to create the \BOOT folder on.
                            '/v'                        # Enabled verbose logging.
                        )

                        Write-Verbose -Message "[$($MyInvocation.MyCommand)] [$DiskNumber] : Disk Layout [$DiskLayout]"
                        switch ($DiskLayout) {
                            'UEFI' {
                                $bcdBootArgs += '/f UEFI'   # Specifies the firmware type of the target system partition
                            }
                            'BIOS' {
                                $bcdBootArgs += '/f BIOS'   # Specifies the firmware type of the target system partition
                            }

                            'WindowsToGo' {
                                # Create entries for both UEFI and BIOS if possible
                                if (Test-Path -Path "$($windowsDrive)\Windows\boot\EFI\bootmgfw.efi") {
                                    $bcdBootArgs += '/f ALL'
                                }
                            }
                        }
                        Write-Verbose -Message "[$($MyInvocation.MyCommand)] [$DiskNumber] System Partition [$($SystemPartition.partitionNumber)] : Running [$windir\System32\bcdBoot.exe] -> $bcdBootArgs"
                        RunExecutable -Executable "$windir\System32\bcdBoot.exe" -Arguments $bcdBootArgs @ParametersToPass
                    }
                    #endregion

                    #region Recovery Tools
                    if ($RecoveryToolsPartition) {
                        $recoverFolder = Join-Path -Path "$($RecoveryToolsPartition.DriveLetter):" -ChildPath 'Recovery'
                        $WindowsRe = Join-Path -Path "$recoverFolder" -ChildPath 'WindowsRe'
                        Write-Verbose -Message "[$($MyInvocation.MyCommand)] [$DiskNumber] Recovery Tools Partition [$($RecoveryToolsPartition.partitionNumber)] : Creating Recovery\WindowsRE folder [$WindowsRe]"
                        $null = mkdir -Path "$($RecoveryToolsPartition.driveLetter):\Recovery\WindowsRE" -ErrorAction SilentlyContinue

                        #the winre.wim file is hidden
                        Get-ChildItem -Path "$windir\System32\recovery\winre.wim" -Hidden |
                        Copy-Item -Destination $WindowsRe.FullName

                        #? Windows 10 and server have this defaulted to enabled and to the same location.
                        #Write-Verbose -Message "[$($MyInvocation.MyCommand)] [$DiskNumber] Recovery Tools Partition [$($RecoveryToolsPartition.partitionNumber)] : Register Recovery Image "
                        #$null = Start-Process -NoNewWindow -Wait -FilePath "$windir\System32\reagentc.exe" -ArgumentList "/setreimage /path $WindowsRe /target $windir"
                    }
                    #endregion
                } catch {
                    Write-Error -Message "[$($MyInvocation.MyCommand)] [$DiskNumber] : Error setting partition content "
                    throw $_.Exception.Message
                } finally {
                    Write-Verbose -Message "[$($MyInvocation.MyCommand)] [$DiskNumber] : Removing Drive letters"
                    Get-Partition -DiskNumber $DiskNumber |
                    Where-Object -FilterScript {
                        $_.driveLetter
                    } |
                    Where-Object -Property Type -NE -Value 'Basic' |
                    Where-Object -Property Type -NE -Value 'IFS' |
                    ForEach-Object -Process {
                        $dl = "$($_.DriveLetter):"
                        $_ |
                        Remove-PartitionAccessPath -AccessPath $dl
                    }
                    #dismount
                    if ($isoPath -and (Get-DiskImage $isoPath).Attached) {
                        $null = Dismount-DiskImage -ImagePath $isoPath
                    }
                    Write-Verbose -Message "[$($MyInvocation.MyCommand)] [$DiskNumber] : Finished"
                }
            } else {
                Write-Warning -Message 'Process aborted by user'
            }
        }
    }
}