mirror of
https://github.com/rbalsleyMSFT/FFU.git
synced 2026-06-14 02:09:35 -06:00
Compare commits
19 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| d809fc021f | |||
| 582df6e3a8 | |||
| d5a4f96482 | |||
| d2fc9cf558 | |||
| b530ac5a5c | |||
| 2659336ee9 | |||
| c32a09bfc1 | |||
| f7ff415374 | |||
| 9cac674d7b | |||
| 2cf7da9c91 | |||
| 0d1d3a1ed5 | |||
| 7e2ebe8013 | |||
| 0606a1278c | |||
| 7c9f24f695 | |||
| 227fb4fa94 | |||
| 77ef154941 | |||
| a00aa3ee02 | |||
| e25a890946 | |||
| bc4a181182 |
@@ -1,5 +1,58 @@
|
||||
# Change Log
|
||||
|
||||
# 2505.1
|
||||
|
||||
Highly recommended that you upgrade to this release. Fixes the issue with the May 2025 cumulative update and some SKU naming issues for SKUs other than Pro.
|
||||
|
||||
# Support for Windows LTSB/LTSC
|
||||
Thanks to @zehadialam for the code to allow support for LTSB and LTSC. This has been a requested feature from a number of customers and some might be opting for LTSC when Windows 10 support ends in October. We support LTSB 2016, LTSC 2019, 2021, 2024 including the N and IoT variants. Extensive testing has gone into validating CU and .net support. File an issue if you see any weird behavior.
|
||||
|
||||
# Support for automating computer naming via CSV
|
||||
Thanks to @JonasKloseBW for PR #150
|
||||
- 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
|
||||
|
||||
# Fixes
|
||||
- Thanks to @JonasKloseBW for PR #129 for adding the -AppListPath parameter
|
||||
- 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 (EDU mainly, but others too)
|
||||
- Fix an issue with checkpoint CUs and May 2025-05B CU. Should future proof new checkpoint CUs in the future.
|
||||
|
||||
# Additional Fixes
|
||||
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
|
||||
|
||||
# 2412.1
|
||||
|
||||
This is a major release with a number of quality-of-life improvements that will reduce the time it takes to create FFUs. I highly recommend you update to this release.
|
||||
|
||||
+516
-116
@@ -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,13 +233,36 @@ 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 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,
|
||||
|
||||
[bool]$InstallApps,
|
||||
|
||||
[string]$AppListPath,
|
||||
[hashtable]$AppsScriptVariables,
|
||||
[bool]$InstallOffice,
|
||||
[ValidateSet('Microsoft', 'Dell', 'HP', 'Lenovo')]
|
||||
@@ -284,7 +316,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',
|
||||
@@ -321,6 +353,7 @@ param(
|
||||
[bool]$RemoveFFU,
|
||||
[bool]$UpdateLatestCU,
|
||||
[bool]$UpdatePreviewCU,
|
||||
[bool]$UpdateLatestMicrocode,
|
||||
[bool]$UpdateLatestNet,
|
||||
[bool]$UpdateLatestDefender,
|
||||
[bool]$UpdateLatestMSRT,
|
||||
@@ -359,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)) {
|
||||
@@ -383,12 +417,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
|
||||
@@ -400,6 +434,53 @@ if ($ConfigFile -and (Test-Path -Path $ConfigFile)) {
|
||||
}
|
||||
}
|
||||
|
||||
# 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
|
||||
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
|
||||
@@ -451,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" }
|
||||
@@ -461,7 +543,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) {
|
||||
@@ -471,6 +553,17 @@ if ($installationType -eq 'Server'){
|
||||
2025 { $WindowsVersion = '24H2' }
|
||||
}
|
||||
}
|
||||
if (-not $AppListPath) { $AppListPath = "$AppsPath\AppList.json" }
|
||||
|
||||
if ($WindowsSKU -like "*LTS*") {
|
||||
switch ($WindowsRelease) {
|
||||
2016 { $WindowsVersion = '1607' }
|
||||
2019 { $WindowsVersion = '1809' }
|
||||
2021 { $WindowsVersion = '21H2' }
|
||||
2024 { $WindowsVersion = '24H2' }
|
||||
}
|
||||
$isLTSC = $true
|
||||
}
|
||||
|
||||
#FUNCTIONS
|
||||
function WriteLog($LogText) {
|
||||
@@ -1732,6 +1825,8 @@ function Confirm-ADKVersionIsLatest {
|
||||
|
||||
function Get-ADK {
|
||||
# Check if latest ADK and WinPE add-on are installed
|
||||
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"
|
||||
|
||||
@@ -1745,6 +1840,7 @@ function Get-ADK {
|
||||
Uninstall-ADK -ADKOption "WinPE add-on"
|
||||
Install-ADK -ADKOption "WinPE add-on"
|
||||
}
|
||||
}
|
||||
|
||||
# Define registry path
|
||||
$adkPathKey = "HKLM:\SOFTWARE\WOW6432Node\Microsoft\Windows Kits\Installed Roots"
|
||||
@@ -1811,7 +1907,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
|
||||
@@ -2429,23 +2525,30 @@ 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
|
||||
# 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 |
|
||||
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 ")(?=.*" ) + ")" ) } |
|
||||
@@ -2522,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"
|
||||
if ($WindowsArch -eq 'x64') {
|
||||
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 ($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
|
||||
@@ -2621,6 +2746,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 }
|
||||
}
|
||||
}
|
||||
@@ -3133,36 +3262,43 @@ 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' }
|
||||
'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' }
|
||||
'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
|
||||
@@ -3170,6 +3306,13 @@ 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
|
||||
@@ -3410,9 +3553,27 @@ Function Get-WindowsVersionInfo {
|
||||
$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"
|
||||
|
||||
# $WindowsSKU = switch ($WindowsSKU) {
|
||||
# Core { 'Home' }
|
||||
# Professional { 'Pro' }
|
||||
@@ -4010,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
|
||||
|
||||
@@ -4307,9 +4499,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) {
|
||||
@@ -4372,40 +4564,46 @@ 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."
|
||||
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 $isLTSC) {
|
||||
$Name = """Windows Malicious Software Removal Tool x64""" + " " + """Windows 10"""
|
||||
}
|
||||
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
|
||||
elseif ($installationType -eq 'server' -and $WindowsRelease -eq '24H2') {
|
||||
$Name = """Windows Malicious Software Removal Tool x64""" + " " + """Windows Server 2019"""
|
||||
@@ -4542,7 +4740,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) {
|
||||
@@ -4551,30 +4749,67 @@ 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 $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)
|
||||
$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 $isLTSC) {
|
||||
$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"
|
||||
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 $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
|
||||
$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"
|
||||
$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
|
||||
#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
|
||||
@@ -4583,36 +4818,174 @@ 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 Preview saved to $CUPPath"
|
||||
}
|
||||
|
||||
#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 -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) {
|
||||
$Name = """Cumulative Update for .NET Framework 3.5, 4.7.2 and 4.8 for Windows Server 2019 for x64"""
|
||||
}
|
||||
if ($WindowsRelease -eq 2016) {
|
||||
$Name = """Cumulative Update for .NET Framework 4.8 for Windows Server 2016 for x64"""
|
||||
}
|
||||
#Check if $KBPath exists, if not, create it
|
||||
If (-not (Test-Path -Path $KBPath)) {
|
||||
if (-not (Test-Path -Path $KBPath)) {
|
||||
WriteLog "Creating $KBPath"
|
||||
New-Item -Path $KBPath -ItemType Directory -Force | Out-Null
|
||||
}
|
||||
|
||||
######
|
||||
#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"
|
||||
if (-not (Test-Path -Path $NETPath)) {
|
||||
WriteLog "Creating $NETPath"
|
||||
New-Item -Path $NETPath -ItemType Directory -Force | Out-Null
|
||||
}
|
||||
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"
|
||||
}
|
||||
|
||||
# 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"
|
||||
}
|
||||
|
||||
# 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"
|
||||
}
|
||||
# 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"
|
||||
}
|
||||
|
||||
########
|
||||
#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"
|
||||
$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 .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
|
||||
@@ -4623,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 = @()
|
||||
@@ -4719,7 +5092,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
|
||||
@@ -4730,14 +5103,41 @@ try {
|
||||
WriteLog "Removing $SSUFilePath"
|
||||
Remove-Item -Path $SSUFilePath -Force | Out-Null
|
||||
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
|
||||
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
|
||||
WriteLog "SSU added to $WindowsPartition"
|
||||
WriteLog "Removing $SSUFilePath"
|
||||
Remove-Item -Path $SSUFilePath -Force | Out-Null
|
||||
WriteLog 'SSU removed'
|
||||
}
|
||||
# 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"
|
||||
}
|
||||
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))
|
||||
@@ -4942,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
|
||||
|
||||
@@ -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
|
||||
@@ -12,26 +11,39 @@ 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'
|
||||
# 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' }
|
||||
}
|
||||
|
||||
@@ -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 = '2505.1'
|
||||
WriteLog 'Begin Logging'
|
||||
WriteLog "Script version: $version"
|
||||
|
||||
@@ -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,6 +285,24 @@ if ($Unattend -and $UnattendPrefix){
|
||||
$computername = Set-Computername($computername)
|
||||
Writelog "Computer name set to $computername"
|
||||
}
|
||||
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'
|
||||
|
||||
Binary file not shown.
Reference in New Issue
Block a user