Files
FFU/archive/FFUDevelopment/BuildFFUVMv2.ps1
T
2023-05-22 16:41:08 -07:00

563 lines
19 KiB
PowerShell
Raw Blame History

This file contains invisible Unicode characters
This file contains invisible Unicode characters that are indistinguishable to humans but may be processed differently by a computer. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
#Requires -Modules Hyper-V, Storage
#Requires -PSEdition Desktop
# To do list (will get around to this stuff sometime next week)
# Change from using variables to parameters - this way you don't even need to open the script to edit it.
# Clean up the output it sends to powershell so you know what step you're on
# Do some real logging incase folks get in trouble and need my help
# Edit the WinPECaptureFFUFiles\CaptureFFU.ps1 file within this script instead of doing it manually. 
# Make Capture/Deploy media creation optional
# Change DownloadFFU.xml file to match C:\FFUDevelopment path
# If drivers = true, check if drivers folder exists
# C:\VM\VMFolder isn't being deleted
#Modify Required variables
$ISOPath = "E:\software\ISOs\Windows\Windows 11\en-us_windows_11_consumer_editions_version_22h2_updated_feb_2023_x64_dvd_4fa87138.iso"
$WindowsSKU = 'Pro'
$FFUDevelopmentPath = 'C:\FFUDevelopment'
$AppsISO = "$FFUDevelopmentPath\Apps.iso"
$AppsPath = "$FFUDevelopmentPath\Apps"
$InstallOffice = $true
$InstallApps = $true
$InstallDrivers = $true
$memory = 8GB
$disksize = 30GB
$processors = 4
$VMSwitchName = '*intel*'
#Optional variables
$rand = get-random
$VMName = "_FFU-$rand"
$VMLocation = "c:\VM"
$VMPath = $VMLocation + $VMName
$VHDXPath = "$VMPath\$VMName.vhdx"
#FUNCTIONS
Function Get-ADK {
# Define the registry key and value name to query
$adkRegKey = "HKLM:\SOFTWARE\WOW6432Node\Microsoft\Windows Kits\Installed Roots"
$adkRegValueName = "KitsRoot10"
# Check if the registry key exists
if (Test-Path $adkRegKey) {
# Get the registry value for the Windows ADK installation path
$adkPath = (Get-ItemProperty -Path $adkRegKey -Name $adkRegValueName).$adkRegValueName
if ($adkPath) {
return $adkPath
}
}
else {
throw "Windows ADK is not installed or the installation path could not be found."
}
}
function Get-ODTURL {
[String]$MSWebPage = Invoke-RestMethod 'https://www.microsoft.com/en-us/download/confirmation.aspx?id=49117'
$MSWebPage | ForEach-Object {
if ($_ -match 'url=(https://.*officedeploymenttool.*\.exe)') {
$matches[1]
}
}
}
function Get-Office {
#Download ODT
$ODTUrl = Get-ODTURL
$ODTInstallFile = "$env:TEMP\odtsetup.exe"
Invoke-WebRequest -Uri $ODTUrl -OutFile $ODTInstallFile
# Extract ODT
$ODTPath = "$AppsPath\Office"
Start-Process -FilePath $ODTInstallFile -ArgumentList "/extract:$ODTPath /quiet" -Wait
# Run setup.exe with config.xml
$ConfigXml = "$ODTPath\DownloadFFU.xml"
#Set-Location $ODTPath
Start-Process -FilePath "$ODTPath\setup.exe" -ArgumentList "/download $ConfigXml" -Wait
#Clean up default configuration files
Remove-Item -Path "$ODTPath\configuration*" -Force
}
function New-AppsISO {
#Create Apps ISO file
$OSCDIMG = 'C:\Program Files (x86)\Windows Kits\10\Assessment and Deployment Kit\Deployment Tools\amd64\Oscdimg\oscdimg.exe'
Start-Process -FilePath $OSCDIMG -ArgumentList "-n -m -d $Appspath $AppsISO" -wait
#Remove the Office Download and ODT
if ($InstallOffice) {
$ODTPath = "$AppsPath\Office"
$OfficeDownloadPath = "$ODTPath\Office"
Remove-Item -Path $OfficeDownloadPath -Recurse -Force
Remove-Item -Path "$ODTPath\setup.exe"
}
}
function Get-WimFromISO {
# Mount the ISO file using Mount-DiskImage cmdlet
$mountResult = Mount-DiskImage -ImagePath $isoPath -PassThru
# Get the drive letter of the mounted ISO
$driveLetter = ($mountResult | Get-Volume).DriveLetter
# Construct the path to the install.wim file
$wimPath = $driveLetter + ":\sources\install.wim"
# Display the path to the install.wim file
Write-Host "The path to the install.wim file is: $wimPath"
return $wimpath
}
function Get-WimIndex {
[Parameter(Mandatory = $true)]
[string]$WindowsSKU
$wimindex = switch ($WindowsSKU) {
Home { 1 }
Home_N { 2 }
Home_SL { 3 }
EDU { 4 }
EDU_N { 5 }
Pro { 6 }
Pro_N { 7 }
Pro_EDU { 8 }
Pro_Edu_N { 9 }
Pro_WKS { 10 }
Pro_WKS_N { 11 }
Default { 6 }
}
Return $WimIndex
}
#Build VHDX
#Create VHDX
function New-ScratchVhdx {
param(
[Parameter(Mandatory = $true)]
[string]$VhdxPath,
[uint64]$SizeBytes = 30GB,
[ValidateSet(512, 4096)]
[uint32]$LogicalSectorSizeBytes = 512,
[switch]$Dynamic,
[Microsoft.PowerShell.Cmdletization.GeneratedTypes.Disk.PartitionStyle]$PartitionStyle = [Microsoft.PowerShell.Cmdletization.GeneratedTypes.Disk.PartitionStyle]::GPT
)
Write-Host "Creating new Scratch VHDX..."
$newVHDX = New-VHD -Path $VhdxPath -SizeBytes $disksize -LogicalSectorSizeBytes $LogicalSectorSizeBytes -Fixed:$true
$toReturn = $newVHDX | Mount-VHD -Passthru | Initialize-Disk -PassThru -PartitionStyle GPT
#Remove auto-created system partition so we can create the correct partition layout
remove-partition $toreturn.DiskNumber -PartitionNumber 1 -Confirm:$False
Write-Host "Done."
return $toReturn
}
#Add System Partition
function New-SystemPartition {
param(
[Parameter(Mandatory = $true)]
[ciminstance]$VhdxDisk,
[uint64]$SystemPartitionSize = 256MB
)
Write-Host "Creating System partition..."
$sysPartition = $VhdxDisk | New-Partition -AssignDriveLetter -Size $SystemPartitionSize -GptType "{c12a7328-f81f-11d2-ba4b-00a0c93ec93b}" -IsHidden
$sysPartition | Format-Volume -FileSystem FAT32 -Force -NewFileSystemLabel "System"
Write-Host "Done. System partition at drive $($sysPartition.DriveLetter):"
return $sysPartition.DriveLetter
}
#Add MSRPartition - skip this initially unless needed
function New-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
}
#Add OS Partition
function New-OSPartition {
param(
[Parameter(Mandatory = $true)]
[ciminstance]$VhdxDisk,
[Parameter(Mandatory = $true)]
[string]$WimPath,
[uint32]$WimIndex,
[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
}
$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
}
#Add Recovery partition
function New-RecoveryPartition {
param(
[Parameter(Mandatory = $true)]
[ciminstance]$VhdxDisk,
[Parameter(Mandatory = $true)]
$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 (($null -ne $winReWim) -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 ($null -ne $DataPartition) {
$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
}
#Add boot files
function Add-BootFiles {
param(
[Parameter(Mandatory = $true)]
[string]$OsPartitionDriveLetter,
[Parameter(Mandatory = $true)]
[string]$SystemPartitionDriveLetter,
[string]$FirmwareType = 'UEFI'
)
Write-Host "Adding boot files for `"$($OsPartitionDriveLetter):\Windows`" to System partition `"$($SystemPartitionDriveLetter):`"..."
bcdboot "$($OsPartitionDriveLetter):\Windows" /S "$($SystemPartitionDriveLetter):" /F "$FirmwareType"
Write-Host "Done."
}
#Dismount VHDX
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."
}
}
#Delete old VMs and remove old certs
function Remove-FFUVM {
$certPath = 'Cert:\LocalMachine\Shielded VM Local Certificates\'
$OLDFFUVMs = get-vm _ffu-* | Where-Object { $_.state -ne 'running' }
If ($null -ne $OLDFFUVMs) {
Foreach ($OLDFFUVM in $OLDFFUVMs) {
$OldVMName = $OLDFFUVM.VMName
Remove-VM -Name $OLDFFUVM.name -Force -ErrorAction SilentlyContinue
#Remove-Item -Path "C:\VM\$OldVMName" -Force -Recurse -ErrorAction SilentlyContinue
Remove-Item -Path "$VMLocation\$OLDVMName" -Force -Recurse -ErrorAction SilentlyContinue
Remove-HgsGuardian -Name $OldVMName
$certs = Get-ChildItem -Path $certPath -Recurse | Where-Object { $_.Subject -like "*$OldVMName*" }
foreach ($cert in $Certs) {
Remove-item -Path $cert.PSPath -force
}
}
}
}
function New-FFUVM {
#Create new Gen2 VM
$VM = New-VM -Name $VMName -Path $VMPath -MemoryStartupBytes $memory -VHDPath $VHDXPath -Generation 2
Set-VMProcessor -VMName $VMName -Count $processors
#Mount Office ISO
Add-VMDvdDrive -VMName $VMName -Path $AppsISO
$VMHardDiskDrive = Get-VMHarddiskdrive -VMName $VMName
Set-VMFirmware -VMName $VMName -FirstBootDevice $VMHardDiskDrive
Set-VM -Name $VMName -AutomaticCheckpointsEnabled $false -StaticMemory
#Configure TPM
New-HgsGuardian -Name $VMName -GenerateCertificates
$owner = get-hgsguardian -Name $VMName
$kp = New-HgsKeyProtector -Owner $owner -AllowUntrustedRoot
Set-VMKeyProtector -VMName $VMName -KeyProtector $kp.RawData
Enable-VMTPM -VMName $VMName
#Connect to VM
vmconnect $VM.ComputerName $VMName
#Start VM
Start-VM -Name $VMName
return $VM
}
function New-PEMedia {
param (
[Parameter()]
[switch]
$Capture,
[Parameter()]
[switch]
$Deploy
)
#Need to use the Demployment and Imaging tools environment to create winPE media
$DandIEnv = "$adkPath`Assessment and Deployment Kit\Deployment Tools\DandISetEnv.bat"
$WinPEFFUPath = "$FFUDevelopmentPath\WinPE"
If (Test-path -Path "$WinPEFFUPath") {
#Dismount-WindowsImage -Path "$WinPEFFUPath\mount" -discard
Remove-Item -Path "$WinPEFFUPath" -Recurse -Force
}
& cmd /c """$DandIEnv"" && copype amd64 $WinPEFFUPath"
Mount-WindowsImage -ImagePath "$WinPEFFUPath\media\sources\boot.wim" -Index 1 -Path "$WinPEFFUPath\mount"
$Packages = @(
"WinPE-WMI.cab",
"en-us\WinPE-WMI_en-us.cab",
"WinPE-NetFX.cab",
"en-us\WinPE-NetFX_en-us.cab",
"WinPE-Scripting.cab",
"en-us\WinPE-Scripting_en-us.cab",
"WinPE-PowerShell.cab",
"en-us\WinPE-PowerShell_en-us.cab",
"WinPE-StorageWMI.cab",
"en-us\WinPE-StorageWMI_en-us.cab",
"WinPE-DismCmdlets.cab",
"en-us\WinPE-DismCmdlets_en-us.cab"
)
$PackagePathBase = "$adkPath`Assessment and Deployment Kit\Windows Preinstallation Environment\amd64\WinPE_OCs\"
foreach ($Package in $Packages) {
$PackagePath = Join-Path $PackagePathBase $Package
Add-WindowsPackage -Path "$WinPEFFUPath\mount" -PackagePath $PackagePath | Out-Null
}
If($Capture){
Copy-Item -Path "$FFUDevelopmentPath\WinPECaptureFFUFiles\*" -Destination "$WinPEFFUPath\mount" -Recurse -Force
#Remove Bootfix.bin if capturing from BIOS systems
Remove-Item -Path "$WinPEFFUPath\media\boot\bootfix.bin" -Force
}
If($Deploy){
Copy-Item -Path "$FFUDevelopmentPath\WinPEDeployFFUFiles\*" -Destination "$WinPEFFUPath\mount" -Recurse -Force
# If you need to add drivers (storage/keyboard most likely), remove the '#' from the below line and change the /Driver:Path to a folder of drivers
# & dism /image:$WinPEFFUPath\mount /Add-Driver /Driver:<Path to Drivers folder e.g c:\drivers> /Recurse
}
Dismount-WindowsImage -Path "$WinPEFFUPath\mount" -Save
#Make ISO
$OSCDIMGPath = "$adkPath`Assessment and Deployment Kit\Deployment Tools\amd64\Oscdimg"
$OSCDIMG = "$OSCDIMGPath\oscdimg.exe"
& "$OSCDIMG" -m -o -u2 -udfver102 -bootdata:2`#p0,e,b$OSCDIMGPath\etfsboot.com`#pEF,e,b$OSCDIMGPath\Efisys_noprompt.bin $WinPEFFUPath\media $FFUDevelopmentPath\WinPE_FFU_Capture.iso
Remove-Item -Path "$WinPEFFUPath" -Recurse -Force
}
function New-FFU {
#Need to use the Demployment and Imaging tools environment to use dism from the Insider ADK to optimize the FFU. This is only needed until Windows 23H2.
$DandIEnv = "$adkPath`Assessment and Deployment Kit\Deployment Tools\DandISetEnv.bat"
#Mount the Capture ISO to the VM
$CaptureISOPath = "$FFUDevelopmentPath\WinPE_FFU_Capture.iso"
$FFUVMs = get-vm _ffu-* | Where-Object { $_.state -ne 'running' }
If ($null -ne $FFUVMs) {
Foreach ($FFUVM in $FFUVMs) {
$VMName = $FFUVM.name
$VMDVDDrive = Get-VMDvdDrive -VMName $VMName
Set-VMFirmware -VMName $VMName -FirstBootDevice $VMDVDDrive
Set-VMDvdDrive -VMName $VMName -Path $CaptureISOPath
$VMSwitch = Get-VMSwitch -name $VMSwitchName
get-vm $VMName | Get-VMNetworkAdapter | Connect-VMNetworkAdapter -SwitchName $VMSwitch.Name
#vmconnect $FFUVM.ComputerName $VMName
}
}
#Start VM
Start-VM -Name $VMName
# Wait for the VM to turn off
do {
$FFUVM = Get-VM -Name $VMName
Start-Sleep -Seconds 5
} while ($FFUVM.State -ne 'Off')
# Check for .ffu files in the FFUDevelopment folder
$FFUFiles = Get-ChildItem -Path $FFUDevelopmentPath -Filter "*.ffu" -File
# If there's more than one .ffu file, get the most recent and store its path in $FFUFile
if ($FFUFiles.Count -gt 0) {
$FFUFile = ($FFUFiles | Sort-Object -Property LastWriteTime -Descending | Select-Object -First 1).FullName
Write-Host "Most recent .ffu file: $FFUFile"
}
else {
Write-Host "No .ffu files found in $FFUFolderPath"
}
#Add drivers
If ($InstallDrivers){
New-Item -Path "$FFUDevelopmentPath\Mount" -ItemType Directory -Force
Mount-WindowsImage -ImagePath $FFUFile -Index 1 -Path "$FFUDevelopmentPath\Mount"
Add-WindowsDriver -Path "$FFUDevelopmentPath\Mount" -Driver "$FFUDevelopmentPath\Drivers" -Recurse
Dismount-WindowsImage -Path "$FFUDevelopmentPath\Mount" -Save
Remove-Item -Path "$FFUDevelopmentPath\Mount" -Recurse -Force
}
#Optimize FFU
& cmd /c """$DandIEnv"" && dism /optimize-ffu /imagefile:$FFUFile"
}
try {
#Check if the Windows ADK is installed
$adkPath = Get-ADK
}
catch {
throw $_
}
#Build ISO for Office and Apps
try{
if($InstallOffice){
Get-Office
}
if($InstallApps){
New-AppsISO
}
}
catch {
Write-Host "Getting Office and/or building the AppsISO failed"
throw $_
}
#Create VHDX
try {
$wimPath = Get-WimFromISO
$WimIndex = Get-WimIndex
$vhdxDisk = New-ScratchVhdx -VhdxPath $VHDXPath -SizeBytes $disksize -Dynamic:$true
$systemPartitionDriveLetter = New-SystemPartition -VhdxDisk $vhdxDisk
New-MSRPartition -VhdxDisk $vhdxDisk
$osPartition = New-OSPartition -VhdxDisk $vhdxDisk -OSPartitionSize $OSPartitionSize -WimPath $WimPath -WimIndex $WimIndex[1]
$osPartitionDriveLetter = $osPartition[1].DriveLetter
$recoveryPartition = New-RecoveryPartition -VhdxDisk $vhdxDisk -OsPartition $osPartition[1] -RecoveryPartitionSize $RecoveryPartitionSize -DataPartition $dataPartition
Write-Host "All necessary partitions created."
Add-BootFiles -OsPartitionDriveLetter $osPartitionDriveLetter -SystemPartitionDriveLetter $systemPartitionDriveLetter[1]
New-Item -Path "$($osPartitionDriveLetter):\Windows\Panther\unattend" -ItemType Directory
Copy-Item -Path "$FFUDevelopmentPath\BuildFFUUnattend\unattend.xml" -Destination "$($osPartitionDriveLetter):\Windows\Panther\Unattend\Unattend.xml" -Force
}
finally {
Dismount-ScratchVhdx -VhdxPath $VHDXPath
Dismount-DiskImage -ImagePath $ISOPath
}
#Clean up old VMs
try {
Remove-FFUVM
}
catch {
Write-Host "VM cleanup failed"
throw $_
}
#Create VM and attach VHDX
try {
$FFUVM = New-FFUVM
}
catch {
Write-Host 'VM creation failed'
throw $_
}
#Create Capture Media
try{
#This should happen while the FFUVM is building
New-PEMedia -Capture
}
catch{
throw $_
}
#Capture FFU file
try {
#Check if VM is done provisioning
do {
$FFUVM = Get-VM -Name $FFUVM.Name
Start-Sleep -Seconds 10
} while ($FFUVM.State -ne 'Off')
#Capture FFU file
New-FFU
}
Catch {
throw $_
}