<# .SYNOPSIS Downloads Windows 10/11 ISOs from the MCT ESD catalog proxy. .DESCRIPTION Supported modes: Win11ARM64Latest - Latest stable Win11 ARM64 ISO. Win11x64Latest - Latest stable Win11 x64 ISO. Win10ARM64Latest - Latest stable Win10 ARM64 ISO. Win10x64Latest - Latest stable Win10 x64 ISO. Win11ARM64RPI - Win11 ARM64 build 22631 (last build with broad ARMv8.0 support, for Raspberry Pi). All modes use the MCT catalog proxy at vesperos.chillcraft.me. Pass -Build to pin to a specific build substring (e.g. "26100", "22631"). .PARAMETER Mode Which image to download (see above). .PARAMETER OutputDir Directory to save the output ISO (default: current directory). .PARAMETER Language Full language name e.g. "English (United States)", or short code e.g. "en-us". .PARAMETER Build Optional build substring filter (matched against ESD filename). E.g. "26100" or "22631". .PARAMETER Edition Windows edition to extract (default: Professional). .PARAMETER Edition Windows edition to extract (default: Professional). .PARAMETER ImageIndex Override which ESD index to use for install.esd. Pro is auto-selected if omitted. .PARAMETER EsdOnly Download and verify the ESD only - skip ISO assembly. Output is the raw .esd file. .EXAMPLE .\utils\Get-WindowsISO.ps1 -Mode Win11x64Latest -OutputDir C:\ISOs .EXAMPLE .\utils\Get-WindowsISO.ps1 -Mode Win11ARM64RPI -OutputDir C:\ISOs -Language "en-us" .EXAMPLE .\utils\Get-WindowsISO.ps1 -Mode Win10x64Latest -OutputDir C:\ISOs -Build "19045" .EXAMPLE .\utils\Get-WindowsISO.ps1 -Mode Win11x64Latest -OutputDir C:\ISOs -Language "French" #> param( [Parameter(Mandatory)] [ValidateSet("Win11ARM64Latest", "Win11x64Latest", "Win10ARM64Latest", "Win10x64Latest", "Win11ARM64RPI")] [string]$Mode, [string]$OutputDir = ".", [string]$Language = "English (United States)", [string]$Build, [string]$Edition = "Professional", [string]$ImageIndex, [switch]$EsdOnly # Download and verify the ESD only - skip ISO assembly ) Set-StrictMode -Off $ErrorActionPreference = "Stop" # Win11ARM64RPI pins to build 22631 (last Win11 release with broad ARMv8.0 support) if ($Mode -eq "Win11ARM64RPI" -and -not $Build) { $Build = "22631" } # Expand-WindowsImage and dism /Export-Image require elevation; -EsdOnly (download only) does not. if (-not $EsdOnly) { $principal = New-Object Security.Principal.WindowsPrincipal([Security.Principal.WindowsIdentity]::GetCurrent()) if (-not $principal.IsInRole([Security.Principal.WindowsBuiltInRole]::Administrator)) { Write-Host "Restarting as administrator (required for DISM/image operations)..." $argStr = "-NoProfile -ExecutionPolicy Bypass -File `"$PSCommandPath`" -Mode `"$Mode`" -OutputDir `"$OutputDir`" -Language `"$Language`"" if ($Build) { $argStr += " -Build `"$Build`"" } if ($Edition) { $argStr += " -Edition `"$Edition`"" } if ($ImageIndex) { $argStr += " -ImageIndex `"$ImageIndex`"" } Start-Process PowerShell -ArgumentList $argStr -Verb RunAs exit } } # ----------------------------------------------------------------- # Helpers # ----------------------------------------------------------------- function Write-Status { param([string]$Msg) Write-Host "`n==> $Msg" -ForegroundColor Cyan } function Write-Step { param([string]$Msg) Write-Host " - $Msg" } function Get-OscdimgPath { foreach ($hive in @("HKLM:\SOFTWARE\WOW6432Node\Microsoft\Windows Kits\Installed Roots", "HKLM:\SOFTWARE\Microsoft\Windows Kits\Installed Roots")) { $root = (Get-ItemProperty $hive -ErrorAction SilentlyContinue)."KitsRoot10" if ($root) { $p = Join-Path ($root.TrimEnd('\')) "Assessment and Deployment Kit\Deployment Tools\$($Env:PROCESSOR_ARCHITECTURE)\Oscdimg\oscdimg.exe" if (Test-Path $p) { return $p } } } $local = Join-Path $PSScriptRoot "..\oscdimg.exe" if (Test-Path $local) { return (Resolve-Path $local).Path } Write-Step "Downloading oscdimg.exe..." Invoke-WebRequest -Uri "https://msdl.microsoft.com/download/symbols/oscdimg.exe/3D44737265000/oscdimg.exe" ` -OutFile $local -UseBasicParsing -ErrorAction Stop return (Resolve-Path $local).Path } function Confirm-Hash { param([string]$Path, [string]$Expected, [ValidateSet("SHA1","SHA256")][string]$Algo = "SHA1") $actual = (Get-FileHash -Path $Path -Algorithm $Algo).Hash if ($actual -ne $Expected.ToUpper()) { Write-Warning "Hash mismatch for $(Split-Path $Path -Leaf)" Write-Warning " Expected : $Expected" Write-Warning " Actual : $actual" return $false } return $true } $LanguageMap = [ordered]@{ "Arabic" = "ar-sa" "Bulgarian" = "bg-bg" "Chinese (Simplified)" = "zh-cn" "Chinese (Traditional)" = "zh-tw" "Croatian" = "hr-hr" "Czech" = "cs-cz" "Danish" = "da-dk" "Dutch" = "nl-nl" "English (United States)" = "en-us" "English (United Kingdom)" = "en-gb" "Estonian" = "et-ee" "Finnish" = "fi-fi" "French" = "fr-fr" "French (Canada)" = "fr-ca" "German" = "de-de" "Greek" = "el-gr" "Hebrew" = "he-il" "Hungarian" = "hu-hu" "Indonesian" = "id-id" "Italian" = "it-it" "Japanese" = "ja-jp" "Korean" = "ko-kr" "Latvian" = "lv-lv" "Lithuanian" = "lt-lt" "Norwegian" = "nb-no" "Polish" = "pl-pl" "Portuguese (Brazil)" = "pt-br" "Portuguese (Portugal)" = "pt-pt" "Romanian" = "ro-ro" "Russian" = "ru-ru" "Serbian (Latin)" = "sr-latn-rs" "Slovak" = "sk-sk" "Slovenian" = "sl-si" "Spanish" = "es-es" "Spanish (Mexico)" = "es-mx" "Swedish" = "sv-se" "Thai" = "th-th" "Turkish" = "tr-tr" "Ukrainian" = "uk-ua" } function Resolve-LangCode { param([string]$Lang) if ($Lang -match '^[a-zA-Z]{2,3}(-[a-zA-Z0-9]{2,8}){0,2}$') { return $Lang.ToLower() } foreach ($key in $LanguageMap.Keys) { if ($key -ieq $Lang) { return $LanguageMap[$key] } } Write-Error "Unrecognized language '$Lang'. Use a full name like 'English (United States)' or short code like 'en-us'." Write-Host "Available languages:" $LanguageMap.GetEnumerator() | ForEach-Object { Write-Host " $($_.Value) ($($_.Key))" } exit 1 } # ----------------------------------------------------------------- # MCT ESD catalog proxy (Win10/Win11, latest or pinned build) # Proxy API: https://vesperos.chillcraft.me/windows/esd.php # ?os=10|11 &arch=x64|arm64|x86 &edition=Professional &lang=en-us # &build=NNNNN (optional substring match on filename) # ----------------------------------------------------------------- function Get-WindowsFromMCT { param( [string]$OutDir, [string]$Lang, [string]$Arch, # "ARM64" or "x64" [string]$OsVer, # "10" or "11" [string]$ImgIndex, [string]$EditionFilter = "Professional", [string]$BuildFilter, [switch]$EsdOnly ) $langCode = Resolve-LangCode $Lang $apiArch = $Arch.ToLower() # proxy uses x64/arm64 Write-Status "Downloading Windows $OsVer $Arch latest ($langCode) via MCT catalog" $apiUrl = "https://vesperos.chillcraft.me/windows/esd.php?os=$OsVer&arch=$apiArch&edition=$EditionFilter&lang=$langCode" if ($BuildFilter) { $apiUrl += "&build=$BuildFilter" } Write-Step "Querying: $apiUrl" try { $raw = (Invoke-WebRequest -Uri $apiUrl -UseBasicParsing -ErrorAction Stop).Content } catch { Write-Error "MCT catalog fetch failed: $($_.Exception.Message)" exit 1 } if (-not ($raw -match '')) { Write-Error "No ESD found for Windows $OsVer $Arch $langCode (edition=$EditionFilter$(if ($BuildFilter) { ", build=$BuildFilter" }))." exit 1 } # If multiple File nodes returned, take the last one (newest build tends to be last) $allMatches = [regex]::Matches($raw, '(?s).*?') $fileBlock = if ($allMatches.Count -gt 0) { $allMatches[$allMatches.Count - 1].Value } else { $raw } $esdUrl = [regex]::Match($fileBlock, '\s*(.*?)\s*').Groups[1].Value.Trim() $sha1 = [regex]::Match($fileBlock, '\s*(.*?)\s*').Groups[1].Value.Trim() $sizeStr = [regex]::Match($fileBlock, '\s*(.*?)\s*').Groups[1].Value.Trim() $fileName = [regex]::Match($fileBlock, '\s*(.*?)\s*').Groups[1].Value.Trim() if (-not $esdUrl) { Write-Error "Could not parse ESD URL from catalog response." exit 1 } Write-Step "File : $fileName" if ($sizeStr) { Write-Step "Size : $([math]::Round([long]$sizeStr / 1MB, 1)) MB" } if ($sha1) { Write-Step "SHA1 : $sha1" } $buildTag = [System.IO.Path]::GetFileNameWithoutExtension($fileName) $esdPath = Join-Path $OutDir "$buildTag.esd" if (Test-Path $esdPath) { Write-Step "Checking existing ESD..." if ($sha1 -and (Confirm-Hash -Path $esdPath -Expected $sha1 -Algo SHA1)) { Write-Step "Existing ESD valid - skipping download." } else { Write-Step "ESD invalid or unverified - re-downloading..." Remove-Item $esdPath -Force Invoke-WebRequest -Uri $esdUrl -OutFile $esdPath -UseBasicParsing -ErrorAction Stop } } else { Write-Step "Downloading ESD..." Invoke-WebRequest -Uri $esdUrl -OutFile $esdPath -UseBasicParsing -ErrorAction Stop } if ($sha1) { Write-Step "Verifying ESD hash..." if (-not (Confirm-Hash -Path $esdPath -Expected $sha1 -Algo SHA1)) { Remove-Item $esdPath -Force Write-Error "ESD corrupt after download. Delete and retry." exit 1 } Write-Step "Hash OK." } else { Write-Step "No SHA1 in catalog response - skipping hash verification." } if ($EsdOnly) { Write-Status "ESD ready: $esdPath" return $esdPath } # Reuse the same ESD→ISO build logic as Get-WindowsFromWOR # MCT ESDs have the same structure: idx1=media skeleton, idx2=WinPE, idx3=Setup, idx4+=OS Write-Step "Scanning ESD indexes..." $allIndexes = Get-WindowsImage -ImagePath $esdPath foreach ($idx in $allIndexes) { Write-Step " [$($idx.ImageIndex)] $($idx.ImageName)" } if ($ImgIndex) { $installIdx = [int]$ImgIndex } else { $installIdx = ($allIndexes | Where-Object { $_.ImageName -match 'Windows 1[01] Pro$' } | Select-Object -First 1).ImageIndex if (-not $installIdx) { $installIdx = $allIndexes[-1].ImageIndex Write-Step "No Pro edition found - using last index: $installIdx" } else { Write-Step "Auto-selected Pro at index $installIdx" } } $stageDir = Join-Path $OutDir "isoextract_${buildTag}" if (Test-Path $stageDir) { Remove-Item $stageDir -Recurse -Force } New-Item -ItemType Directory -Path $stageDir | Out-Null Write-Status "Extracting setup media structure (index 1)..." Expand-WindowsImage -ImagePath $esdPath -Index 1 -ApplyPath $stageDir -ErrorAction Stop Write-Step "Setup structure extracted." $bootWim = Join-Path $stageDir "sources\boot.wim" if (Test-Path $bootWim) { Remove-Item $bootWim -Force } Write-Status "Building boot.wim (PE index 2 + Setup index 3)..." & dism /English /Export-Image "/SourceImageFile:$esdPath" "/SourceIndex:2" ` "/DestinationImageFile:$bootWim" /Compress:max /CheckIntegrity | Out-Null & dism /English /Export-Image "/SourceImageFile:$esdPath" "/SourceIndex:3" ` "/DestinationImageFile:$bootWim" /Compress:max /CheckIntegrity | Out-Null Write-Step "boot.wim built." $installEsd = Join-Path $stageDir "sources\install.esd" Write-Status "Extracting install image (index $installIdx)..." & dism /English /Export-Image "/SourceImageFile:$esdPath" "/SourceIndex:$installIdx" ` "/DestinationImageFile:$installEsd" /Compress:recovery /CheckIntegrity | Out-Null Write-Step "install.esd created." $noprompt = Join-Path $stageDir "efi\microsoft\boot\efisys_noprompt.bin" $efisys = Join-Path $stageDir "efi\microsoft\boot\efisys.bin" if (Test-Path $noprompt) { Copy-Item $noprompt $efisys -Force Write-Step "Replaced efisys.bin with noprompt variant." } $archTag = $Arch.ToLower() $label = "Win${OsVer}_${archTag}_${langCode}".ToUpper() -replace '[^A-Z0-9_]', '_' $isoOut = Join-Path $OutDir "win${OsVer}_${archTag}_${langCode}.iso" $OSCDIMG = Get-OscdimgPath $etfsboot = Join-Path $stageDir "boot\etfsboot.com" $bootData = if (Test-Path $etfsboot) { "2#p0,e,b$etfsboot#pEF,e,b$efisys" } else { Write-Warning "etfsboot.com not found - ISO will be EFI-only (no BIOS boot)." "1#pEF,e,b$efisys" } Write-Status "Creating ISO: $isoOut" & "$OSCDIMG" "-l$label" '-m' '-o' '-u2' '-udfver102' "-bootdata:$bootData" $stageDir $isoOut if ($LASTEXITCODE -ne 0) { Write-Error "oscdimg failed (exit $LASTEXITCODE)." exit 1 } Write-Step "Cleaning up..." Remove-Item $stageDir -Recurse -Force Remove-Item $esdPath -Force Write-Status "Done. ISO saved to: $isoOut" return $isoOut } # ----------------------------------------------------------------- # Entry point # ----------------------------------------------------------------- if (-not (Test-Path $OutputDir)) { New-Item -ItemType Directory -Path $OutputDir | Out-Null } $OutputDir = (Resolve-Path $OutputDir).Path $mctArgs = @{ OutDir = $OutputDir Lang = $Language ImgIndex = $ImageIndex EditionFilter = $Edition BuildFilter = $Build EsdOnly = $EsdOnly } switch ($Mode) { "Win11ARM64Latest" { Get-WindowsFromMCT @mctArgs -Arch "ARM64" -OsVer "11" } "Win11x64Latest" { Get-WindowsFromMCT @mctArgs -Arch "x64" -OsVer "11" } "Win10ARM64Latest" { Get-WindowsFromMCT @mctArgs -Arch "ARM64" -OsVer "10" } "Win10x64Latest" { Get-WindowsFromMCT @mctArgs -Arch "x64" -OsVer "10" } "Win11ARM64RPI" { Get-WindowsFromMCT @mctArgs -Arch "ARM64" -OsVer "11" } }