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' } } } } |