Skip to content
Merged
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
165 changes: 144 additions & 21 deletions scripts/windows/Make2023BootableMedia.ps1
Original file line number Diff line number Diff line change
Expand Up @@ -8,8 +8,8 @@
.NOTES
File Name : Make2023BootableMedia.ps1
Author : Microsoft Corporation
Version : 1.3
Date : 2025-11-07
Version : 1.4
Date : 2026-03-13

.LICENSE
Licensed under the BSD License. See License.txt in the project root for full license information.
Expand Down Expand Up @@ -76,11 +76,73 @@ function Show-Usage {
}

function Show-ADK-Req {
Write-Host "The Windows ADK must be installed on the system if trying to create ISO media. Available at http://aka.ms/adk" -ForegroundColor Red
Write-Host "The Windows ADK must be installed on the system if trying to create ISO media. Available at https://aka.ms/adk" -ForegroundColor Red
Write-Host "After install, open an admin-elevated 'Deploy and Imaging Tools Environment' command prompt provided with the ADK." -ForegroundColor Red
Write-Host "Then run PowerShell from this command prompt and you should be good to go.`r`n" -ForegroundColor Red
}

function Download-Oscdimg {
<#
.SYNOPSIS
Downloads oscdimg.exe from the Microsoft public symbol server for the current architecture. These are not signed so
they are validated against known SHA256 hashes before being used.
.OUTPUTS
The file path to the downloaded oscdimg.exe, or $null on failure.
#>

$archUrls = @{
"AMD64" = "https://msdl.microsoft.com/download/symbols/oscdimg.exe/9F01AFB765000/oscdimg.exe"
"ARM64" = "https://msdl.microsoft.com/download/symbols/oscdimg.exe/2267BF2C66000/oscdimg.exe"
"x86" = "https://msdl.microsoft.com/download/symbols/oscdimg.exe/CFBCC93A60000/oscdimg.exe"
}

$arch = $env:PROCESSOR_ARCHITECTURE
if (-not $archUrls.ContainsKey($arch)) {
Write-Host "Unsupported architecture [$arch] for oscdimg download." -ForegroundColor Red
return $null
}

$url = $archUrls[$arch]
$expectedHash = $global:oscdimg_known_hashes[$arch]
$destPath = Join-Path -Path $env:TEMP -ChildPath "oscdimg.exe"

Write-Host "Downloading oscdimg.exe for [$arch] from Microsoft symbol server..." -ForegroundColor Blue
Write-Dbg-Host "Download URL: $url"
Write-Dbg-Host "Destination: $destPath"

$tmpDownloadPath = "$destPath.download"
try {
Invoke-WebRequest -Uri $url -OutFile $tmpDownloadPath -UseBasicParsing -ErrorAction Stop
} catch {
Write-Host "Failed to download oscdimg.exe: $($_.Exception.Message)" -ForegroundColor Red
Remove-Item -Path $tmpDownloadPath -Force -ErrorAction SilentlyContinue
return $null
}

if (-not (Test-Path $tmpDownloadPath)) {
Write-Host "Download appeared to succeed but file not found at [$tmpDownloadPath]." -ForegroundColor Red
return $null
}

# Validate downloaded file against known SHA256 hash
$actualHash = (Get-FileHash -Path $tmpDownloadPath -Algorithm SHA256).Hash
if ($actualHash -ne $expectedHash) {
Write-Host "Downloaded oscdimg.exe failed integrity check." -ForegroundColor Red
Write-Host "Expected SHA256: $expectedHash" -ForegroundColor Red
Write-Host "Actual SHA256: $actualHash" -ForegroundColor Red
Remove-Item -Path $tmpDownloadPath -Force -ErrorAction SilentlyContinue
return $null
}
Write-Dbg-Host "SHA256 hash verified: $actualHash"

# Move validated file into place
Move-Item -Path $tmpDownloadPath -Destination $destPath -Force

$fileSize = (Get-Item $destPath).Length
Write-Host "Successfully downloaded oscdimg.exe ($fileSize bytes) to [$destPath]" -ForegroundColor Green
return $destPath
}

function Debug-Pause {

if ($global:Dbg_Pause) {
Expand Down Expand Up @@ -175,8 +237,39 @@ function Validate-Requirements {
# See if oscdimg.exe exists in the current working directory
$executablePath = Join-Path -Path $PWD.Path -ChildPath "oscdimg.exe"
if (-not (Test-Path -Path $executablePath)) {
Write-Host "`r`nRequired support tools not found!" -ForegroundColor Red
Write-Dbg-Host "[oscdimg.exe] not found in [$PWD] or in the system PATH!"

# Check if oscdimg.exe was previously downloaded to the temp directory
$tempOscdimg = Join-Path -Path $env:TEMP -ChildPath "oscdimg.exe"
if (Test-Path -Path $tempOscdimg) {
# Validate hash before trusting a cached copy from user-writable temp dir
$expectedHash = $global:oscdimg_known_hashes[$env:PROCESSOR_ARCHITECTURE]
$actualHash = (Get-FileHash -Path $tempOscdimg -Algorithm SHA256).Hash
if ($expectedHash -and $actualHash -eq $expectedHash) {
Write-Dbg-Host "Found previously downloaded [oscdimg.exe] in [$tempOscdimg] with valid hash"
Write-Host "Using previously downloaded oscdimg.exe from [$tempOscdimg]" -ForegroundColor Green
$global:oscdimg_exe = $tempOscdimg
return $true
} else {
Write-Dbg-Host "Cached [oscdimg.exe] at [$tempOscdimg] failed integrity check. Removing."
Remove-Item -Path $tempOscdimg -Force -ErrorAction SilentlyContinue
}
}

# Offer to download oscdimg.exe from the Microsoft public symbol server
Write-Host "`r`noscdimg.exe is required for ISO media creation and was not found on this system." -ForegroundColor Yellow
Write-Host "It can be downloaded directly from the Microsoft public symbol server (~450 KB)." -ForegroundColor Yellow
Write-Host "Alternatively, it is included with an install of the full Windows ADK (https://aka.ms/adk).`r`n" -ForegroundColor Yellow
$response = Read-Host "Download oscdimg.exe from Microsoft? (Y/N)"
if ($response -match '^[Yy]') {
$downloadedPath = Download-Oscdimg
if ($null -ne $downloadedPath) {
$global:oscdimg_exe = $downloadedPath
return $true
}
Write-Host "Download failed. Please install the Windows ADK instead." -ForegroundColor Red
}

Show-ADK-Req
return $false
}
Expand Down Expand Up @@ -367,17 +460,18 @@ function Initialize-StagingDirectory {

$global:Staging_Directory_Path = $tmpPath

$driveLetter = $global:Staging_Directory_Path.Substring(0, 1)
$driveLetter = (Split-Path -Qualifier $global:Staging_Directory_Path).TrimEnd(':')
try {
$fs = (Get-Volume -DriveLetter $driveLetter -ErrorAction Stop).FileSystem
} catch {
Write-Host "Drive [$driveLetter`:] does not exist or is not accessible." -ForegroundColor Red
return $false
}

# Make sure the staging directory is on an NTFS or ReFS formatted file system. This is required for the WIM mounting process.
if ($fs -ne "NTFS" -and $fs -ne "ReFS") {
Write-Host "`r`nStagingDir [$global:Staging_Directory_Path] must target an NTFS or ReFS formatted file system.`r`n" -ForegroundColor Red
# Make sure the staging directory is on an NTFS formatted file system. This is required for WIM mounting
# which uses reparse points not fully supported on ReFS or other file systems.
if ($fs -ne "NTFS") {
Write-Host "`r`nStagingDir [$global:Staging_Directory_Path] must target an NTFS formatted file system (required for WIM mounting).`r`n" -ForegroundColor Red

if ($global:StagingDir_Created -eq $true) {
Write-Dbg-Host "Removing staging directory [$global:Staging_Directory_Path]"
Expand Down Expand Up @@ -473,6 +567,17 @@ function Validate-Parameters {
Write-Dbg-Host "Invalid ISOPath [$ISOPath]"
return $false
}

# Normalize ISOPath to an absolute path
try {
$script:ISOPath = ConvertTo-AbsolutePath -Path $ISOPath
Write-Dbg-Host "ISOPath: [$ISOPath] -> [$script:ISOPath]"
$ISOPath = $script:ISOPath
} catch {
Write-Host "Invalid -ISOPath '$ISOPath': $($_.Exception.Message)" -ForegroundColor Red
return $false
}

# if $ISOPath exists, ask the user if they want to overwrite it, otherwise abort
if (Test-Path -Path $ISOPath) {
Write-Host "ISO [$ISOPath] already exists. Do you want to overwrite it? (Y/N)" -ForegroundColor Yellow
Expand Down Expand Up @@ -570,18 +675,18 @@ function Validate-Parameters {
return $false
}

$driveLetter = $tmpPath.Substring(0,1)
$fs = (Get-Volume -DriveLetter $driveLetter).FileSystem
$driveLetter = (Split-Path -Qualifier $tmpPath).TrimEnd(':')
try {
$fs = (Get-Volume -DriveLetter $driveLetter -ErrorAction Stop).FileSystem
} catch {
Write-Host "Drive [$driveLetter`:] does not exist or is not accessible." -ForegroundColor Red
return $false
}

# Make sure the target drive is NTFS or ReFS. This is required for the WIM mount operations.
if ($fs -ne "NTFS" -and $fs -ne "ReFS") {
Write-Host "`r`n-NewMediaPath [$tmpPath] must target an NTFS or ReFS file system.`r`n" -ForegroundColor Red
# Make sure the target drive is NTFS. This is required for WIM mounting which uses
# reparse points not fully supported on ReFS or other file systems.
if ($fs -ne "NTFS") {
Write-Host "`r`n-NewMediaPath [$tmpPath] must target an NTFS formatted file system (required for WIM mounting).`r`n" -ForegroundColor Red
return $false
}

Expand All @@ -607,17 +712,18 @@ function ConvertTo-AbsolutePath {
[bool] $AllowUNC = $false
)

if ([string]::IsNullOrWhiteSpace($Path)) {
throw "Path cannot be null or empty"
}

# Reject UNC paths
if (-not $AllowUNC) {
if ($Path -match "^\\\\") {
throw "Network (UNC) path not allowed"
}
}

$tmpPath = $Path
if ($Path[-1] -eq "\") {
$tmpPath = $Path.Substring(0, $Path.Length - 1)
}
$tmpPath = $Path.TrimEnd('\')

# If a root drive path (C:\), return as-is
if ($tmpPath -match "^[a-zA-Z]:") {
Expand Down Expand Up @@ -735,7 +841,7 @@ function Copy-2023BootBins {
$mountedImage = Mount-WindowsImage -ImagePath $bootWimPath -Index 1 -Path $bootWimMount -ReadOnly -ErrorAction stop | Out-Null
Write-Dbg-Host "Mounted [$bootWimPath] --> [$bootWimMount]"
} catch {
Write-Host "Failed to mount boot.wim of the source media!`r`nMake sure -StagingDir and -NewMediaPath are targetting an NTFS or ReFS based filesystem." -ForegroundColor Red
Write-Host "Failed to mount boot.wim of the source media!`r`nMake sure -StagingDir is targeting an NTFS formatted file system (ReFS is not supported for WIM mounting)." -ForegroundColor Red
Write-Host $_.Exception.Message -ForegroundColor Red
return $false
}
Expand Down Expand Up @@ -798,6 +904,18 @@ function Copy-2023BootBins {
Write-Dbg-Host "Removing [$global:Temp_Media_To_Update_Path\efi\microsoft\boot\fonts_ex]"
Remove-Item -Path $global:Temp_Media_To_Update_Path"\efi\microsoft\boot\fonts_ex" -Recurse -Force -ErrorAction stop | Out-Null

# Copy boot.stl from the mounted boot.wim to the staged media if not already present
$bootStlSource = $bootWimMount + "\Windows\Boot\EFI\boot.stl"
$bootStlDest = $global:Temp_Media_To_Update_Path + "\EFI\Microsoft\Boot\boot.stl"
if (-not (Test-Path -Path $bootStlSource)) {
Write-Dbg-Host "[boot.stl] not found in mounted boot.wim at [$bootStlSource]. Skipping."
} elseif (Test-Path -Path $bootStlDest) {
Write-Dbg-Host "[boot.stl] already exists at [$bootStlDest]. Preserving existing file."
} else {
Write-Dbg-Host "Copying [$bootStlSource] to [$bootStlDest]"
Copy-Item -Path $bootStlSource -Destination $bootStlDest -Force -ErrorAction stop | Out-Null
}

} catch {
Write-Host "$_" -ForegroundColor Red
return $false
Expand Down Expand Up @@ -843,8 +961,8 @@ function Create-ISOMedia {
Write-Dbg-Host "Running [$global:oscdimg_exe $runCommand]"
try {

# strip the file name from $ISOPath
$isoDirPath = $ISOPath.Substring(0, $ISOPath.LastIndexOf("\"))
# Extract the directory portion of $ISOPath
$isoDirPath = Split-Path -Parent $ISOPath

# Make sure ISO path is valid or the call to oscdimg.exe will fail
if (-not (Test-Path $isoDirPath)) {
Expand Down Expand Up @@ -937,11 +1055,16 @@ $global:WIM_Mount_Path = $null
$global:ISO_Mount_Path = $null
$global:ISO_Label = $null
$global:oscdimg_exe = $null
$global:oscdimg_known_hashes = @{
"AMD64" = "ABCD07318EBD8CDBE274B46C9DE78820DCA9709D558CDBC1F5D1730924264D07"
"ARM64" = "CDAE3649F6A6DE45F50A0B5FB5E2BBC098503B9EEFB1AE6A398FC955B434F579"
"x86" = "85AC2DDD96239D037560E5336727F9A8BE2B902734B9DD88264DD7DB5612EFB9"
}
$global:Dbg_Pause = $false
$global:Dbg_Output = $DebugOn

try {
Write-Host "`r`n`r`nMicrosoft 'Windows UEFI CA 2023' Media Update Script - Version 1.3`r`n" -ForegroundColor DarkYellow
Write-Host "`r`n`r`nMicrosoft 'Windows UEFI CA 2023' Media Update Script - Version 1.4`r`n" -ForegroundColor DarkYellow

# First validate that the required tools/environment exist
$result = Validate-Parameters -TargetType $TargetType -ISOPath $ISOPath -USBDrive $USBDrive -NewMediaPath $NewMediaPath -FileSystem $FileSystem -StagingDir $StagingDir
Expand Down