From bc4a181182a5de9ecae4aef527aba80534b07694 Mon Sep 17 00:00:00 2001 From: Zehadi Alam <63765084+zehadialam@users.noreply.github.com> Date: Sat, 28 Dec 2024 20:01:01 -0500 Subject: [PATCH 01/11] Add support for LTSC versions of Windows --- FFUDevelopment/BuildFFUVM.ps1 | 115 +++++++++++++++--- .../WinPECaptureFFUFiles/CaptureFFU.ps1 | 4 +- 2 files changed, 104 insertions(+), 15 deletions(-) diff --git a/FFUDevelopment/BuildFFUVM.ps1 b/FFUDevelopment/BuildFFUVM.ps1 index efff889..d5f862e 100644 --- a/FFUDevelopment/BuildFFUVM.ps1 +++ b/FFUDevelopment/BuildFFUVM.ps1 @@ -224,7 +224,7 @@ param( [Parameter(Mandatory = $false, Position = 0)] [ValidateScript({ Test-Path $_ })] [string]$ISOPath, - [ValidateSet('Home', 'Home N', 'Home Single Language', 'Education', 'Education N', 'Pro', 'Pro N', 'Pro Education', 'Pro Education N', 'Pro for Workstations', 'Pro N for Workstations', 'Enterprise', 'Enterprise N', 'Standard', 'Standard (Desktop Experience)', 'Datacenter', 'Datacenter (Desktop Experience)')] + [ValidateSet('Home', 'Home N', 'Home Single Language', 'Education', 'Education N', 'Pro', 'Pro N', 'Pro Education', 'Pro Education N', 'Pro for Workstations', 'Pro N for Workstations', 'Enterprise', 'Enterprise N', 'Enterprise LTSC', 'Enterprise N LTSC', 'IoT Enterprise LTSC', 'IoT Enterprise N LTSC', 'Standard', 'Standard (Desktop Experience)', 'Datacenter', 'Datacenter (Desktop Experience)')] [string]$WindowsSKU = 'Pro', [ValidateScript({ Test-Path $_ })] [string]$FFUDevelopmentPath = $PSScriptRoot, @@ -284,7 +284,7 @@ param( [string]$ProductKey, [bool]$BuildUSBDrive, [Parameter(Mandatory = $false)] - [ValidateSet(10, 11, 2016, 2019, 2022, 2025)] + [ValidateSet(10, 11, 2016, 2019, 2021, 2022, 2024, 2025)] [int]$WindowsRelease = 11, [Parameter(Mandatory = $false)] [string]$WindowsVersion = '24h2', @@ -400,6 +400,26 @@ if ($ConfigFile -and (Test-Path -Path $ConfigFile)) { } } +$clientSKUs = @('Home', 'Home N', 'Home Single Language', 'Education', 'Education N', 'Pro', 'Pro N', 'Pro Education', 'Pro Education N', 'Pro for Workstations', 'Pro N for Workstations', 'Enterprise', 'Enterprise N') +$LTSCSKUs = @('Enterprise LTSC', 'Enterprise N LTSC', 'IoT Enterprise LTSC', 'IoT Enterprise N LTSC') +$ServerSKUs = @('Standard', 'Standard (Desktop Experience)', 'Datacenter', 'Datacenter (Desktop Experience)') +$releaseToSKUMapping = @{ + 10 = $clientSKUs + 11 = $clientSKUs + 2016 = $LTSCSKUs + $ServerSKUs + 2019 = $LTSCSKUs + $ServerSKUs + 2021 = $LTSCSKUs + 2022 = $ServerSKUs + 2024 = $LTSCSKUs + 2025 = $ServerSKUs +} +if ($releaseToSKUMapping.ContainsKey($WindowsRelease) -and $WindowsSKU -notin $releaseToSKUMapping[$WindowsRelease]) { + throw "Selected SKU is $WindowsSKU. Windows $WindowsRelease requires one of these SKUs: $($releaseToSKUMapping[$WindowsRelease] -join ', ')" +} +if ($WindowsRelease -notin 10, 11 -and -not $ISOPath) { + throw "Windows $WindowsRelease cannot automatically be downloaded. Please specify your own ISO using the -ISOPath parameter." +} + #Class definition for vhdx cache class VhdxCacheUpdateItem { [string]$Name @@ -461,7 +481,7 @@ if (-not $UnattendFolder) { $UnattendFolder = "$FFUDevelopmentPath\Unattend" } if (-not $AutopilotFolder) { $AutopilotFolder = "$FFUDevelopmentPath\Autopilot" } if (-not $PEDriversFolder) { $PEDriversFolder = "$FFUDevelopmentPath\PEDrivers" } if (-not $VHDXCacheFolder) { $VHDXCacheFolder = "$FFUDevelopmentPath\VHDXCache" } -if (-not $installationType) { $installationType = if ($WindowsRelease.ToString().Length -eq 2) { 'Client' } else { 'Server' } } +if (-not $installationType) { $installationType = if ($WindowsSKU -like "Standard*" -or $WindowsSKU -like "Datacenter*") { 'Server' } else { 'Client' } } if ($installationType -eq 'Server'){ #Map $WindowsRelease to $WindowsVersion for Windows Server switch ($WindowsRelease) { @@ -472,6 +492,15 @@ if ($installationType -eq 'Server'){ } } +if ($WindowsSKU -like "*LTSC") { + switch ($WindowsRelease) { + 2016 { $WindowsVersion = '1607' } + 2019 { $WindowsVersion = '1809' } + 2021 { $WindowsVersion = '21H2' } + 2024 { $WindowsVersion = '24H2' } + } +} + #FUNCTIONS function WriteLog($LogText) { Add-Content -path $LogFile -value "$((Get-Date).ToString()) $LogText" -Force -ErrorAction SilentlyContinue @@ -1809,7 +1838,7 @@ function Get-WindowsESD { elseif ($WindowsRelease -eq 11) { 'https://go.microsoft.com/fwlink/?LinkId=2156292' } else { - throw "Downloading Windows Server is not supported. Please use the -ISOPath parameter to specify the path to the Windows Server ISO file." + throw "Downloading Windows $WindowsRelease is not supported. Please use the -ISOPath parameter to specify the path to the Windows $WindowsRelease ISO file." } # Download cab file @@ -2619,6 +2648,10 @@ function Get-WimIndex { 'Pro_Edu_N' { 9 } 'Pro_WKS' { 10 } 'Pro_WKS_N' { 11 } + 'Enterprise' { 3 } + 'Enterprise N' { 4 } + 'Enterprise LTSC' { 1 } + 'Enterprise N LTSC' { 2 } Default { 6 } } } @@ -3371,6 +3404,8 @@ Function Get-WindowsVersionInfo { Professional { 'Pro' } ProfessionalEducation { 'Pro_Edu' } Enterprise { 'Ent' } + EnterpriseS { 'Ent_LTSC' } + IoTEnterpriseS { 'IoT_Ent_LTSC' } Education { 'Edu' } ProfessionalWorkstation { 'Pro_Wks' } ServerStandard { 'Srv_Std' } @@ -4356,9 +4391,15 @@ if ($InstallApps) { if ($UpdateLatestMSRT) { WriteLog "`$UpdateLatestMSRT is set to true." if ($WindowsArch -eq 'x64') { - if ($installationType -eq 'client') { + if ($WindowsRelease -in 10, 11) { $Name = """Windows Malicious Software Removal Tool x64""" + " " + """Windows $WindowsRelease""" } + elseif ($WindowsRelease -in 2016, 2019, 2021 -and $WindowsSKU -like "*LTSC") { + $Name = """Windows Malicious Software Removal Tool x64""" + " " + """Windows 10""" + } + elseif ($WindowsRelease -in 2024 -and $WindowsSKU -like "*LTSC") { + $Name = """Windows Malicious Software Removal Tool x64""" + " " + """Windows 11""" + } #Windows Server 2025 isn't listed as a product in the Microsoft Update Catalog, so we'll use the 2019 version elseif ($installationType -eq 'server' -and $WindowsRelease -eq '24H2') { $Name = """Windows Malicious Software Removal Tool x64""" + " " + """Windows Server 2019""" @@ -4495,7 +4536,7 @@ try { #The Windows release info page is updated later than the MU Catalog if ($UpdateLatestCU -and -not $UpdatePreviewCU) { Writelog "`$UpdateLatestCU is set to true, checking for latest CU" - if ($installationType -eq 'Client') { + if ($WindowsRelease -in 10, 11) { $Name = """Cumulative update for Windows $WindowsRelease Version $WindowsVersion for $WindowsArch""" } if ($WindowsRelease -eq 2025) { @@ -4504,9 +4545,20 @@ try { if ($WindowsRelease -eq 2022) { $Name = """Cumulative Update for Microsoft server operating system, version 21h2 for $WindowsArch""" } - if ($WindowsRelease -in 2016, 2019) { + if ($WindowsRelease -in 2016, 2019 -and $installationType -eq "Server") { $Name = """Cumulative update for Windows Server $WindowsRelease for $WindowsArch""" } + if ($WindowsRelease -in 2016, 2019, 2021 -and $WindowsSKU -like "*LTSC") { + $today = Get-Date + $firstDayOfMonth = Get-Date -Year $today.Year -Month $today.Month -Day 1 + $secondTuesday = $firstDayOfMonth.AddDays(((2 - [int]$firstDayOfMonth.DayOfWeek + 7) % 7) + 7) + $updateDate = if ($today -gt $secondTuesday) { $today } else { $today.AddMonths(-1) } + # More precise search to prevent Dynamic cumulative update from being chosen. + $Name = """$($updateDate.ToString('yyyy-MM')) Cumulative update for Windows 10 Version $WindowsVersion for $WindowsArch""" + } + if ($WindowsRelease -eq 2024 -and $WindowsSKU -like "*LTSC") { + $Name = """Cumulative update for Windows 11 Version $WindowsVersion for $WindowsArch""" + } #Check if $KBPath exists, if not, create it If (-not (Test-Path -Path $KBPath)) { WriteLog "Creating $KBPath" @@ -4520,6 +4572,13 @@ try { $SSUFilePath = "$KBPath\$SSUFile" WriteLog "Latest SSU saved to $SSUFilePath" } + if ($WindowsRelease -in 2016, 2019, 2021 -and $WindowsSKU -like "*LTSC") { + $SSUName = """Servicing Stack Update for Windows 10 Version $WindowsVersion for $WindowsArch""" + WriteLog "Searching for $SSUName from Microsoft Update Catalog and saving to $KBPath" + $SSUFile = Save-KB -Name $SSUName -Path $KBPath + $SSUFilePath = "$KBPath\$SSUFile" + WriteLog "Latest SSU saved to $SSUFilePath" + } WriteLog "Searching for $name from Microsoft Update Catalog and saving to $KBPath" $KBFilePath = Save-KB -Name $Name -Path $KBPath WriteLog "Latest CU saved to $KBPath\$KBFilePath" @@ -4527,7 +4586,7 @@ try { #Update Latest Preview Cumlative Update for Client OS only #will take Precendence over $UpdateLatestCU if both were set to $true - if ($UpdatePreviewCU -and $installationType -eq 'Client') { + if ($UpdatePreviewCU -and $installationType -eq 'Client' -and $WindowsSKU -notlike "*LTSC") { Writelog "`$UpdatePreviewCU is set to true, checking for latest Preview CU" $Name = """Cumulative update Preview for Windows $WindowsRelease Version $WindowsVersion for $WindowsArch""" #Check if $KBPath exists, if not, create it @@ -4543,8 +4602,8 @@ try { #Update Latest .NET Framework if ($UpdateLatestNet) { Writelog "`$UpdateLatestNet is set to true, checking for latest .NET Framework" - if ($installationType -eq 'Client') { - $Name = "Cumulative update for .net framework windows $WindowsRelease $WindowsVersion $WindowsArch -preview" + if ($WindowsRelease -in 10, 11) { + $Name = "Cumulative update for .NET framework windows $WindowsRelease $WindowsVersion $WindowsArch -preview" } if ($WindowsRelease -eq 2025) { $Name = """Cumulative Update for .NET Framework"" ""3.5 and 4.8.1"" for Windows 11 24H2 x64 -preview" @@ -4552,12 +4611,18 @@ try { if ($WindowsRelease -eq 2022) { $Name = """Cumulative Update for .NET Framework 3.5, 4.8 and 4.8.1"" ""operating system version 21H2 for x64""" } - if ($WindowsRelease -eq 2019) { + if ($WindowsRelease -eq 2019 -and $installationType -eq "Server") { $Name = """Cumulative Update for .NET Framework 3.5, 4.7.2 and 4.8 for Windows Server 2019 for x64""" } - if ($WindowsRelease -eq 2016) { + if ($WindowsRelease -eq 2016 -and $installationType -eq "Server") { $Name = """Cumulative Update for .NET Framework 4.8 for Windows Server 2016 for x64""" } + if ($WindowsRelease -in 2016, 2019, 2021 -and $WindowsSKU -like "*LTSC") { + $Name = "Cumulative update for .net framework windows 10 $WindowsVersion $WindowsArch" + } + if ($WindowsRelease -eq 2024) { + $Name = "Cumulative update for .NET framework windows 11 $WindowsVersion $WindowsArch" + } #Check if $KBPath exists, if not, create it If (-not (Test-Path -Path $KBPath)) { WriteLog "Creating $KBPath" @@ -4566,6 +4631,18 @@ try { WriteLog "Searching for $name from Microsoft Update Catalog and saving to $KBPath" $KBFilePath = Save-KB -Name $Name -Path $KBPath WriteLog "Latest .NET saved to $KBPath\$KBFilePath" + if ($WindowsRelease -eq 2021) { + WriteLog "Checking for latest .NET Framework feature pack for Windows $WindowsRelease $WindowsSKU" + $Name = """Microsoft .NET Framework 4.8.1 for Windows 10 Version 21H2 for x64""" + $KBFilePath = Save-KB -Name $Name -Path $KBPath + WriteLog "Latest .NET Framework feature pack saved to $KBPath\$KBFilePath" + } + if ($WindowsRelease -in 2016, 2019) { + WriteLog "Checking for latest .NET Framework feature pack for Windows $WindowsRelease $WindowsSKU" + $Name = """Microsoft .NET Framework 4.8 for Windows 10 Version $WindowsVersion and Windows Server $WindowsRelease for x64""" + $KBFilePath = Save-KB -Name $Name -Path $KBPath + WriteLog "Latest .NET Framework feature pack saved to $KBPath\$KBFilePath" + } } #Search for cached VHDX and skip VHDX creation if there's a cached version @@ -4672,7 +4749,7 @@ try { WriteLog "Adding KBs to $WindowsPartition" WriteLog 'This can take 10+ minutes depending on how old the media is and the size of the KB. Please be patient' # If WindowsRelease is 2016, we need to add the SSU first - if ($WindowsRelease -eq 2016) { + if ($WindowsRelease -eq 2016 -and $installationType -eq "Server") { WriteLog 'WindowsRelease is 2016, adding SSU first' WriteLog "Adding SSU to $WindowsPartition" # Add-WindowsPackage -Path $WindowsPartition -PackagePath $SSUFilePath -PreventPending | Out-Null @@ -4683,8 +4760,17 @@ try { WriteLog "Removing $SSUFilePath" Remove-Item -Path $SSUFilePath -Force | Out-Null WriteLog 'SSU removed' - WriteLog "Adding CU to $WindowsPartition" } + if ($WindowsRelease -in 2016, 2019, 2021 -and $WindowsSKU -like "*LTSC") { + WriteLog "WindowsRelease is $WindowsRelease and is $WindowsSKU, adding SSU first" + WriteLog "Adding SSU to $WindowsPartition" + Add-WindowsPackage -Path $WindowsPartition -PackagePath $SSUFilePath | Out-Null + WriteLog "SSU added to $WindowsPartition" + WriteLog "Removing $SSUFilePath" + Remove-Item -Path $SSUFilePath -Force | Out-Null + WriteLog 'SSU removed' + } + WriteLog "Adding Cumulative Updates to $WindowsPartition" # Add-WindowsPackage -Path $WindowsPartition -PackagePath $KBPath -PreventPending | Out-Null Add-WindowsPackage -Path $WindowsPartition -PackagePath $KBPath | Out-Null WriteLog "KBs added to $WindowsPartition" @@ -4701,6 +4787,7 @@ try { WriteLog 'Clean Up the WinSxS Folder' WriteLog 'This can take 10+ minutes depending on how old the media is and the size of the KB. Please be patient' Dism /Image:$WindowsPartition /Cleanup-Image /StartComponentCleanup /ResetBase | Out-Null + # Repair-WindowsImage -Path $WindowsPartition -StartComponentCleanup -ResetBase | Out-Null WriteLog 'Clean Up the WinSxS Folder completed' } catch { Write-Host "Adding KB to VHDX failed with error $_" diff --git a/FFUDevelopment/WinPECaptureFFUFiles/CaptureFFU.ps1 b/FFUDevelopment/WinPECaptureFFUFiles/CaptureFFU.ps1 index b51ba5a..08be73a 100644 --- a/FFUDevelopment/WinPECaptureFFUFiles/CaptureFFU.ps1 +++ b/FFUDevelopment/WinPECaptureFFUFiles/CaptureFFU.ps1 @@ -27,7 +27,9 @@ $SKU = switch ($SKU) { ProfessionalEducation { 'Pro_Edu' } ProfessionalEducationN { 'Pro_EduN' } Enterprise { 'Ent' } - EnterpriseN { 'EntN' } + EnterpriseN { 'EntN'} + EnterpriseS { 'Ent_LTSC' } + IoTEnterpriseS { 'IoT_Ent_LTSC' } Education { 'Edu' } EducationN { 'EduN' } ProfessionalWorkstation { 'Pro_Wks' } From e25a890946dcbd7ab093b2e49ecaff09490f91ae Mon Sep 17 00:00:00 2001 From: Zehadi Alam <63765084+zehadialam@users.noreply.github.com> Date: Sat, 28 Dec 2024 20:30:03 -0500 Subject: [PATCH 02/11] Remove extra lines --- FFUDevelopment/BuildFFUVM.ps1 | 2 -- 1 file changed, 2 deletions(-) diff --git a/FFUDevelopment/BuildFFUVM.ps1 b/FFUDevelopment/BuildFFUVM.ps1 index d5f862e..ed7af00 100644 --- a/FFUDevelopment/BuildFFUVM.ps1 +++ b/FFUDevelopment/BuildFFUVM.ps1 @@ -228,9 +228,7 @@ param( [string]$WindowsSKU = 'Pro', [ValidateScript({ Test-Path $_ })] [string]$FFUDevelopmentPath = $PSScriptRoot, - [bool]$InstallApps, - [hashtable]$AppsScriptVariables, [bool]$InstallOffice, [ValidateSet('Microsoft', 'Dell', 'HP', 'Lenovo')] From 77ef15494163f517a60587824224c71415f39007 Mon Sep 17 00:00:00 2001 From: JonasKloseBW Date: Mon, 13 Jan 2025 12:51:32 +0100 Subject: [PATCH 03/11] Add $AppListPath parameter - Add $AppListPath parameter - Set default value to "$AppsPath\AppList.json" - Modify Get-Apps call to use the $AppListPath parameter --- FFUDevelopment/BuildFFUVM.ps1 | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/FFUDevelopment/BuildFFUVM.ps1 b/FFUDevelopment/BuildFFUVM.ps1 index d788a05..352d315 100644 --- a/FFUDevelopment/BuildFFUVM.ps1 +++ b/FFUDevelopment/BuildFFUVM.ps1 @@ -230,6 +230,7 @@ param( [string]$FFUDevelopmentPath = $PSScriptRoot, [bool]$InstallApps, + [string]$AppListPath, [hashtable]$AppsScriptVariables, [bool]$InstallOffice, @@ -471,6 +472,7 @@ if ($installationType -eq 'Server'){ 2025 { $WindowsVersion = '24H2' } } } +if (-not $AppListPath) { $AppListPath = "$AppsPath\AppList.json" } #FUNCTIONS function WriteLog($LogText) { @@ -4307,9 +4309,9 @@ if ($InstallApps) { exit } WriteLog "$AppsPath\InstallAppsandSysprep.cmd found" - If (Test-Path -Path "$AppsPath\AppList.json"){ - WriteLog "$AppsPath\AppList.json found, checking for winget apps to install" - Get-Apps -AppList "$AppsPath\AppList.json" + If (Test-Path -Path $AppListPath){ + WriteLog "$AppListPath found, checking for winget apps to install" + Get-Apps -AppList "$AppListPath" } if (-not $InstallOffice) { From 7c9f24f6958d4f149be646cd60704af1ca52147b Mon Sep 17 00:00:00 2001 From: rbalsleyMSFT <53497092+rbalsleyMSFT@users.noreply.github.com> Date: Thu, 16 Jan 2025 18:00:08 -0800 Subject: [PATCH 04/11] Fixed issues - Fixed an issue where if AppsScriptVariables was configured in a config file, the hashtable wasn't being created by the script when setting the variable. - Fixed a crash where shortening the Windows SKU was creating duplicate shortened names for certain SKUs. --- FFUDevelopment/BuildFFUVM.ps1 | 36 +++++++++---------- .../WinPEDeployFFUFiles/ApplyFFU.ps1 | 2 +- 2 files changed, 18 insertions(+), 20 deletions(-) diff --git a/FFUDevelopment/BuildFFUVM.ps1 b/FFUDevelopment/BuildFFUVM.ps1 index 352d315..fe2107c 100644 --- a/FFUDevelopment/BuildFFUVM.ps1 +++ b/FFUDevelopment/BuildFFUVM.ps1 @@ -384,12 +384,12 @@ if ($ConfigFile -and (Test-Path -Path $ConfigFile)) { } # If this is the Headers parameter, convert PSCustomObject to hashtable - if ($key -eq 'Headers' -and $value -is [System.Management.Automation.PSCustomObject]) { - $headers = [hashtable]::new() + if ((($key -eq 'Headers') -or ($key -eq 'AppsScriptVariables')) -and ($value -is [System.Management.Automation.PSCustomObject])) { + $hashtableValue = [hashtable]::new() foreach ($prop in $value.psobject.Properties) { - $headers[$prop.Name] = $prop.Value + $hashtableValue[$prop.Name] = $prop.Value } - $value = $headers + $value = $hashtableValue } # Check if this key matches a parameter in the script @@ -3135,36 +3135,34 @@ function Get-ShortenedWindowsSKU { ) $shortenedWindowsSKU = switch ($WindowsSKU) { 'Core' { 'Home' } - 'CoreN' { 'Home_N' } - 'CoreSingleLanguage' { 'Home_SL' } - 'Education' { 'Edu' } - 'EducationN' { 'Edu_N' } - 'Professional' { 'Pro' } - 'ProfessionalN' { 'Pro_N' } - 'ProfessionalEducation' { 'Pro_Edu' } - 'ProfessionalEducationN' { 'Pro_Edu_N' } - 'ProfessionalWorkstation' { 'Pro_WKS' } - 'ProfessionalWorkstationN' { 'Pro_WKS_N' } - 'Enterprise' { 'Ent' } - 'EnterpriseN' { 'Ent_N' } - 'ServerStandard' { 'Srv_Std' } - 'ServerDatacenter' { 'Srv_Dtc' } 'Home' { 'Home' } + 'CoreN' { 'Home_N' } 'Home N' { 'Home_N' } + 'CoreSingleLanguage' { 'Home_SL' } 'Home Single Language' { 'Home_SL' } 'Education' { 'Edu' } + 'EducationN' { 'Edu_N' } 'Education N' { 'Edu_N' } 'Professional' { 'Pro' } + 'Pro' { 'Pro' } + 'ProfessionalN' { 'Pro_N' } 'Pro N' { 'Pro_N' } + 'ProfessionalEducation' { 'Pro_Edu' } 'Pro Education' { 'Pro_Edu' } + 'ProfessionalEducationN' { 'Pro_Edu_N' } 'Pro Education N' { 'Pro_Edu_N' } + 'ProfessionalWorkstation' { 'Pro_WKS' } 'Pro for Workstations' { 'Pro_WKS' } + 'ProfessionalWorkstationN' { 'Pro_WKS_N' } 'Pro N for Workstations' { 'Pro_WKS_N' } 'Enterprise' { 'Ent' } + 'EnterpriseN' { 'Ent_N' } 'Enterprise N' { 'Ent_N' } + 'ServerStandard' { 'Srv_Std' } 'Standard' { 'Srv_Std' } - 'Standard (Desktop Experience)' { 'Srv_Std_DE' } + 'ServerDatacenter' { 'Srv_Dtc' } 'Datacenter' { 'Srv_Dtc' } + 'Standard (Desktop Experience)' { 'Srv_Std_DE' } 'Datacenter (Desktop Experience)' { 'Srv_Dtc_DE' } } return $shortenedWindowsSKU diff --git a/FFUDevelopment/WinPEDeployFFUFiles/ApplyFFU.ps1 b/FFUDevelopment/WinPEDeployFFUFiles/ApplyFFU.ps1 index 74746a7..99b701a 100644 --- a/FFUDevelopment/WinPEDeployFFUFiles/ApplyFFU.ps1 +++ b/FFUDevelopment/WinPEDeployFFUFiles/ApplyFFU.ps1 @@ -135,7 +135,7 @@ $LogFileName = 'ScriptLog.txt' $USBDrive = Get-USBDrive New-item -Path $USBDrive -Name $LogFileName -ItemType "file" -Force | Out-Null $LogFile = $USBDrive + $LogFilename -$version = '2412.3' +$version = '2412.4' WriteLog 'Begin Logging' WriteLog "Script version: $version" From 0606a1278c1876eec2dad6fbea6f64a7229a71c9 Mon Sep 17 00:00:00 2001 From: JonasKloseBW Date: Thu, 27 Feb 2025 18:37:29 +0100 Subject: [PATCH 05/11] Update ApplyFFU.ps1 - Allows setting the computer name with a predefined list (SerialComputerNames.csv) of serial numbers and matching computer names - Defaults to FFU-{Random} if no matching serial number is found in list so FFU deployment can continue without user input --- .../WinPEDeployFFUFiles/ApplyFFU.ps1 | 27 ++++++++++++++++++- 1 file changed, 26 insertions(+), 1 deletion(-) diff --git a/FFUDevelopment/WinPEDeployFFUFiles/ApplyFFU.ps1 b/FFUDevelopment/WinPEDeployFFUFiles/ApplyFFU.ps1 index 99b701a..0d7b7b7 100644 --- a/FFUDevelopment/WinPEDeployFFUFiles/ApplyFFU.ps1 +++ b/FFUDevelopment/WinPEDeployFFUFiles/ApplyFFU.ps1 @@ -222,6 +222,7 @@ if (Test-Path -Path $PPKGFolder){ $UnattendFolder = $USBDrive + "unattend\" $UnattendFilePath = $UnattendFolder + "unattend.xml" $UnattendPrefixPath = $UnattendFolder + "prefixes.txt" +$UnattendComputerNamePath = $UnattendFolder + "SerialComputerNames.csv" If (Test-Path -Path $UnattendFilePath){ $UnattendFile = Get-ChildItem -Path $UnattendFilePath If ($UnattendFile){ @@ -234,6 +235,12 @@ If (Test-Path -Path $UnattendPrefixPath){ $UnattendPrefix = $true } } +If (Test-Path -Path $UnattendComputerNamePath){ + $UnattendComputerNameFile = Get-ChildItem -Path $UnattendComputerNamePath + If ($UnattendComputerNameFile){ + $UnattendComputerName = $true + } +} #Ask for device name if unattend exists if ($Unattend -and $UnattendPrefix){ @@ -278,7 +285,25 @@ if ($Unattend -and $UnattendPrefix){ $computername = Set-Computername($computername) Writelog "Computer name set to $computername" } -elseif($Unattend){ +elseif($Unattend -and $UnattendComputerName){ + Writelog 'Unattend file found with SerialComputerNames.csv. Getting name for current computer.' + $SerialComputerNames = Import-Csv -Path $UnattendComputerNameFile.FullName -Delimiter "," + + $SerialNumber = (Get-CimInstance -Class Win32_Bios).SerialNumber + $SCName = $SerialComputerNames | Where-Object { $_.SerialNumber -eq $SerialNumber } + + If ($SCName) { + [string]$computername = $SCName.ComputerName + $computername = Set-Computername($computername) + Writelog "Computer name set to $computername" + } else { + Writelog 'No matching serial number found in SerialComputerNames.csv. Setting random computer name to complete setup.' + [string]$computername = ("FFU-" + (-join ((48..57) + (65..90) + (97..122) | Get-Random -Count 11 | ForEach-Object { [char]$_ }))) + $computername = Set-Computername($computername) + Writelog "Computer name set to $computername" + } +} +elseif($Unattend) { Writelog 'Unattend file found with no prefixes.txt, asking for name' [string]$computername = Read-Host 'Enter device name' Set-Computername($computername) From 0d1d3a1ed549ec232f8a44b6c7d105f68b90cb18 Mon Sep 17 00:00:00 2001 From: rbalsleyMSFT Date: Wed, 14 May 2025 19:59:04 -0700 Subject: [PATCH 06/11] Fix issue with checkpoint CUs and May 2025-05B CU. Should future proof new checkpoint CUs in the future. --- FFUDevelopment/BuildFFUVM.ps1 | 88 +++++++++++++++++++++++++++++------ 1 file changed, 74 insertions(+), 14 deletions(-) diff --git a/FFUDevelopment/BuildFFUVM.ps1 b/FFUDevelopment/BuildFFUVM.ps1 index fe2107c..c73a46f 100644 --- a/FFUDevelopment/BuildFFUVM.ps1 +++ b/FFUDevelopment/BuildFFUVM.ps1 @@ -2431,23 +2431,27 @@ function Get-KBLink { $VerbosePreference = 'SilentlyContinue' $results = Invoke-WebRequest -Uri "http://www.catalog.update.microsoft.com/Search.aspx?q=$Name" -Headers $Headers -UserAgent $UserAgent $VerbosePreference = $OriginalVerbosePreference + + # Extract the first KB article ID from the HTML content and store it globally + if ($results.Content -match '>\s*([^\(<]+)\(KB(\d+)\)\s*<') { + $kbArticleID = "KB$($matches[2])" + $global:LastKBArticleID = $kbArticleID + WriteLog "Found KB article ID: $kbArticleID" + } + else { + WriteLog "No KB article ID found in search results." + $global:LastKBArticleID = $null + } + $kbids = $results.InputFields | Where-Object { $_.type -eq 'Button' -and $_.Value -eq 'Download' } | Select-Object -ExpandProperty ID - # Write-Verbose -Message "$kbids" - if (-not $kbids) { Write-Warning -Message "No results found for $Name" return } - # $guids = $results.Links | - # Where-Object ID -match '_link' | - # Where-Object { $_.OuterHTML -match ( "(?=.*" + ( $Filter -join ")(?=.*" ) + ")" ) } | - # ForEach-Object { $_.id.replace('_link', '') } | - # Where-Object { $_ -in $kbids } - $guids = $results.Links | Where-Object ID -match '_link' | Where-Object { $_.OuterHTML -match ( "(?=.*" + ( $Filter -join ")(?=.*" ) + ")" ) } | @@ -4568,8 +4572,27 @@ try { WriteLog "Latest SSU saved to $SSUFilePath" } WriteLog "Searching for $name from Microsoft Update Catalog and saving to $KBPath" - $KBFilePath = Save-KB -Name $Name -Path $KBPath - WriteLog "Latest CU saved to $KBPath\$KBFilePath" + $CUFileName = Save-KB -Name $Name -Path $KBPath + # Check if $CUFileName contains the string in $global:LastKBArticleID + # If it does not, look in $KBPath for the file that contains the string in $global:LastKBArticleID + # and set that as the $CUFileName + # This is because checkpoint CUs download indeterministically + WriteLog "Checking if $CUFileName contains $global:LastKBArticleID" + if ($CUFileName -notmatch $global:LastKBArticleID) { + WriteLog "$CUFileName does not contain $global:LastKBArticleID, searching for file that contains it" + $CUFileName = $null + # Get the file that contains the string in $global:LastKBArticleID + $CUFileName = (Get-ChildItem -Path $KBPath -Filter "*$global:LastKBArticleID*" | Select-Object -First 1).Name + if ($null -ne $CUFileName) { + WriteLog "Found $CUFileName" + } + else { + WriteLog "Could not find file that contains $global:LastKBArticleID" + throw "Could not find file that contains $global:LastKBArticleID" + } + } + $CUPath = "$KBPath\$CUFileName" + WriteLog "Latest CU saved to $CUPath" } #Update Latest Preview Cumlative Update for Client OS only @@ -4583,8 +4606,27 @@ try { New-Item -Path $KBPath -ItemType Directory -Force | Out-Null } WriteLog "Searching for $name from Microsoft Update Catalog and saving to $KBPath" - $KBFilePath = Save-KB -Name $Name -Path $KBPath - WriteLog "Latest Preview CU saved to $KBPath\$KBFilePath" + $CUPFileName = Save-KB -Name $Name -Path $KBPath + # Check if $CUPFileName contains the string in $global:LastKBArticleID + # If it does not, look in $KBPath for the file that contains the string in $global:LastKBArticleID + # and set that as the $CUPFileName + # This is because checkpoint CUs download indeterministically + WriteLog "Checking if $CUPFileName contains $global:LastKBArticleID" + if ($CUPFileName -notmatch $global:LastKBArticleID) { + WriteLog "$CUPFileName does not contain $global:LastKBArticleID, searching for file that contains it" + $CUPFileName = $null + # Get the file that contains the string in $global:LastKBArticleID + $CUPFileName = (Get-ChildItem -Path $KBPath -Filter "*$global:LastKBArticleID*" | Select-Object -First 1).Name + if ($null -ne $CUPFileName) { + WriteLog "Found $CUPFileName" + } + else { + WriteLog "Could not find file that contains $global:LastKBArticleID" + throw "Could not find file that contains $global:LastKBArticleID" + } + } + $CUPPath = "$KBPath\$CUPFileName" + WriteLog "Latest CU saved to $CUPPath" } #Update Latest .NET Framework @@ -4611,8 +4653,26 @@ try { New-Item -Path $KBPath -ItemType Directory -Force | Out-Null } WriteLog "Searching for $name from Microsoft Update Catalog and saving to $KBPath" - $KBFilePath = Save-KB -Name $Name -Path $KBPath - WriteLog "Latest .NET saved to $KBPath\$KBFilePath" + $NETFileName = Save-KB -Name $Name -Path $KBPath + # Check if $NETFileName contains the string in $global:LastKBArticleID + # If it does not, look in $KBPath for the file that contains the string in $global:LastKBArticleID + # and set that as the $NETFileName + WriteLog "Checking if $NETFileName contains $global:LastKBArticleID" + if ($NETFileName -notmatch $global:LastKBArticleID) { + WriteLog "$NETFileName does not contain $global:LastKBArticleID, searching for file that contains it" + $NETFileName = $null + # Get the file that contains the string in $global:LastKBArticleID + $NETFileName = (Get-ChildItem -Path $KBPath -Filter "*$global:LastKBArticleID*" | Select-Object -First 1).Name + if ($null -ne $NETFileName) { + WriteLog "Found $NETFileName" + } + else { + WriteLog "Could not find file that contains $global:LastKBArticleID" + throw "Could not find file that contains $global:LastKBArticleID" + } + } + $NETPath = "$KBPath\$NETFileName" + WriteLog "Latest CU saved to $NETPath" } #Search for cached VHDX and skip VHDX creation if there's a cached version From 9cac674d7be4ae85a8555f5da70b3bc7610e8104 Mon Sep 17 00:00:00 2001 From: rbalsleyMSFT Date: Thu, 15 May 2025 08:38:01 -0700 Subject: [PATCH 07/11] Marged in changes to Add-WindowsPackage that were missing --- FFUDevelopment/BuildFFUVM.ps1 | 23 +++++++++++++++++++---- 1 file changed, 19 insertions(+), 4 deletions(-) diff --git a/FFUDevelopment/BuildFFUVM.ps1 b/FFUDevelopment/BuildFFUVM.ps1 index c73a46f..3e1d1c6 100644 --- a/FFUDevelopment/BuildFFUVM.ps1 +++ b/FFUDevelopment/BuildFFUVM.ps1 @@ -4626,7 +4626,7 @@ try { } } $CUPPath = "$KBPath\$CUPFileName" - WriteLog "Latest CU saved to $CUPPath" + WriteLog "Latest CU Preview saved to $CUPPath" } #Update Latest .NET Framework @@ -4672,7 +4672,7 @@ try { } } $NETPath = "$KBPath\$NETFileName" - WriteLog "Latest CU saved to $NETPath" + WriteLog "Latest .NET Framework saved to $NETPath" } #Search for cached VHDX and skip VHDX creation if there's a cached version @@ -4792,8 +4792,23 @@ try { WriteLog 'SSU removed' WriteLog "Adding CU to $WindowsPartition" } - # Add-WindowsPackage -Path $WindowsPartition -PackagePath $KBPath -PreventPending | Out-Null - Add-WindowsPackage -Path $WindowsPartition -PackagePath $KBPath | Out-Null + + # Break out CU and NET updates to be added separately to abide by Checkpoint Update recommendations + if ($UpdateLatestCU) { + WriteLog "Adding $CUPath to $WindowsPartition" + Add-WindowsPackage -Path $WindowsPartition -PackagePath $CUPath | Out-Null + WriteLog "$CUPath added to $WindowsPartition" + } + if ($UpdatePreviewCU) { + WriteLog "Adding $CUPPath to $WindowsPartition" + Add-WindowsPackage -Path $WindowsPartition -PackagePath $CUPPath | Out-Null + WriteLog "$CUPPath added to $WindowsPartition" + } + if ($UpdateLatestNet) { + WriteLog "Adding $NETPath to $WindowsPartition" + Add-WindowsPackage -Path $WindowsPartition -PackagePath $NETPath | Out-Null + WriteLog "$NETPath added to $WindowsPartition" + } WriteLog "KBs added to $WindowsPartition" if ($AllowVHDXCaching) { $cachedVHDXInfo = [VhdxCacheItem]::new() From c32a09bfc12031950d89262a2f40b68ce3055452 Mon Sep 17 00:00:00 2001 From: Zehadi Alam <63765084+zehadialam@users.noreply.github.com> Date: Fri, 16 May 2025 00:11:50 -0400 Subject: [PATCH 08/11] Add .NET update support for Windows LTSC --- FFUDevelopment/BuildFFUVM.ps1 | 74 +++++++++++++++++++++-------------- 1 file changed, 45 insertions(+), 29 deletions(-) diff --git a/FFUDevelopment/BuildFFUVM.ps1 b/FFUDevelopment/BuildFFUVM.ps1 index 8ed105e..c422a8e 100644 --- a/FFUDevelopment/BuildFFUVM.ps1 +++ b/FFUDevelopment/BuildFFUVM.ps1 @@ -4701,6 +4701,23 @@ try { #Update Latest .NET Framework if ($UpdateLatestNet) { Writelog "`$UpdateLatestNet is set to true, checking for latest .NET Framework" + #Check if $KBPath exists, if not, create it + if (-not (Test-Path -Path $KBPath)) { + WriteLog "Creating $KBPath" + New-Item -Path $KBPath -ItemType Directory -Force | Out-Null + } + $NETPath = Join-Path -Path $KBPath -ChildPath "NET" + if (-not (Test-Path -Path $NETPath)) { + WriteLog "Creating $NETPath" + New-Item -Path $NETPath -ItemType Directory -Force | Out-Null + } + if ($WindowsRelease -in 2016, 2019, 2021 -and $WindowsSKU -like "*LTSC") { + $SSUName = """Servicing Stack Update for Windows 10 Version $WindowsVersion for $WindowsArch""" + WriteLog "Searching for $SSUName from Microsoft Update Catalog and saving to $KBPath" + $SSUFile = Save-KB -Name $SSUName -Path $KBPath + $SSUFilePath = "$KBPath\$SSUFile" + WriteLog "Latest SSU saved to $SSUFilePath" + } if ($WindowsRelease -in 10, 11) { $Name = "Cumulative update for .NET framework windows $WindowsRelease $WindowsVersion $WindowsArch -preview" } @@ -4718,48 +4735,47 @@ try { } if ($WindowsRelease -in 2016, 2019, 2021 -and $WindowsSKU -like "*LTSC") { $Name = "Cumulative update for .net framework windows 10 $WindowsVersion $WindowsArch" + $NETFileName = Save-KB -Name $Name -Path $NETPath + WriteLog "Latest .NET Framework cumulative update saved to $NETPath\$NETFileName" } if ($WindowsRelease -eq 2024) { $Name = "Cumulative update for .NET framework windows 11 $WindowsVersion $WindowsArch" } - #Check if $KBPath exists, if not, create it - If (-not (Test-Path -Path $KBPath)) { - WriteLog "Creating $KBPath" - New-Item -Path $KBPath -ItemType Directory -Force | Out-Null - } WriteLog "Searching for $name from Microsoft Update Catalog and saving to $KBPath" if ($WindowsRelease -eq 2021) { WriteLog "Checking for latest .NET Framework feature pack for Windows $WindowsRelease $WindowsSKU" - $Name = """Microsoft .NET Framework 4.8.1 for Windows 10 Version 21H2 for x64""" - $KBFilePath = Save-KB -Name $Name -Path $KBPath - WriteLog "Latest .NET Framework feature pack saved to $KBPath\$KBFilePath" + $NETFeatureName = """Microsoft .NET Framework 4.8.1 for Windows 10 Version 21H2 for x64""" + $NETFeaturePackFile = Save-KB -Name $NETFeatureName -Path $NETPath + WriteLog "Latest .NET Framework Feature pack saved to $NETPath\$NETFeaturePackFile" } if ($WindowsRelease -in 2016, 2019) { WriteLog "Checking for latest .NET Framework feature pack for Windows $WindowsRelease $WindowsSKU" - $Name = """Microsoft .NET Framework 4.8 for Windows 10 Version $WindowsVersion and Windows Server $WindowsRelease for x64""" - $KBFilePath = Save-KB -Name $Name -Path $KBPath - WriteLog "Latest .NET Framework feature pack saved to $KBPath\$KBFilePath" + $NETFeatureName = """Microsoft .NET Framework 4.8 for Windows 10 Version $WindowsVersion and Windows Server $WindowsRelease for x64""" + $NETFeaturePackFile = Save-KB -Name $NETFeatureName -Path $NETPath + WriteLog "Latest .NET Framework Feature pack saved to $NETPath\$NETFeaturePackFile" } - $NETFileName = Save-KB -Name $Name -Path $KBPath - # Check if $NETFileName contains the string in $global:LastKBArticleID - # If it does not, look in $KBPath for the file that contains the string in $global:LastKBArticleID - # and set that as the $NETFileName - WriteLog "Checking if $NETFileName contains $global:LastKBArticleID" - if ($NETFileName -notmatch $global:LastKBArticleID) { - WriteLog "$NETFileName does not contain $global:LastKBArticleID, searching for file that contains it" - $NETFileName = $null - # Get the file that contains the string in $global:LastKBArticleID - $NETFileName = (Get-ChildItem -Path $KBPath -Filter "*$global:LastKBArticleID*" | Select-Object -First 1).Name - if ($null -ne $NETFileName) { - WriteLog "Found $NETFileName" - } - else { - WriteLog "Could not find file that contains $global:LastKBArticleID" - throw "Could not find file that contains $global:LastKBArticleID" + if (-not ($WindowsRelease -in 2016, 2019, 2021 -and $WindowsSKU -like "*LTSC")) { + $NETFileName = Save-KB -Name $Name -Path $KBPath + # Check if $NETFileName contains the string in $global:LastKBArticleID + # If it does not, look in $KBPath for the file that contains the string in $global:LastKBArticleID + # and set that as the $NETFileName + WriteLog "Checking if $NETFileName contains $global:LastKBArticleID" + if ($NETFileName -notmatch $global:LastKBArticleID) { + WriteLog "$NETFileName does not contain $global:LastKBArticleID, searching for file that contains it" + $NETFileName = $null + # Get the file that contains the string in $global:LastKBArticleID + $NETFileName = (Get-ChildItem -Path $KBPath -Filter "*$global:LastKBArticleID*" | Select-Object -First 1).Name + if ($null -ne $NETFileName) { + WriteLog "Found $NETFileName" + } + else { + WriteLog "Could not find file that contains $global:LastKBArticleID" + throw "Could not find file that contains $global:LastKBArticleID" + } } + $NETPath = "$KBPath\$NETFileName" + WriteLog "Latest .NET Framework saved to $NETPath" } - $NETPath = "$KBPath\$NETFileName" - WriteLog "Latest .NET Framework saved to $NETPath" } #Search for cached VHDX and skip VHDX creation if there's a cached version From 2659336ee9a79002db698a9538682a7a4b88b96c Mon Sep 17 00:00:00 2001 From: Zehadi Alam <63765084+zehadialam@users.noreply.github.com> Date: Fri, 16 May 2025 00:26:06 -0400 Subject: [PATCH 09/11] Move .NET folder creation code --- FFUDevelopment/BuildFFUVM.ps1 | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/FFUDevelopment/BuildFFUVM.ps1 b/FFUDevelopment/BuildFFUVM.ps1 index c422a8e..ee2bbca 100644 --- a/FFUDevelopment/BuildFFUVM.ps1 +++ b/FFUDevelopment/BuildFFUVM.ps1 @@ -4706,11 +4706,6 @@ try { WriteLog "Creating $KBPath" New-Item -Path $KBPath -ItemType Directory -Force | Out-Null } - $NETPath = Join-Path -Path $KBPath -ChildPath "NET" - if (-not (Test-Path -Path $NETPath)) { - WriteLog "Creating $NETPath" - New-Item -Path $NETPath -ItemType Directory -Force | Out-Null - } if ($WindowsRelease -in 2016, 2019, 2021 -and $WindowsSKU -like "*LTSC") { $SSUName = """Servicing Stack Update for Windows 10 Version $WindowsVersion for $WindowsArch""" WriteLog "Searching for $SSUName from Microsoft Update Catalog and saving to $KBPath" @@ -4735,6 +4730,11 @@ try { } if ($WindowsRelease -in 2016, 2019, 2021 -and $WindowsSKU -like "*LTSC") { $Name = "Cumulative update for .net framework windows 10 $WindowsVersion $WindowsArch" + $NETPath = Join-Path -Path $KBPath -ChildPath "NET" + if (-not (Test-Path -Path $NETPath)) { + WriteLog "Creating $NETPath" + New-Item -Path $NETPath -ItemType Directory -Force | Out-Null + } $NETFileName = Save-KB -Name $Name -Path $NETPath WriteLog "Latest .NET Framework cumulative update saved to $NETPath\$NETFileName" } From b530ac5a5c8c51015e5d52eeab3ce445a00dab58 Mon Sep 17 00:00:00 2001 From: Zehadi Alam <63765084+zehadialam@users.noreply.github.com> Date: Sun, 18 May 2025 22:06:40 -0400 Subject: [PATCH 10/11] Added comment for .NET updates and added condition in CaptureFFU.ps1 to fix naming for LTSC 2019 --- FFUDevelopment/BuildFFUVM.ps1 | 2 ++ FFUDevelopment/WinPECaptureFFUFiles/CaptureFFU.ps1 | 4 ++-- 2 files changed, 4 insertions(+), 2 deletions(-) diff --git a/FFUDevelopment/BuildFFUVM.ps1 b/FFUDevelopment/BuildFFUVM.ps1 index ee2bbca..f5fda1c 100644 --- a/FFUDevelopment/BuildFFUVM.ps1 +++ b/FFUDevelopment/BuildFFUVM.ps1 @@ -4731,6 +4731,8 @@ try { if ($WindowsRelease -in 2016, 2019, 2021 -and $WindowsSKU -like "*LTSC") { $Name = "Cumulative update for .net framework windows 10 $WindowsVersion $WindowsArch" $NETPath = Join-Path -Path $KBPath -ChildPath "NET" + # These LTSC editions include multiple .NET updates, so a separate directory is created and specified to allow DISM + # to install them all, instead of specifying each .NET update individually. if (-not (Test-Path -Path $NETPath)) { WriteLog "Creating $NETPath" New-Item -Path $NETPath -ItemType Directory -Force | Out-Null diff --git a/FFUDevelopment/WinPECaptureFFUFiles/CaptureFFU.ps1 b/FFUDevelopment/WinPECaptureFFUFiles/CaptureFFU.ps1 index 08be73a..2f77e74 100644 --- a/FFUDevelopment/WinPECaptureFFUFiles/CaptureFFU.ps1 +++ b/FFUDevelopment/WinPECaptureFFUFiles/CaptureFFU.ps1 @@ -12,10 +12,10 @@ reg load "HKLM\FFU" $Software $SKU = Get-ItemPropertyValue -Path 'HKLM:\FFU\Microsoft\Windows NT\CurrentVersion\' -Name 'EditionID' [int]$CurrentBuild = Get-ItemPropertyValue -Path 'HKLM:\FFU\Microsoft\Windows NT\CurrentVersion\' -Name 'CurrentBuild' -if ($CurrentBuild -notin 14393, 17763) { +$InstallationType = Get-ItemPropertyValue -Path 'HKLM:\FFU\Microsoft\Windows NT\CurrentVersion\' -Name 'InstallationType' +if ($CurrentBuild -notin 14393, 17763 -and $InstallationType -ne "Server") { $WindowsVersion = Get-ItemPropertyValue -Path 'HKLM:\FFU\Microsoft\Windows NT\CurrentVersion\' -Name 'DisplayVersion' } -$InstallationType = Get-ItemPropertyValue -Path 'HKLM:\FFU\Microsoft\Windows NT\CurrentVersion\' -Name 'InstallationType' $BuildDate = Get-Date -uformat %b%Y $SKU = switch ($SKU) { From d5a4f964823dd674e050f7243e6806912dafafec Mon Sep 17 00:00:00 2001 From: rbalsleyMSFT Date: Wed, 21 May 2025 16:44:21 -0700 Subject: [PATCH 11/11] Enhance FFU Development Scripts and Configuration BuildFFUVM.ps1 - Added parameter definitions that were missing: - AppListPath - Path to a JSON file containing a list of applications to install using WinGet. Default is $FFUDevelopmentPath\Apps\AppList.json. - PEDriversFolder - Path to the folder containing drivers to be injected into the WinPE deployment media. Default is $FFUDevelopmentPath\PEDrivers. - Added two new parameters: - UpdateLatestMicrocode - This is used for Windows 10/Server. When set to $true, will download and install the latest microcode updates for applicable Windows releases (e.g., Windows Server 2016/2019, Windows 10 LTSC 2016/2019) into the FFU. Default is $false. - UpdateADK - Added for airgapped scenarios where you've manually updated the ADK and don't need it to continually check. When set to $true, the script will check for and install the latest Windows ADK and WinPE add-on if they are not already installed or up-to-date. Default is $true. - Reorganized the WindowsSKU validateset to make it easier to read and added in 2016 LTSB releases - Changed version to 2505.1 - Reorganized the releasetoMapping SKUs to make it easier to read - Omitted Defender/Edge from reporting KB ID since neither includes it - Updated Save-KB with some enhancements from the UI branch which will handle KBs that don't have an architecture defined in their file name that will leverage a new function Get-PEArchitecture that can interrogate the file name and determine the correct architecture - Updated Get-ShortenedWindowsSKU with LTSB/LTSC SKUs - Updated New-FFUFileName to use $winverinfo.Name for $WindowsRelease for client OSes to which will set $WindowsRelease to using Win10 or Win11. This fixes a bug where you might see 10 or 11 instead of Win10 or Win11 for FFU builds that use only the VHDX (e.g. `-InstallApps $false`. This keeps the naming consistent with FFUs built via VM. - Updated Get-WindowsVersionInfo to fix an issue with naming LTSC 2019 - Added Get-PEArchitecture function - Commented out the Windows Security Platform Update code since the URL is dead for the content. This is fixed in the UI branch and will be reintroduced in Dev and Main at a later date when the UI work is complete. - Created a new variable `$isLTSC` - Modified and reorganized the search strings for the various .net framework components. LTSC introduced some complexity with handling the various .net releases. - VHDXCaching will now recurse the KBPath folder when finding downloaded KBs to include in its config file Sample_default.json - Added new/missing parameters - ApplistPath - UpdateADK - UpdateLatestMicrocode CaptureFFU.ps1 - `$WindowsVersion` 2016 and 2019 for LTSC releases - Changed some SKU spacing to make things more consistent and included Enterprise N LTSC ApplyFFU.ps1 - Updated version to 2505.1 --- FFUDevelopment/BuildFFUVM.ps1 | 482 +++++++++++++----- .../WinPECaptureFFUFiles/CaptureFFU.ps1 | 26 +- .../WinPEDeployFFUFiles/ApplyFFU.ps1 | 2 +- FFUDevelopment/config/Sample_default.json | Bin 5784 -> 6036 bytes 4 files changed, 366 insertions(+), 144 deletions(-) diff --git a/FFUDevelopment/BuildFFUVM.ps1 b/FFUDevelopment/BuildFFUVM.ps1 index f5fda1c..886246e 100644 --- a/FFUDevelopment/BuildFFUVM.ps1 +++ b/FFUDevelopment/BuildFFUVM.ps1 @@ -15,6 +15,9 @@ When set to $true, will allow the use of media identified as External Hard Disk .PARAMETER AllowVHDXCaching When set to $true, will cache the VHDX file to the $FFUDevelopmentPath\VHDXCache folder and create a config json file that will keep track of the Windows build information, the updates installed, and the logical sector byte size information. Default is $false. +.PARAMETER AppListPath +Path to a JSON file containing a list of applications to install using WinGet. Default is $FFUDevelopmentPath\Apps\AppList.json. + .PARAMETER AppsScriptVariables When passed a hashtable, the script will alter the $FFUDevelopmentPath\Apps\InstallAppsandSysprep.cmd file to set variables with the hashtable keys as variable names and the hashtable values their content. @@ -117,6 +120,9 @@ When set to $true, will optimize the FFU file. Default is $true. .PARAMETER OptionalFeatures Provide a semicolon-separated list of Windows optional features you want to include in the FFU (e.g., netfx3;TFTP). +.PARAMETER PEDriversFolder +Path to the folder containing drivers to be injected into the WinPE deployment media. Default is $FFUDevelopmentPath\PEDrivers. + .PARAMETER Processors Number of virtual processors for the virtual machine. Recommended to use at least 4. @@ -132,6 +138,9 @@ When set to $true, will remove the FFU file from the $FFUDevelopmentPath\FFU fol .PARAMETER ShareName Name of the shared folder for FFU capture. The default is FFUCaptureShare. This share will be created with rights for the user account. When finished, the share will be removed. +.PARAMETER UpdateADK +When set to $true, the script will check for and install the latest Windows ADK and WinPE add-on if they are not already installed or up-to-date. Default is $true. + .PARAMETER UpdateEdge When set to $true, will download and install the latest Microsoft Edge for Windows 10/11. Default is $false. @@ -141,12 +150,12 @@ When set to $true, will download and install the latest cumulative update for Wi .PARAMETER UpdatePreviewCU When set to $true, will download and install the latest Preview cumulative update for Windows 10/11. Default is $false. -.PARAMETER UpdateLatestNet -When set to $true, will download and install the latest .NET Framework for Windows 10/11. Default is $false. - .PARAMETER UpdateLatestDefender When set to $true, will download and install the latest Windows Defender definitions and Defender platform update. Default is $false. +.PARAMETER UpdateLatestMicrocode +When set to $true, will download and install the latest microcode updates for applicable Windows releases (e.g., Windows Server 2016/2019, Windows 10 LTSC 2016/2019) into the FFU. Default is $false. + .PARAMETER UpdateLatestMSRT When set to $true, will download and install the latest Windows Malicious Software Removal Tool. Default is $false. @@ -224,7 +233,31 @@ param( [Parameter(Mandatory = $false, Position = 0)] [ValidateScript({ Test-Path $_ })] [string]$ISOPath, - [ValidateSet('Home', 'Home N', 'Home Single Language', 'Education', 'Education N', 'Pro', 'Pro N', 'Pro Education', 'Pro Education N', 'Pro for Workstations', 'Pro N for Workstations', 'Enterprise', 'Enterprise N', 'Enterprise LTSC', 'Enterprise N LTSC', 'IoT Enterprise LTSC', 'IoT Enterprise N LTSC', 'Standard', 'Standard (Desktop Experience)', 'Datacenter', 'Datacenter (Desktop Experience)')] + [ValidateSet( + 'Home', + 'Home N', + 'Home Single Language', + 'Education', + 'Education N', + 'Pro', + 'Pro N', + 'Pro Education', + 'Pro Education N', + 'Pro for Workstations', + 'Pro N for Workstations', + 'Enterprise', + 'Enterprise N', + 'Enterprise 2016 LTSB', + 'Enterprise N 2016 LTSB', + 'Enterprise LTSC', + 'Enterprise N LTSC', + 'IoT Enterprise LTSC', + 'IoT Enterprise N LTSC', + 'Standard', + 'Standard (Desktop Experience)', + 'Datacenter', + 'Datacenter (Desktop Experience)' + )] [string]$WindowsSKU = 'Pro', [ValidateScript({ Test-Path $_ })] [string]$FFUDevelopmentPath = $PSScriptRoot, @@ -320,6 +353,7 @@ param( [bool]$RemoveFFU, [bool]$UpdateLatestCU, [bool]$UpdatePreviewCU, + [bool]$UpdateLatestMicrocode, [bool]$UpdateLatestNet, [bool]$UpdateLatestDefender, [bool]$UpdateLatestMSRT, @@ -358,9 +392,10 @@ param( [ValidateScript({ $_ -eq $null -or (Test-Path $_) })] [string]$ConfigFile, [Parameter(Mandatory = $false)] - [string]$ExportConfigFile + [string]$ExportConfigFile, + [bool]$UpdateADK = $true ) -$version = '2412.3' +$version = '2505.1' # If a config file is specified and it exists, load it if ($ConfigFile -and (Test-Path -Path $ConfigFile)) { @@ -399,9 +434,36 @@ if ($ConfigFile -and (Test-Path -Path $ConfigFile)) { } } -$clientSKUs = @('Home', 'Home N', 'Home Single Language', 'Education', 'Education N', 'Pro', 'Pro N', 'Pro Education', 'Pro Education N', 'Pro for Workstations', 'Pro N for Workstations', 'Enterprise', 'Enterprise N') -$LTSCSKUs = @('Enterprise LTSC', 'Enterprise N LTSC', 'IoT Enterprise LTSC', 'IoT Enterprise N LTSC') -$ServerSKUs = @('Standard', 'Standard (Desktop Experience)', 'Datacenter', 'Datacenter (Desktop Experience)') +# Validate that the selected Windows SKU is compatible with the chosen Windows release and ensure an ISO is provided for unsupported releases +$clientSKUs = @( + 'Home', + 'Home N', + 'Home Single Language', + 'Education', + 'Education N', + 'Pro', + 'Pro N', + 'Pro Education', + 'Pro Education N', + 'Pro for Workstations', + 'Pro N for Workstations', + 'Enterprise', + 'Enterprise N' +) +$LTSCSKUs = @( + 'Enterprise 2016 LTSB', + 'Enterprise N 2016 LTSB', + 'Enterprise LTSC', + 'Enterprise N LTSC', + 'IoT Enterprise LTSC', + 'IoT Enterprise N LTSC' +) +$ServerSKUs = @( + 'Standard', + 'Standard (Desktop Experience)', + 'Datacenter', + 'Datacenter (Desktop Experience)' +) $releaseToSKUMapping = @{ 10 = $clientSKUs 11 = $clientSKUs @@ -470,6 +532,7 @@ if (-not $VHDXPath) { $VHDXPath = "$VMPath\$VMName.vhdx" } if (-not $FFUCaptureLocation) { $FFUCaptureLocation = "$FFUDevelopmentPath\FFU" } if (-not $LogFile) { $LogFile = "$FFUDevelopmentPath\FFUDevelopment.log" } if (-not $KBPath) { $KBPath = "$FFUDevelopmentPath\KB" } +if (-not $MicrocodePath) { $MicrocodePath = "$KBPath\Microcode" } if (-not $DefenderPath) { $DefenderPath = "$AppsPath\Defender" } if (-not $MSRTPath) { $MSRTPath = "$AppsPath\MSRT" } if (-not $OneDrivePath) { $OneDrivePath = "$AppsPath\OneDrive" } @@ -492,13 +555,14 @@ if ($installationType -eq 'Server'){ } if (-not $AppListPath) { $AppListPath = "$AppsPath\AppList.json" } -if ($WindowsSKU -like "*LTSC") { +if ($WindowsSKU -like "*LTS*") { switch ($WindowsRelease) { 2016 { $WindowsVersion = '1607' } 2019 { $WindowsVersion = '1809' } 2021 { $WindowsVersion = '21H2' } 2024 { $WindowsVersion = '24H2' } } + $isLTSC = $true } #FUNCTIONS @@ -1761,18 +1825,21 @@ function Confirm-ADKVersionIsLatest { function Get-ADK { # Check if latest ADK and WinPE add-on are installed - $latestADKInstalled = Confirm-ADKVersionIsLatest -ADKOption "Windows ADK" - $latestWinPEInstalled = Confirm-ADKVersionIsLatest -ADKOption "WinPE add-on" + if ($UpdateADK) { + WriteLog "Checking if latest ADK and WinPE add-on are installed" + $latestADKInstalled = Confirm-ADKVersionIsLatest -ADKOption "Windows ADK" + $latestWinPEInstalled = Confirm-ADKVersionIsLatest -ADKOption "WinPE add-on" - # Uninstall older versions and install latest versions if necessary - if (-not $latestADKInstalled) { - Uninstall-ADK -ADKOption "Windows ADK" - Install-ADK -ADKOption "Windows ADK" - } + # Uninstall older versions and install latest versions if necessary + if (-not $latestADKInstalled) { + Uninstall-ADK -ADKOption "Windows ADK" + Install-ADK -ADKOption "Windows ADK" + } - if (-not $latestWinPEInstalled) { - Uninstall-ADK -ADKOption "WinPE add-on" - Install-ADK -ADKOption "WinPE add-on" + if (-not $latestWinPEInstalled) { + Uninstall-ADK -ADKOption "WinPE add-on" + Install-ADK -ADKOption "WinPE add-on" + } } # Define registry path @@ -2460,14 +2527,17 @@ function Get-KBLink { $VerbosePreference = $OriginalVerbosePreference # Extract the first KB article ID from the HTML content and store it globally - if ($results.Content -match '>\s*([^\(<]+)\(KB(\d+)\)\s*<') { - $kbArticleID = "KB$($matches[2])" - $global:LastKBArticleID = $kbArticleID - WriteLog "Found KB article ID: $kbArticleID" - } - else { - WriteLog "No KB article ID found in search results." - $global:LastKBArticleID = $null + # Edge and Defender do not have KB article IDs + if ($Name -notmatch 'Defender|Edge') { + if ($results.Content -match '>\s*([^\(<]+)\(KB(\d+)\)\s*<') { + $kbArticleID = "KB$($matches[2])" + $global:LastKBArticleID = $kbArticleID + WriteLog "Found KB article ID: $kbArticleID" + } + else { + WriteLog "No KB article ID found in search results." + $global:LastKBArticleID = $null + } } $kbids = $results.InputFields | @@ -2555,41 +2625,63 @@ function Save-KB { [string[]]$Name, [string]$Path ) - - if ($WindowsArch -eq 'x64') { - [array]$WindowsArch = @("x64", "amd64") - } foreach ($kb in $name) { $links = Get-KBLink -Name $kb foreach ($link in $links) { - if (!($link -match 'x64' -or $link -match 'amd64' -or $link -match 'x86' -or $link -match 'arm64')) { - WriteLog "No architecture found in $link, skipping" - continue - } + # if (!($link -match 'x64' -or $link -match 'amd64' -or $link -match 'x86' -or $link -match 'arm64')) { + # WriteLog "No architecture found in $link, skipping" + # continue + # } if ($link -match 'x64' -or $link -match 'amd64') { - if($WindowsArch -is [array]) { - if ($link -match $WindowsArch[0] -or $link -match $WindowsArch[1]) { - Writelog "Downloading $Link for $WindowsArch to $Path" - Start-BitsTransferWithRetry -Source $link -Destination $Path - $fileName = ($link -split '/')[-1] - Writelog "Returning $fileName" - #With Windows 11 24H2 and Checkpoint CUs, there are multiple files that are downloaded - # break - } + if ($WindowsArch -eq 'x64') { + Writelog "Downloading $link for $WindowsArch to $Path" + Start-BitsTransferWithRetry -Source $link -Destination $Path + $fileName = ($link -split '/')[-1] + Writelog "Returning $fileName" } } - if ($link -match 'arm64') { + elseif ($link -match 'arm64') { if ($WindowsArch -eq 'arm64') { Writelog "Downloading $Link for $WindowsArch to $Path" Start-BitsTransferWithRetry -Source $link -Destination $Path $fileName = ($link -split '/')[-1] Writelog "Returning $fileName" - #With Windows 11 24H2 and Checkpoint CUs, there are multiple files that are downloaded - # break } - } + } + elseif ($link -match 'x86') { + if ($WindowsArch -eq 'x86') { + Writelog "Downloading $link for $WindowsArch to $Path" + Start-BitsTransferWithRetry -Source $link -Destination $Path + $fileName = ($link -split '/')[-1] + Writelog "Returning $fileName" + } + + } + else { + WriteLog "No architecture found in $link" + + #If no architecture is found, download the file and run it through Get-PEArchitecture to determine the architecture + Writelog "Downloading $link to $Path and analyzing file for architecture" + Start-BitsTransferWithRetry -Source $link -Destination $Path + + #Take the file and run it through Get-PEArchitecture to determine the architecture + $fileName = ($link -split '/')[-1] + $filePath = Join-Path -Path $Path -ChildPath $fileName + $arch = Get-PEArchitecture -FilePath $filePath + Writelog "$fileName is $arch" + #If the architecture matches $WindowsArch, keep the file, otherwise delete it + if ($arch -eq $WindowsArch) { + Writelog "Architecture for $fileName matches $WindowsArch, keeping file" + return $fileName + } + else { + Writelog "Deleting $fileName, architecture does not match" + Remove-Item -Path $filePath -Force + } + } + } } return $fileName @@ -3193,6 +3285,15 @@ function Get-ShortenedWindowsSKU { 'Enterprise' { 'Ent' } 'EnterpriseN' { 'Ent_N' } 'Enterprise N' { 'Ent_N' } + 'Enterprise N LTSC' { 'Ent_N_LTSC' } + 'EnterpriseS' { 'Ent_LTSC' } + 'EnterpriseSN' { 'Ent_N_LTSC' } + 'Enterprise LTSC' { 'Ent_LTSC' } + 'Enterprise 2016 LTSB' { 'Ent_LTSC' } + 'Enterprise N 2016 LTSB' { 'Ent_N_LTSC' } + 'IoT Enterprise LTSC' { 'IoT_Ent_LTSC' } + 'IoTEnterpriseS' { 'IoT_Ent_LTSC' } + 'IoT Enterprise N LTSC' { 'IoT_Ent_N_LTSC' } 'ServerStandard' { 'Srv_Std' } 'Standard' { 'Srv_Std' } 'ServerDatacenter' { 'Srv_Dtc' } @@ -3204,7 +3305,14 @@ function Get-ShortenedWindowsSKU { } function New-FFUFileName { - + + # $Winverinfo.name will be either Win10 or Win11 for client OSes + # Since WindowsRelease now includes dates, it breaks default name template in the config file + # This should keep in line with the naming that's done via VM Captures + if ($installationType -eq 'Client' -and $winverinfo) { + $WindowsRelease = $winverinfo.name + } + $BuildDate = Get-Date -uformat %b%Y # Replace '{WindowsRelease}' with the Windows release (e.g., 10, 11, 2016, 2019, 2022, 2025) $CustomFFUNameTemplate = $CustomFFUNameTemplate -replace '{WindowsRelease}', $WindowsRelease @@ -3441,26 +3549,30 @@ Function Get-WindowsVersionInfo { [int]$CurrentBuild = Get-ItemPropertyValue -Path 'HKLM:\FFU\Microsoft\Windows NT\CurrentVersion\' -Name 'CurrentBuild' WriteLog "Windows Build: $CurrentBuild" #DisplayVersion does not exist for 1607 builds (RS1 and Server 2016) and Server 2019 - if($CurrentBuild -notin (14393, 17763)) { - $DisplayVersion = Get-ItemPropertyValue -Path 'HKLM:\FFU\Microsoft\Windows NT\CurrentVersion\' -Name 'DisplayVersion' - WriteLog "Windows Version: $DisplayVersion" + if ($CurrentBuild -notin (14393, 17763)) { + $DisplayVersion = Get-ItemPropertyValue -Path 'HKLM:\FFU\Microsoft\Windows NT\CurrentVersion\' -Name 'DisplayVersion' + WriteLog "Windows Version: $DisplayVersion" + } + # For Windows 10 LTSC 2019, set DisplayVersion to 2019 + if ($CurrentBuild -eq 17763 -and $InstallationType -eq "Client") { + $DisplayVersion = '2019' } $BuildDate = Get-Date -uformat %b%Y - $SKU = switch ($SKU) { - Core { 'Home' } - Professional { 'Pro' } - ProfessionalEducation { 'Pro_Edu' } - Enterprise { 'Ent' } - EnterpriseS { 'Ent_LTSC' } - IoTEnterpriseS { 'IoT_Ent_LTSC' } - Education { 'Edu' } - ProfessionalWorkstation { 'Pro_Wks' } - ServerStandard { 'Srv_Std' } - ServerDatacenter { 'Srv_Dtc' } - } - WriteLog "Windows SKU Modified to: $SKU" + # $SKU = switch ($SKU) { + # Core { 'Home' } + # Professional { 'Pro' } + # ProfessionalEducation { 'Pro_Edu' } + # Enterprise { 'Ent' } + # EnterpriseS { 'Ent_LTSC' } + # IoTEnterpriseS { 'IoT_Ent_LTSC' } + # Education { 'Edu' } + # ProfessionalWorkstation { 'Pro_Wks' } + # ServerStandard { 'Srv_Std' } + # ServerDatacenter { 'Srv_Dtc' } + # } + # WriteLog "Windows SKU Modified to: $SKU" # $WindowsSKU = switch ($WindowsSKU) { # Core { 'Home' } @@ -3469,7 +3581,7 @@ Function Get-WindowsVersionInfo { # Enterprise { 'Ent' } # Education { 'Edu' } # ProfessionalWorkstation { 'Pro_Wks' } - # ServerStandard { 'Srv_Std' } + # ServerStandard { 'Srv_Std' } # ServerDatacenter { 'Srv_Dtc' } # } @@ -4059,6 +4171,37 @@ function Export-ConfigFile{ # Convert to JSON and save $orderedParams | ConvertTo-Json | Out-File $ExportConfigFile -Force } +function Get-PEArchitecture { + param( + [string]$FilePath + ) + + # Read the entire file as bytes. + $bytes = [System.IO.File]::ReadAllBytes($FilePath) + + # Check for the 'MZ' signature. + if ($bytes[0] -ne 0x4D -or $bytes[1] -ne 0x5A) { + throw "The file is not a valid PE file." + } + + # The PE header offset is stored at offset 0x3C. + $peHeaderOffset = [System.BitConverter]::ToInt32($bytes, 0x3C) + + # Verify the PE signature "PE\0\0". + if ($bytes[$peHeaderOffset] -ne 0x50 -or $bytes[$peHeaderOffset + 1] -ne 0x45) { + throw "Invalid PE header." + } + + # The Machine field is located immediately after the PE signature. + $machine = [System.BitConverter]::ToUInt16($bytes, $peHeaderOffset + 4) + + switch ($machine) { + 0x014c { return "x86" } + 0x8664 { return "x64" } + 0xAA64 { return "ARM64" } + default { return ("Unknown architecture: 0x{0:X}" -f $machine) } + } +} ###END FUNCTIONS @@ -4421,33 +4564,33 @@ if ($InstallApps) { Set-Content -Path "$AppsPath\InstallAppsandSysprep.cmd" -Value $UpdatedcmdContent WriteLog "Update complete" - ###### 9/4/2024 - Windows Security Platform update is no longer available from Update Catalog. Will change to using + ###### 5/20/2025 - Security Platform URLs are not available for download, will go back to using the Microsoft Update Catalog in UI build ###### https://support.microsoft.com/en-us/topic/windows-security-update-a6ac7d2e-b1bf-44c0-a028-41720a242da3 #Download Windows Security Platform Update - WriteLog "Downloading Windows Security Platform Update" - if ($WindowsArch -eq 'x64') { - $securityPlatformURL = 'https://definitionupdates.microsoft.com/download/DefinitionUpdates/windowssecurity/10.0.27703.1006/x64/securityhealthsetup.exe' - } - if ($WindowsArch -eq 'ARM64') { - $securityPlatformURL = 'https://definitionupdates.microsoft.com/download/DefinitionUpdates/windowssecurity/10.0.27703.1006/arm64/securityhealthsetup.exe' - } - try { - WriteLog "Windows Security Platform Update URL is $securityPlatformURL" - Start-BitsTransferWithRetry -Source $securityPlatformURL -Destination "$DefenderPath\securityhealthsetup.exe" - WriteLog "Windows Security Platform Update downloaded to $DefenderPath\securityhealthsetup.exe" - } - catch { - Write-Host "Downloading Windows Security Platform Update Failed" - WriteLog "Downloading Windows Security Platform Update Failed with error $_" - throw $_ - } - # Modify InstallAppsandSysprep.cmd to add in $KBFilePath on the line after REM Install Windows Security Platform Update - WriteLog "Updating $AppsPath\InstallAppsandSysprep.cmd to include Windows Security Platform Update" - $CmdContent = Get-Content -Path "$AppsPath\InstallAppsandSysprep.cmd" - $UpdatedcmdContent = $CmdContent -replace '^(REM Install Windows Security Platform Update)', ("REM Install Windows Security Platform Update`r`nd:\Defender\securityhealthsetup.exe") - Set-Content -Path "$AppsPath\InstallAppsandSysprep.cmd" -Value $UpdatedcmdContent - WriteLog "Update complete" + # WriteLog "Downloading Windows Security Platform Update" + # if ($WindowsArch -eq 'x64') { + # $securityPlatformURL = 'https://definitionupdates.microsoft.com/download/DefinitionUpdates/windowssecurity/10.0.27703.1006/x64/securityhealthsetup.exe' + # } + # if ($WindowsArch -eq 'ARM64') { + # $securityPlatformURL = 'https://definitionupdates.microsoft.com/download/DefinitionUpdates/windowssecurity/10.0.27703.1006/arm64/securityhealthsetup.exe' + # } + # try { + # WriteLog "Windows Security Platform Update URL is $securityPlatformURL" + # Start-BitsTransferWithRetry -Source $securityPlatformURL -Destination "$DefenderPath\securityhealthsetup.exe" + # WriteLog "Windows Security Platform Update downloaded to $DefenderPath\securityhealthsetup.exe" + # } + # catch { + # Write-Host "Downloading Windows Security Platform Update Failed" + # WriteLog "Downloading Windows Security Platform Update Failed with error $_" + # throw $_ + # } + # # Modify InstallAppsandSysprep.cmd to add in $KBFilePath on the line after REM Install Windows Security Platform Update + # WriteLog "Updating $AppsPath\InstallAppsandSysprep.cmd to include Windows Security Platform Update" + # $CmdContent = Get-Content -Path "$AppsPath\InstallAppsandSysprep.cmd" + # $UpdatedcmdContent = $CmdContent -replace '^(REM Install Windows Security Platform Update)', ("REM Install Windows Security Platform Update`r`nd:\Defender\securityhealthsetup.exe") + # Set-Content -Path "$AppsPath\InstallAppsandSysprep.cmd" -Value $UpdatedcmdContent + # WriteLog "Update complete" } if ($UpdateLatestMSRT) { WriteLog "`$UpdateLatestMSRT is set to true." @@ -4455,10 +4598,10 @@ if ($InstallApps) { if ($WindowsRelease -in 10, 11) { $Name = """Windows Malicious Software Removal Tool x64""" + " " + """Windows $WindowsRelease""" } - elseif ($WindowsRelease -in 2016, 2019, 2021 -and $WindowsSKU -like "*LTSC") { + elseif ($WindowsRelease -in 2016, 2019, 2021 -and $isLTSC) { $Name = """Windows Malicious Software Removal Tool x64""" + " " + """Windows 10""" } - elseif ($WindowsRelease -in 2024 -and $WindowsSKU -like "*LTSC") { + elseif ($WindowsRelease -in 2024 -and $isLTSC) { $Name = """Windows Malicious Software Removal Tool x64""" + " " + """Windows 11""" } #Windows Server 2025 isn't listed as a product in the Microsoft Update Catalog, so we'll use the 2019 version @@ -4609,7 +4752,7 @@ try { if ($WindowsRelease -in 2016, 2019 -and $installationType -eq "Server") { $Name = """Cumulative update for Windows Server $WindowsRelease for $WindowsArch""" } - if ($WindowsRelease -in 2016, 2019, 2021 -and $WindowsSKU -like "*LTSC") { + if ($WindowsRelease -in 2016, 2019, 2021 -and $isLTSC) { $today = Get-Date $firstDayOfMonth = Get-Date -Year $today.Year -Month $today.Month -Day 1 $secondTuesday = $firstDayOfMonth.AddDays(((2 - [int]$firstDayOfMonth.DayOfWeek + 7) % 7) + 7) @@ -4617,7 +4760,7 @@ try { # More precise search to prevent Dynamic cumulative update from being chosen. $Name = """$($updateDate.ToString('yyyy-MM')) Cumulative update for Windows 10 Version $WindowsVersion for $WindowsArch""" } - if ($WindowsRelease -eq 2024 -and $WindowsSKU -like "*LTSC") { + if ($WindowsRelease -eq 2024 -and $isLTSC) { $Name = """Cumulative update for Windows 11 Version $WindowsVersion for $WindowsArch""" } #Check if $KBPath exists, if not, create it @@ -4626,14 +4769,14 @@ try { New-Item -Path $KBPath -ItemType Directory -Force | Out-Null } #Get latest Servicing Stack Update for Windows Server 2016 - if ($WindowsRelease -eq 2016) { + if ($WindowsRelease -eq 2016 -and $installationType -eq "Server") { $SSUName = """Servicing stack update for Windows Server $WindowsRelease for $WindowsArch""" WriteLog "Searching for $SSUName from Microsoft Update Catalog and saving to $KBPath" $SSUFile = Save-KB -Name $SSUName -Path $KBPath $SSUFilePath = "$KBPath\$SSUFile" WriteLog "Latest SSU saved to $SSUFilePath" } - if ($WindowsRelease -in 2016, 2019, 2021 -and $WindowsSKU -like "*LTSC") { + if ($WindowsRelease -in 2016, 2019, 2021 -and $isLTSC) { $SSUName = """Servicing Stack Update for Windows 10 Version $WindowsVersion for $WindowsArch""" WriteLog "Searching for $SSUName from Microsoft Update Catalog and saving to $KBPath" $SSUFile = Save-KB -Name $SSUName -Path $KBPath @@ -4706,57 +4849,100 @@ try { WriteLog "Creating $KBPath" New-Item -Path $KBPath -ItemType Directory -Force | Out-Null } - if ($WindowsRelease -in 2016, 2019, 2021 -and $WindowsSKU -like "*LTSC") { - $SSUName = """Servicing Stack Update for Windows 10 Version $WindowsVersion for $WindowsArch""" - WriteLog "Searching for $SSUName from Microsoft Update Catalog and saving to $KBPath" - $SSUFile = Save-KB -Name $SSUName -Path $KBPath - $SSUFilePath = "$KBPath\$SSUFile" - WriteLog "Latest SSU saved to $SSUFilePath" - } - if ($WindowsRelease -in 10, 11) { - $Name = "Cumulative update for .NET framework windows $WindowsRelease $WindowsVersion $WindowsArch -preview" - } - if ($WindowsRelease -eq 2025) { - $Name = """Cumulative Update for .NET Framework"" ""3.5 and 4.8.1"" for Windows 11 24H2 x64 -preview" - } - if ($WindowsRelease -eq 2022) { - $Name = """Cumulative Update for .NET Framework 3.5, 4.8 and 4.8.1"" ""operating system version 21H2 for x64""" - } - if ($WindowsRelease -eq 2019 -and $installationType -eq "Server") { - $Name = """Cumulative Update for .NET Framework 3.5, 4.7.2 and 4.8 for Windows Server 2019 for x64""" - } - if ($WindowsRelease -eq 2016 -and $installationType -eq "Server") { - $Name = """Cumulative Update for .NET Framework 4.8 for Windows Server 2016 for x64""" - } - if ($WindowsRelease -in 2016, 2019, 2021 -and $WindowsSKU -like "*LTSC") { - $Name = "Cumulative update for .net framework windows 10 $WindowsVersion $WindowsArch" + + ###### + #LTSC# + ###### + + # For Windows 10 LTSC editions (2016, 2019, 2021), download and save the latest Servicing Stack Update (SSU) and .NET Framework cumulative update(s) + if ($WindowsRelease -in 2016, 2019, 2021 -and $isLTSC) { + # SSU likely was downloaded via CU, but still needed here if .net is being updated, no need to download twice though + if ($null -eq $SSUFile) { + $SSUName = """Servicing Stack Update for Windows 10 Version $WindowsVersion for $WindowsArch""" + WriteLog "Searching for $SSUName from Microsoft Update Catalog and saving to $KBPath" + $SSUFile = Save-KB -Name $SSUName -Path $KBPath + $SSUFilePath = "$KBPath\$SSUFile" + WriteLog "Latest SSU saved to $SSUFilePath" + } + + # For Windows 10 LTSC editions (2016, 2019, 2021), download and save the latest .NET Framework cumulative update(s) + # to a dedicated NET subdirectory, as these editions may include multiple .NET updates that need to be installed together. + if ($WindowsRelease -in 2016) { + $name = """Cumulative Update for .NET Framework 4.8 for Windows 10 version $WindowsVersion for $WindowsArch""" + } + if ($WindowsRelease -eq 2019) { + $name = """Cumulative Update for .NET Framework 3.5, 4.7.2 and 4.8 for Windows 10 Version $WindowsVersion for $WindowsArch""" + } + if ($WindowsRelease -eq 2021){ + $name = """Cumulative Update for .NET Framework 3.5, 4.8 and 4.8.1 for Windows 10 Version $WindowsVersion for $WindowsArch""" + } + $NETPath = Join-Path -Path $KBPath -ChildPath "NET" - # These LTSC editions include multiple .NET updates, so a separate directory is created and specified to allow DISM - # to install them all, instead of specifying each .NET update individually. if (-not (Test-Path -Path $NETPath)) { WriteLog "Creating $NETPath" New-Item -Path $NETPath -ItemType Directory -Force | Out-Null } - $NETFileName = Save-KB -Name $Name -Path $NETPath + WriteLog "Searching for $name from Microsoft Update Catalog and saving to $NETPath" + $NETFileName = Save-KB -Name $name -Path $NETPath WriteLog "Latest .NET Framework cumulative update saved to $NETPath\$NETFileName" } - if ($WindowsRelease -eq 2024) { - $Name = "Cumulative update for .NET framework windows 11 $WindowsVersion $WindowsArch" + + # For Windows 11 LTSC 2024, set the update name to search for the latest .NET Framework cumulative update in the Microsoft Update Catalog + if ($WindowsRelease -eq 2024 -and $isLTSC) { + $Name = "Cumulative update for .NET framework windows 11 $WindowsVersion $WindowsArch -preview" } - WriteLog "Searching for $name from Microsoft Update Catalog and saving to $KBPath" - if ($WindowsRelease -eq 2021) { + + # For Windows 10 LTSC 2021, download and save the latest .NET Framework 4.8.1 feature pack to the NET subdirectory. + if ($WindowsRelease -eq 2021 -and $isLTSC) { WriteLog "Checking for latest .NET Framework feature pack for Windows $WindowsRelease $WindowsSKU" $NETFeatureName = """Microsoft .NET Framework 4.8.1 for Windows 10 Version 21H2 for x64""" $NETFeaturePackFile = Save-KB -Name $NETFeatureName -Path $NETPath WriteLog "Latest .NET Framework Feature pack saved to $NETPath\$NETFeaturePackFile" } - if ($WindowsRelease -in 2016, 2019) { + # For Windows 10 LTSC 2016 and 2019, download and save the latest .NET Framework 4.8 feature pack to the NET subdirectory. + if ($WindowsRelease -in 2016, 2019 -and $isLTSC) { WriteLog "Checking for latest .NET Framework feature pack for Windows $WindowsRelease $WindowsSKU" $NETFeatureName = """Microsoft .NET Framework 4.8 for Windows 10 Version $WindowsVersion and Windows Server $WindowsRelease for x64""" $NETFeaturePackFile = Save-KB -Name $NETFeatureName -Path $NETPath WriteLog "Latest .NET Framework Feature pack saved to $NETPath\$NETFeaturePackFile" } - if (-not ($WindowsRelease -in 2016, 2019, 2021 -and $WindowsSKU -like "*LTSC")) { + + ######## + #CLIENT# + ######## + + # For Windows 10 and 11, set the update name to search for the latest .NET Framework cumulative update (excluding preview) in the Microsoft Update Catalog + if ($WindowsRelease -in 10, 11) { + $Name = "Cumulative update for .NET framework windows $WindowsRelease $WindowsVersion $WindowsArch -preview" + } + + ######## + #SERVER# + ######## + + # For Windows Server 2025, set the update name to search for the latest .NET Framework cumulative update (excluding preview) in the Microsoft Update Catalog + if ($WindowsRelease -eq 2025 -and $installationType -eq "Server") { + $Name = """Cumulative Update for .NET Framework"" ""3.5 and 4.8.1"" for Windows 11 24H2 x64 -preview" + } + + # For Windows Server 2022, set the update name to search for the latest .NET Framework cumulative update (3.5, 4.8, and 4.8.1) for OS version 21H2 x64 + if ($WindowsRelease -eq 2022 -and $installationType -eq "Server") { + $Name = """Cumulative Update for .NET Framework 3.5, 4.8 and 4.8.1"" ""operating system version 21H2 for x64""" + } + # For Windows Server 2019, set the update name to search for the latest .NET Framework cumulative update (3.5, 4.7.2, and 4.8) for x64 + if ($WindowsRelease -eq 2019 -and $installationType -eq "Server") { + $Name = """Cumulative Update for .NET Framework 3.5, 4.7.2 and 4.8 for Windows Server 2019 for x64""" + } + + # For Windows Server 2016, set the update name to search for the latest .NET Framework 4.8 cumulative update for x64 + if ($WindowsRelease -eq 2016 -and $installationType -eq "Server") { + $Name = """Cumulative Update for .NET Framework 4.8 for Windows Server 2016 for x64""" + } + + # For all editions except Windows 10 LTSC (2016, 2019, 2021), search for the latest .NET Framework cumulative update in the Microsoft Update Catalog, + # download it to $KBPath, and verify the correct file was downloaded by matching the KB article ID. If not found, search for the file by KB article ID. + if (-not ($WindowsRelease -in 2016, 2019, 2021 -and $isLTSC)) { + WriteLog "Searching for $name from Microsoft Update Catalog and saving to $KBPath" $NETFileName = Save-KB -Name $Name -Path $KBPath # Check if $NETFileName contains the string in $global:LastKBArticleID # If it does not, look in $KBPath for the file that contains the string in $global:LastKBArticleID @@ -4779,6 +4965,28 @@ try { WriteLog "Latest .NET Framework saved to $NETPath" } } + # Update latest Microcode + if ($UpdateLatestMicrocode -and $WindowsRelease -in 2016, 2019) { + WriteLog "`$UpdateLatestMicrocode is set to true, checking for latest Microcode" + #Check if $MicrocodePath exists, if not, create it + If (-not (Test-Path -Path $MicrocodePath)) { + WriteLog "Creating $MicrocodePath" + New-Item -Path $MicrocodePath -ItemType Directory -Force | Out-Null + } + + # Windows 10 LTSC 2016 (1607) and Windows Server 2016 + if($WindowsRelease -eq 2016){ + $name = "KB4589210 $windowsArch" + } + + # Windows 10 LTSC 2019 (1809) and Windows Server 2019 + if($WindowsRelease -eq 2019){ + $name = "KB4589208 $windowsArch" + } + WriteLog "Searching for $name from Microsoft Update Catalog and saving to $MicrocodePath" + $MicrocodeFileName = Save-KB -Name $name -Path $MicrocodePath + WriteLog "Latest Microcode saved to $MicrocodePath\$MicrocodeFileName" + } #Search for cached VHDX and skip VHDX creation if there's a cached version if ($AllowVHDXCaching) { @@ -4788,7 +4996,7 @@ try { $vhdxJsons = @(Get-ChildItem -File -Path $VHDXCacheFolder -Filter '*_config.json' | Sort-Object -Property CreationTime -Descending) WriteLog "Found $($vhdxJsons.Count) cached VHDX files" if (Test-Path -Path $KBPath){ - $downloadedKBs = @(Get-ChildItem -File -Path $KBPath) + $downloadedKBs = @(Get-ChildItem -File -Path $KBPath -Recurse) } else { $downloadedKBs = @() @@ -4896,7 +5104,7 @@ try { Remove-Item -Path $SSUFilePath -Force | Out-Null WriteLog 'SSU removed' } - if ($WindowsRelease -in 2016, 2019, 2021 -and $WindowsSKU -like "*LTSC") { + if ($WindowsRelease -in 2016, 2019, 2021 -and $isLTSC) { WriteLog "WindowsRelease is $WindowsRelease and is $WindowsSKU, adding SSU first" WriteLog "Adding SSU to $WindowsPartition" Add-WindowsPackage -Path $WindowsPartition -PackagePath $SSUFilePath | Out-Null @@ -4921,10 +5129,15 @@ try { Add-WindowsPackage -Path $WindowsPartition -PackagePath $NETPath | Out-Null WriteLog "$NETPath added to $WindowsPartition" } + if ($UpdateLatestMicrocode -and $WindowsRelease -in 2016, 2019) { + WriteLog "Adding $MicrocodePath to $WindowsPartition" + Add-WindowsPackage -Path $WindowsPartition -PackagePath $MicrocodePath | Out-Null + WriteLog "$MicrocodePath added to $WindowsPartition" + } WriteLog "KBs added to $WindowsPartition" if ($AllowVHDXCaching) { $cachedVHDXInfo = [VhdxCacheItem]::new() - $includedUpdates = Get-ChildItem -Path $KBPath -File + $includedUpdates = Get-ChildItem -Path $KBPath -File -Recurse foreach ($includedUpdate in $includedUpdates) { $cachedVHDXInfo.IncludedUpdates += ([VhdxCacheUpdateItem]::new($includedUpdate.Name)) @@ -4935,7 +5148,6 @@ try { WriteLog 'Clean Up the WinSxS Folder' WriteLog 'This can take 10+ minutes depending on how old the media is and the size of the KB. Please be patient' Dism /Image:$WindowsPartition /Cleanup-Image /StartComponentCleanup /ResetBase | Out-Null - # Repair-WindowsImage -Path $WindowsPartition -StartComponentCleanup -ResetBase | Out-Null WriteLog 'Clean Up the WinSxS Folder completed' } catch { Write-Host "Adding KB to VHDX failed with error $_" @@ -5130,7 +5342,7 @@ try { } else { #Shorten Windows SKU for use in FFU file name to remove spaces and long names - WriteLog 'Shortening Windows SKU for FFU file name' + WriteLog "Shortening Windows SKU: $WindowsSKU for FFU file name" $shortenedWindowsSKU = Get-ShortenedWindowsSKU -WindowsSKU $WindowsSKU WriteLog "Shortened Windows SKU: $shortenedWindowsSKU" #Create FFU file diff --git a/FFUDevelopment/WinPECaptureFFUFiles/CaptureFFU.ps1 b/FFUDevelopment/WinPECaptureFFUFiles/CaptureFFU.ps1 index 2f77e74..cbd2d7d 100644 --- a/FFUDevelopment/WinPECaptureFFUFiles/CaptureFFU.ps1 +++ b/FFUDevelopment/WinPECaptureFFUFiles/CaptureFFU.ps1 @@ -1,7 +1,6 @@ #Modify the net use W: \\192.168.1.158\FFUCaptureShare /user:ffu_user ddb1f077-3eed-433c-b4d9-7b8cd54ce727 net use W: \\192.168.1.158\FFUCaptureShare /user:ffu_user ddb1f077-3eed-433c-b4d9-7b8cd54ce727 #Custom naming placeholder - $AssignDriveLetter = 'x:\AssignDriveLetter.txt' Start-Process -FilePath diskpart.exe -ArgumentList "/S $AssignDriveLetter" -Wait -ErrorAction Stop | Out-Null #Load Registry Hive @@ -16,24 +15,35 @@ $InstallationType = Get-ItemPropertyValue -Path 'HKLM:\FFU\Microsoft\Windows NT\ if ($CurrentBuild -notin 14393, 17763 -and $InstallationType -ne "Server") { $WindowsVersion = Get-ItemPropertyValue -Path 'HKLM:\FFU\Microsoft\Windows NT\CurrentVersion\' -Name 'DisplayVersion' } +# For Windows 10 LTSB 2016, set WindowsVersion to 2016 +if ($CurrentBuild -eq 14393 -and $InstallationType -eq "Client") { + $WindowsVersion = '2016' +} +# For Windows 10 LTSC 2019, set WindowsVersion to 2019 +if ($CurrentBuild -eq 17763 -and $InstallationType -eq "Client") { + $WindowsVersion = '2019' +} + + $BuildDate = Get-Date -uformat %b%Y $SKU = switch ($SKU) { Core { 'Home' } - CoreN { 'HomeN' } - CoreSingleLanguage { 'HomeSL' } + CoreN { 'Home_N' } + CoreSingleLanguage { 'Home_SL' } Professional { 'Pro' } - ProfessionalN { 'ProN' } + ProfessionalN { 'Pro_N' } ProfessionalEducation { 'Pro_Edu' } - ProfessionalEducationN { 'Pro_EduN' } + ProfessionalEducationN { 'Pro_Edu_N' } Enterprise { 'Ent' } - EnterpriseN { 'EntN'} + EnterpriseN { 'Ent_N' } EnterpriseS { 'Ent_LTSC' } + EnterpriseSN { 'Ent_N_LTSC' } IoTEnterpriseS { 'IoT_Ent_LTSC' } Education { 'Edu' } - EducationN { 'EduN' } + EducationN { 'Edu_N' } ProfessionalWorkstation { 'Pro_Wks' } - ProfessionalWorkstationN { 'Pro_WksN' } + ProfessionalWorkstationN { 'Pro_Wks_N' } ServerStandard { 'Srv_Std' } ServerDatacenter { 'Srv_Dtc' } } diff --git a/FFUDevelopment/WinPEDeployFFUFiles/ApplyFFU.ps1 b/FFUDevelopment/WinPEDeployFFUFiles/ApplyFFU.ps1 index 0d7b7b7..8dc030c 100644 --- a/FFUDevelopment/WinPEDeployFFUFiles/ApplyFFU.ps1 +++ b/FFUDevelopment/WinPEDeployFFUFiles/ApplyFFU.ps1 @@ -135,7 +135,7 @@ $LogFileName = 'ScriptLog.txt' $USBDrive = Get-USBDrive New-item -Path $USBDrive -Name $LogFileName -ItemType "file" -Force | Out-Null $LogFile = $USBDrive + $LogFilename -$version = '2412.4' +$version = '2505.1' WriteLog 'Begin Logging' WriteLog "Script version: $version" diff --git a/FFUDevelopment/config/Sample_default.json b/FFUDevelopment/config/Sample_default.json index 139042464c9a30ae9b76ea84c8966d439e20580d..175d175b4b104f049c40c95a2507e7a6a4edd725 100644 GIT binary patch delta 113 zcmbQCJ4JuO8h#&!Oon2H5{3YVL?D?lS&+?G*^!|D2#XnFfDpolsnTP}0*dD|klYuyeA$9UZX64Bj1SK}-2