#Requires -Modules Hyper-V, Storage #Requires -PSEdition Desktop <# .NOTES Copyright (c) Microsoft Corporation. Licensed under the MIT License. .SYNOPSIS Creates an FFU with proper disk layout given an input WIM .DESCRIPTION Creates an FFU with proper disk layout given an input WIM .PARAMETER WimPath The path to the WIM to be converted into an FFU .PARAMETER WimIndex The index of the image within the WIM to be used .PARAMETER FfuPath Output path of the FFU to be created .PARAMETER ScratchVhdxPath Output path of the scratch VHDX which will be created as an intermediate-step to the process of creating the FFU. .PARAMETER SaveScratchVhdx Keep the scratch VHDX after capturing the FFU. This VHDX can be used to create a VM for testing. .PARAMETER SizeBytes Size in bytes of the disk the FFU is to be applied to. With an Optimized FFU, the FFU can then be applied to any disk large enough for the data within the FFU. .PARAMETER LogicalSectorSizeBytes Logical sector size to store data within the FFU. This should match the logical sector size of the disks this FFU is applied to. .PARAMETER Dynamic Whether the scratch VHDX is to be a dynamically sized file or a fixed file size to match the maximum size of the VHDX. .PARAMETER PartitionStyle GPT or MBR partition style for the final disk layout. .PARAMETER SkipSystemPartition Skips creating a System partition. Boot files will be placed on the OS partition. .PARAMETER SystemPartitionSize If creating a System partition, specifies the size in bytes of that partition. .PARAMETER SkipMSRPartition Skips creating an MSR partition. .PARAMETER OSPartitionSize Allows the specification of the OS partition size. If left at default, the OS partition will take all available space, minus what is needed by the Recovery partition, if any. If a Data partition is specified, an OS partition size must be specified. .PARAMETER AddDataPartition Allows the addition of an extra Data partition, separate from the OS partition. .PARAMETER DataPartitionSize Allows specifying the size of the Data partition, if the -AddDataPartition flag is used. .PARAMETER SkipRecoveryPartition Skips the creation of a Recovery partition, used to store the Windows Recovery Environment (WinRE.wim). .PARAMETER RecoveryPartitionSize Specifies the size of the Recovery partition to be created. If left at default, the partition will be the size of the WinRE.wim in the OS partition plus 52 MB plus a buffer of 32 MB since free space of WinRE.wim + 52 MB is needed. .PARAMETER FFUDriveName Name passed to DISM's /Capture-FFU /Name parameter, used to set a name for the FFU, separate from the file name. .PARAMETER FFUCompression Specifies FFU compression of Default or None. .PARAMETER FirmwareType Specifies to create boot files for the disk for a firmware of BIOS, UEFI, or both (ALL). .PARAMETER OptimizeFfu Creates an Optimized FFU which can be applied to a disk of a different size than the original FFU disk size as long as the disk it is applied to is large enough to fit the data within the FFU. Optimized FFUs are only available on Windows version 1903 and higher. .PARAMETER Force Forces the overwriting of existing scratch VHDX or FFUs if the script is run multiple times or specifying a path with an existing VHDX or FFU of the same name. .EXAMPLE .\Convert-WimToFfu.ps1 -WimPath .\install.wim Creates an FFU named install.ffu in the same directory as the passed install.wim .EXAMPLE .\Convert-WimToFfu.ps1 -WimPath .\install.wim -WimIndex 1 -FfuPath .\flash.ffu Creates an FFU from the Windows image at index 1 within install.wim and names the FFU "flash.ffu" .EXAMPLE .\Convert-WimToFfu.ps1 -WimPath .\install.wim -WimIndex 1 -FfuPath .\flash.ffu -SizeBytes 64GB Creates an FFU that can only be applied on "64GB" disks. Keep in mind that 64GB is 68,719,476,736 bytes which may be larger than the target disks. .EXAMPLE .\Convert-WimToFfu.ps1 -WimPath .\install.wim -WimIndex 1 -FfuPath .\flash.ffu -OptimizeFfu Creates an FFU which can be applied to disks of a different size than the original FFU disk size. #> param( [Parameter(Mandatory = $true, Position = 0)] [Alias("Path")] [ValidateScript({ Test-Path $_ })] [string]$WimPath, [uint32]$WimIndex = 1, [string]$FfuPath, [string]$ScratchVhdxPath, [switch]$SaveScratchVhdx, [uint64]$SizeBytes = 31000000000, [ValidateSet(512, 4096)] [uint32]$LogicalSectorSizeBytes = 512, [switch]$Dynamic, [Microsoft.PowerShell.Cmdletization.GeneratedTypes.Disk.PartitionStyle]$PartitionStyle = [Microsoft.PowerShell.Cmdletization.GeneratedTypes.Disk.PartitionStyle]::GPT, [switch]$SkipSystemPartition, [uint64]$SystemPartitionSize = 256MB, [switch]$SkipMSRPartition, [uint64]$OSPartitionSize = 0, [switch]$AddDataPartition, [uint64]$DataPartitionSize = 0, [switch]$SkipRecoveryPartition, [uint64]$RecoveryPartitionSize = 0, [string]$FFUDriveName = "WimToFfu", [ValidateSet("Default", "None")] [string]$FFUCompression = "Default", [ValidateSet("UEFI", "BIOS", "ALL")] [string]$FirmwareType = "UEFI", [switch]$OptimizeFFU, [switch]$Force ); #region FUNCTIONS function Add-BootFiles { param( [Parameter(Mandatory = $true)] [string]$OsPartitionDriveLetter, [Parameter(Mandatory = $true)] [string]$SystemPartitionDriveLetter ); Write-Host "Adding boot files for `"$($OsPartitionDriveLetter):\Windows`" to System partition `"$($SystemPartitionDriveLetter):`"..."; bcdboot "$($OsPartitionDriveLetter):\Windows" /S "$($SystemPartitionDriveLetter):" /F "$FirmwareType"; Write-Host "Done."; } function Get-RecoveryPartition { param( [Parameter(Mandatory = $true)] [ciminstance]$VhdxDisk, [Parameter(Mandatory = $true)] [ciminstance]$OsPartition, [uint64]$RecoveryPartitionSize = 0, [ciminstance]$DataPartition ); Write-Host "Creating empty Recovery partition (to be filled on first boot automatically)..."; $calculatedRecoverySize = 0; $recoveryPartition = $null; if($RecoveryPartitionSize -gt 0) { $calculatedRecoverySize = $RecoveryPartitionSize; } else { $winReWim = Get-ChildItem "$($OsPartition.DriveLetter):\Windows\System32\Recovery\Winre.wim"; if(($winReWim -ne $null) -and ($winReWim.Count -eq 1)) { # Wim size + 52MB is minimum WinRE partition size. # NTFS and other partitioning size differences account for about 17MB of space that's unavailable. # Adding 32MB as a buffer to ensure there's enough space. $calculatedRecoverySize = $winReWim.Length + 52MB + 32MB; Write-Host "Calculated space needed for recovery in bytes: $calculatedRecoverySize"; if($DataPartition -ne $null) { $DataPartition | Resize-Partition -Size ($DataPartition.Size - $calculatedRecoverySize); Write-Host "Data partition shrunk by $calculatedRecoverySize bytes for Recovery partition."; } else { $OsPartition | Resize-Partition -Size ($OsPartition.Size - $calculatedRecoverySize); Write-Host "OS partition shrunk by $calculatedRecoverySize bytes for Recovery partition."; } $recoveryPartition = $VhdxDisk | New-Partition -AssignDriveLetter -UseMaximumSize -GptType "{de94bba4-06d1-4d40-a16a-bfd50179d6ac}" ` | Format-Volume -FileSystem NTFS -Confirm:$false -Force -NewFileSystemLabel "WinRE"; Write-Host "Done. Recovery partition at drive $($recoveryPartition.DriveLetter):"; } else { Write-Host "No WinRE.WIM found in the OS partition under \Windows\System32\Recovery."; Write-Host "Skipping creating the Recovery partition."; Write-Host "If a Recovery partition is desired, please re-run the script setting the -RecoveryPartitionSize flag as appropriate." } } return $recoveryPartition; } function Get-DataPartition { param( [Parameter(Mandatory = $true)] [ciminstance]$VhdxDisk, [uint64]$DataPartitionSize = 0 ); Write-Host "Creating Data partition..."; $dataPartition = $null; if(($OSPartitionSize -ne $null) -and ($OSPartitionSize -gt 0)) { if(($DataPartitionSize -ne $null) -and ($DataPartitionSize -gt 0)) { $dataPartition = $vhdxDisk | New-Partition -AssignDriveLetter -Size $DataPartitionSize; } else { $dataPartition = $vhdxDisk | New-Partition -AssignDriveLetter -UseMaximumSize; } } else { Write-Host "To add a data partition, OS partition size must be set. Skipping adding data partition..."; } Write-Host "Done. Data partition at drive $($dataPartition.DriveLetter):"; return $dataPartition; } function Get-OSPartition { param( [Parameter(Mandatory = $true)] [ciminstance]$VhdxDisk, [Parameter(Mandatory = $true)] [string]$WimPath, [uint32]$WimIndex = 1, [uint64]$OSPartitionSize = 0 ); Write-Host "Creating OS partition..."; if($OSPartitionSize -gt 0) { $osPartition = $vhdxDisk | New-Partition -AssignDriveLetter -Size $OSPartitionSize; } else { $osPartition = $vhdxDisk | New-Partition -AssignDriveLetter -UseMaximumSize; } $formattedOsPartition = $osPartition | Format-Volume -FileSystem NTFS -Confirm:$false -Force -NewFileSystemLabel "Windows"; Write-Host "Done. OS partition at drive $($osPartition.DriveLetter):"; Write-Host "Writing WIM at $WimPath to OS partition at drive $($osPartition.DriveLetter):..."; #Server 2019 is missing the Windows Overlay Filter (wof.sys), likely other Server SKUs are missing it as well. Script will error if trying to use the -compact switch on Server OSes if((Get-CimInstance Win32_OperatingSystem).Caption -match "Server"){ Write-Host (Expand-WindowsImage -ImagePath $WimPath -Index $WimIndex -ApplyPath "$($osPartition.DriveLetter):\"); } else { Write-Host (Expand-WindowsImage -ImagePath $WimPath -Index $WimIndex -ApplyPath "$($osPartition.DriveLetter):\" -Compact); } Write-Host "Done."; return $osPartition; } function Get-MSRPartition { param( [Parameter(Mandatory = $true)] [ciminstance]$VhdxDisk ); Write-Host "Creating MSR partition..."; $toReturn = $VhdxDisk | New-Partition -AssignDriveLetter -Size 16MB -GptType "{e3c9e316-0b5c-4db8-817d-f92df00215ae}" -IsHidden; Write-Host "Done."; return $toReturn; } function Get-SystemPartition { param( [Parameter(Mandatory = $true)] [ciminstance]$VhdxDisk, [uint64]$SystemPartitionSize = 100MB ); Write-Host "Creating System partition..."; $sysPartition = $VhdxDisk | New-Partition -AssignDriveLetter -Size $SystemPartitionSize -GptType "{c12a7328-f81f-11d2-ba4b-00a0c93ec93b}" -IsHidden; $formattedSysPartition = $sysPartition | Format-Volume -FileSystem FAT32 -Force -NewFileSystemLabel "System"; Write-Host "Done. System partition at drive $($sysPartition.DriveLetter):"; return $sysPartition.DriveLetter; } function Dismount-ScratchVhdx { param( [Parameter(Mandatory = $true)] [string]$VhdxPath ); if(Test-Path $VhdxPath) { Write-Host "Dismounting scratch VHDX..."; Dismount-VHD -Path $VhdxPath; Write-Host "Done."; } } function Get-ScratchVhdx { param( [Parameter(Mandatory = $true)] [string]$VhdxPath, [uint64]$SizeBytes = 64000000000, [ValidateSet(512, 4096)] [uint32]$LogicalSectorSizeBytes = 512, [switch]$Dynamic, [Microsoft.PowerShell.Cmdletization.GeneratedTypes.Disk.PartitionStyle]$PartitionStyle = [Microsoft.PowerShell.Cmdletization.GeneratedTypes.Disk.PartitionStyle]::GPT, [switch]$AddRecoveryPartition, [uint64]$RecoveryPartitionSize = 0 ); Write-Host "Creating new Scratch VHDX..."; $newVHDX = New-VHD -Path $VhdxPath -SizeBytes $SizeBytes -LogicalSectorSizeBytes $LogicalSectorSizeBytes -Dynamic:($Dynamic.IsPresent); $toReturn = $newVHDX | Mount-VHD -Passthru | Initialize-Disk -PassThru -PartitionStyle $PartitionStyle; Write-Host "Done."; return $toReturn; } function Get-OutputFilePath { param( [Parameter(Mandatory = $true)] [string]$WimPath, [string]$OutputFilePath, [Parameter(Mandatory = $true)] [string]$ParamName, [Parameter(Mandatory = $true)] [string]$Extension, [Parameter(Mandatory = $true)] [bool]$Force ); if([string]::IsNullOrEmpty($OutputFilePath)) { $OutputFilePath = [System.IO.Path]::ChangeExtension($WimPath, $Extension); } if((Test-Path $OutputFilePath) -and (-not $Force)) { throw New-Object System.ArgumentException("Unable to overwrite existing file $OutputFilePath without -Force flag.", $ParamName); } return $OutputFilePath; } function Write-If { param( [Parameter(Mandatory = $true)] [bool]$Condition, [Parameter(Mandatory = $true)] [string]$MessageIfTrue, [Parameter(Mandatory = $true)] [string]$MessageIfFalse ); if($Condition) { Write-Host $MessageIfTrue; } else { Write-Host $MessageIfFalse; } } #endregion #region MAIN SCRIPT BODY #region PRINT INPUT PARAMETERS Write-Host "Using WIM path: $WimPath."; $FfuPath = Get-OutputFilePath -WimPath $WimPath -OutputFilePath $FfuPath -ParamName "FfuPath" -Extension "ffu" -Force $Force.IsPresent; Write-Host "Using FFU path: $FfuPath."; $ScratchVhdxPath = Get-OutputFilePath -WimPath $WimPath -OutputFilePath $ScratchVhdxPath -ParamName "ScratchVhdxPath" -Extension "vhdx" -Force $Force.IsPresent; Write-Host "Using VHDX path: $ScratchVhdxPath."; Write-Host "Using WIM Index: $WimIndex."; Write-If -Condition $SaveScratchVhdx.IsPresent ` -MessageIfTrue "Will save intermediate scratch VHDX." ` -MessageIfFalse "Will delete intermediate scratch VHDX."; Write-Host "Using Disk Size (bytes) of $SizeBytes."; Write-Host "Using Logical Sector Size (bytes) of $LogicalSectorSizeBytes."; Write-If -Condition $Dynamic.IsPresent ` -MessageIfTrue "Intermediate scratch VHDX on disk will be Dynamically sized." ` -MessageIfFalse "Intermediate scratch VHDX on disk will be Fixed sized."; Write-Host "Partition style will be $PartitionStyle."; Write-If -Condition $SkipSystemPartition.IsPresent ` -MessageIfTrue "Will not add System partition." ` -MessageIfFalse "Will add System partition of size (bytes) $SystemPartitionSize."; Write-If -Condition $SkipMSRPartition.IsPresent ` -MessageIfTrue "Will not add MSR partition." ` -MessageIfFalse "Will add 16 MB MSR partition."; Write-If -Condition ($OSPartitionSize -eq 0) ` -MessageIfTrue "Will create maximum-sized OS partition." ` -MessageIfFalse "Will create OS partition of size (bytes) $OSPartitionSize."; Write-If -Condition $AddDataPartition.IsPresent ` -MessageIfTrue "Will add extra Data partition of size (bytes) $DataPartitionSize." ` -MessageIfFalse "Will not add extra Data partition."; Write-If -Condition $SkipRecoveryPartition.IsPresent ` -MessageIfTrue "Will not add Recovery partition." ` -MessageIfFalse "Will add Recovery partition."; Write-If -Condition $OptimizeFFU.IsPresent ` -MessageIfTrue "Will run DISM's /Optimize-FFU command." ` -MessageIfFalse "Will skip DISM's /Optimize-FFU command."; if(-not ($SkipSystemPartition.IsPresent)) { if($RecoveryPartitionSize -eq 0) { Write-Host "Will use default, calculated Recovery partition size (WinRE.WIM size + 52 MB + plus a buffer of 32 MB due to NTFS)."; } else { Write-Host "Will add Recovery partition of size (bytes) $RecoveryPartitionSize."; } } Write-Host "Using FFU Drive name of $FFUDriveName."; Write-Host "Using FFU compression of $FFUCompression."; Write-Host "Using Firmware Type (for boot files) of $FirmwareType."; Write-If -Condition $Force.IsPresent ` -MessageIfTrue "Force flag is present. Overwriting files when necessary." ` -MessageIfFalse "Force flag is not present. Will not overwrite existing files."; #endregion PRINT INPUT PARAMETERS try { $vhdxDisk = Get-ScratchVhdx -VhdxPath $ScratchVhdxPath -SizeBytes $SizeBytes -LogicalSectorSizeBytes $LogicalSectorSizeBytes -Dynamic:($Dynamic.IsPresent) -PartitionStyle $PartitionStyle; if(-not ($SkipSystemPartition.IsPresent)) { $systemPartitionDriveLetter = Get-SystemPartition -VhdxDisk $vhdxDisk -SystemPartitionSize $SystemPartitionSize; } if(-not ($SkipMSRPartition.IsPresent)) { $msrPartition = Get-MSRPartition -VhdxDisk $vhdxDisk; } $osPartition = Get-OSPartition -VhdxDisk $vhdxDisk -OSPartitionSize $OSPartitionSize -WimPath $WimPath -WimIndex $WimIndex; if($AddDataPartition.IsPresent) { $dataPartition = Get-DataPartition -VhdxDisk $vhdxDisk -DataPartitionSize $DataPartitionSize; } if(-not($SkipRecoveryPartition.IsPresent)) { $recoveryPartition = Get-RecoveryPartition -VhdxDisk $vhdxDisk -OsPartition $osPartition -RecoveryPartitionSize $RecoveryPartitionSize -DataPartition $dataPartition; } Write-Host "All necessary partitions created."; if($SkipSystemPartition.IsPresent) { Add-BootFiles -OsPartitionDriveLetter $osPartition.DriveLetter -SystemPartitionDriveLetter $osPartition.DriveLetter; } else { Add-BootFiles -OsPartitionDriveLetter $osPartition.DriveLetter -SystemPartitionDriveLetter $systemPartitionDriveLetter; } Write-Host "Capturing scratch VHDX into FFU..."; dism /Capture-FFU /ImageFile:"$FfuPath" /CaptureDrive:"\\.\PhysicalDrive$($vhdxDisk.DiskNumber)" /Name:"$FFUDriveName" /Compress:"$FFUCompression" Write-Host "Done."; } finally { Dismount-ScratchVhdx -VhdxPath $ScratchVhdxPath; } if($SaveScratchVhdx.IsPresent) { Write-Host "Scratch VHDX has been kept at $ScratchVhdxPath"; } else { Remove-Item -Path $ScratchVhdxPath -Force -Confirm:$false; Write-Host "Scratch VHDX has been deleted."; } if($OptimizeFFU.IsPresent) { Write-Host "Running DISM /Optimize-FFU /ImageFile:$FfuPath..."; dism /Optimize-FFU /ImageFile:"$FfuPath" Write-Host "Done."; } else { Write-Host "Skipping running DISM /Optimize-FFU."; } Write-Host "Convert-WimToFfu.ps1 script complete."; #endregion