mirror of
https://github.com/rbalsleyMSFT/FFU.git
synced 2026-06-14 02:09:35 -06:00
+536
-136
@@ -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,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
|
||||
@@ -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"
|
||||
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
|
||||
@@ -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,43 +3262,57 @@ 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
|
||||
|
||||
}
|
||||
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
|
||||
@@ -3406,13 +3549,31 @@ 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"
|
||||
|
||||
# $WindowsSKU = switch ($WindowsSKU) {
|
||||
# Core { 'Home' }
|
||||
# Professional { 'Pro' }
|
||||
@@ -3420,7 +3581,7 @@ Function Get-WindowsVersionInfo {
|
||||
# Enterprise { 'Ent' }
|
||||
# Education { 'Edu' }
|
||||
# ProfessionalWorkstation { 'Pro_Wks' }
|
||||
# ServerStandard { 'Srv_Std' }
|
||||
# ServerStandard { 'Srv_Std' }
|
||||
# ServerDatacenter { 'Srv_Dtc' }
|
||||
# }
|
||||
|
||||
@@ -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
|
||||
}
|
||||
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"
|
||||
|
||||
######
|
||||
#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"
|
||||
$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,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)
|
||||
|
||||
Binary file not shown.
Reference in New Issue
Block a user