# Lay out the MSVC ARM64 cross toolchain locally, without the VS Installer. # ASCII only - PS 5.1 reads BOM-less files as ANSI. # # WHY: the VS Installer on some machines refuses every unattended `modify` - it insists on # updating itself first and exits with "Status changed to UpdateAvailable". Six different # invocations (setup.exe modify/update, vs_installer.exe, --noUpdateInstaller, --quiet, # --passive) all stopped there. The same payloads are plain .vsix (zip) files in the VS # catalog with direct download URLs, so we fetch and extract them into the repo instead. # # This changes nothing outside the repo: no admin, no registry, no Program Files. # If the ARM64 component is later installed properly, build-portable.ps1 prefers that. # # Usage: .\scripts\fetch-arm64-toolchain.ps1 [-Force] # Output: .toolchain\arm64\ (VC\Tools\MSVC\\{bin,lib}\...) param([switch]$Force) $ErrorActionPreference = "Stop" $root = Split-Path $PSScriptRoot -Parent $dest = Join-Path $root ".toolchain\arm64" # Two independent halves: MSVC ARM64 (compiler + CRT) and clang (assembles .S). # Each is skipped when already present, so re-running only fetches what is missing. $haveMsvc = $false if ((Test-Path $dest) -and -not $Force) { $haveMsvc = [bool](Get-ChildItem $dest -Recurse -Filter "cl.exe" -ErrorAction SilentlyContinue | Where-Object { $_.FullName -match "Hostx64\\arm64" } | Select-Object -First 1) } $cache = Join-Path $root ".toolchain\.cache" New-Item -ItemType Directory -Path $cache -Force | Out-Null # --- resolve packages from the VS catalog on disk --------------------------- if ($haveMsvc) { Write-Host "[arm64] msvc already present - skipping" } else { $chans = Join-Path $env:LOCALAPPDATA "Microsoft\VisualStudio\Packages\_Channels" if (-not (Test-Path $chans)) { throw "VS catalog not found - is Visual Studio installed?" } $catalog = Get-ChildItem $chans -Recurse -Filter "catalog.json" | Sort-Object Length -Descending | Select-Object -First 1 if (-not $catalog) { throw "catalog.json not found under $chans" } $vcRoot = "C:\Program Files\Microsoft Visual Studio\18\Community\VC\Tools\MSVC" if (-not (Test-Path $vcRoot)) { $vsw = "${env:ProgramFiles(x86)}\Microsoft Visual Studio\Installer\vswhere.exe" $vcRoot = Join-Path (& $vsw -latest -products * -property installationPath) "VC\Tools\MSVC" } $toolset = (Get-ChildItem $vcRoot -Directory | Sort-Object Name | Select-Object -Last 1).Name $minor = ($toolset -split '\.')[0..1] -join '.' Write-Host "[arm64] host toolset $toolset (family $minor) - matching ARM64 payloads" # Parsing a 18MB json with ConvertFrom-Json is slow in PS 5.1; node is already a dependency. $listScript = @' const fs=require('fs'); const cat=JSON.parse(fs.readFileSync(process.argv[2],'utf8')); const minor=process.argv[3].replace('.','\\.'); // 순서가 곧 우선순위다 - 뒤 패키지는 '없는 파일만' 채운다(아래 fill-only 참고). // Tools : cl/link/lib (Hostx64 -> ARM64) // CRT.Desktop : libcmt, libvcruntime, libcpmt ... // CRT.Store : oldnames.lib 이 여기에만 있다(데스크톱 패키지에 빠져 있어 링크가 LNK1104로 죽는다) const want=[ new RegExp(`^Microsoft\\.VC\\.${minor}\\.Tools\\.HostX64\\.TargetARM64\\.base$`,'i'), new RegExp(`^Microsoft\\.VC\\.${minor}\\.CRT\\.ARM64\\.Desktop\\.base$`,'i'), new RegExp(`^Microsoft\\.VC\\.${minor}\\.CRT\\.ARM64\\.Store\\.base$`,'i'), ]; const seen=new Set(); const out=[]; for(const re of want){ for(const p of cat.packages){ const id=p.id||''; if(!re.test(id)||seen.has(id)) continue; for(const pl of (p.payloads||[])){ seen.add(id); out.push({id,url:pl.url,size:pl.size,sha256:pl.sha256}); break; } } } process.stdout.write(JSON.stringify(out)); '@ $tmpJs = Join-Path $env:TEMP "vs-arm-list.js" Set-Content -Path $tmpJs -Value $listScript -Encoding ascii $pkgs = & node $tmpJs $catalog.FullName $minor | ConvertFrom-Json if (-not $pkgs -or $pkgs.Count -lt 2) { throw "ARM64 payloads for $minor not found in the catalog" } # --- download + extract ----------------------------------------------------- New-Item -ItemType Directory -Path $dest -Force | Out-Null foreach ($p in $pkgs) { $file = Join-Path $cache "$($p.id).vsix" if (-not (Test-Path $file) -or (Get-Item $file).Length -ne $p.size) { Write-Host ("[arm64] download {0} ({1:N1} MB)" -f $p.id, ($p.size / 1MB)) Invoke-WebRequest -Uri $p.url -OutFile $file -UseBasicParsing } # Payloads are signed; verify before extracting anything into the repo. $got = (Get-FileHash $file -Algorithm SHA256).Hash if ($p.sha256 -and $got -ne $p.sha256.ToUpper()) { Remove-Item $file -Force throw "sha256 mismatch for $($p.id) - download discarded" } Write-Host "[arm64] extract $($p.id)" # .vsix is a zip; the useful tree is under Contents\. # Expand-Archive refuses anything not named .zip, so go through the .NET API. $stage = Join-Path $cache "x_$($p.id)" if (Test-Path $stage) { Remove-Item $stage -Recurse -Force } Add-Type -AssemblyName System.IO.Compression.FileSystem [System.IO.Compression.ZipFile]::ExtractToDirectory($file, $stage) $contents = Join-Path $stage "Contents" if (-not (Test-Path $contents)) { throw "unexpected vsix layout: $stage" } # **Fill only, never overwrite.** The Store package carries its own store/uwp flavour of # libcmt & friends next to the one file we actually need from it (oldnames.lib); copying # over the Desktop CRT would silently swap the runtime out from under the build. $prefix = (Resolve-Path $contents).Path.TrimEnd('\') + '\' foreach ($src in Get-ChildItem $contents -Recurse -File) { $rel = $src.FullName.Substring($prefix.Length) $dst = Join-Path $dest $rel if (Test-Path $dst) { continue } New-Item -ItemType Directory -Path (Split-Path $dst -Parent) -Force | Out-Null Copy-Item $src.FullName $dst } Remove-Item $stage -Recurse -Force } $cl = Get-ChildItem $dest -Recurse -Filter "cl.exe" -ErrorAction SilentlyContinue | Where-Object { $_.FullName -match "Hostx64\\arm64" } | Select-Object -First 1 if (-not $cl) { throw "cl.exe for Hostx64/arm64 not found after extraction" } Write-Host "[arm64] msvc ready: $($cl.FullName)" } # --- clang ----------------------------------------------------------------- # cl.exe cannot assemble the .S files that aws-lc-sys and ring ship for aarch64-windows # (it prints "unrecognized source file type" and SKIPS them, so the link fails on a missing # .o). Both crates expect a clang-compatible driver there. We use the plain tarball rather # than LLVM's installer: the installer asks for elevation, and on this machine every # elevation path is what pushed us to this script in the first place. $llvmDir = Join-Path $root ".toolchain\llvm" $clangCl = Join-Path $llvmDir "bin\clang-cl.exe" if ((Test-Path $clangCl) -and -not $Force) { Write-Host "[arm64] clang already present: $clangCl" return } Write-Host "[arm64] resolving latest LLVM release" $rel = Invoke-RestMethod "https://api.github.com/repos/llvm/llvm-project/releases/latest" ` -Headers @{ "User-Agent" = "archive-build" } $asset = $rel.assets | Where-Object { $_.name -match "^clang\+llvm-.*-x86_64-pc-windows-msvc\.tar\.xz$" } | Select-Object -First 1 if (-not $asset) { throw "no x86_64-pc-windows-msvc tarball in LLVM release $($rel.tag_name)" } $tar = Join-Path $cache $asset.name if (-not (Test-Path $tar) -or (Get-Item $tar).Length -ne $asset.size) { Write-Host ("[arm64] download {0} ({1:N0} MB) - this one is large" -f $asset.name, ($asset.size / 1MB)) # Invoke-WebRequest buffers the whole body in memory on PS 5.1; use the streaming client. $wc = New-Object System.Net.WebClient $wc.Headers.Add("User-Agent", "archive-build") $wc.DownloadFile($asset.browser_download_url, $tar) } Write-Host "[arm64] extract clang (bin + builtin headers only)" $stage = Join-Path $cache "llvm_x" if (Test-Path $stage) { Remove-Item $stage -Recurse -Force } New-Item -ItemType Directory -Path $stage -Force | Out-Null # Windows ships bsdtar, which reads .tar.xz. Pull only what the build needs - the full # tree is ~2.5GB and we want clang-cl, clang, and the compiler's builtin headers. & tar -xf $tar -C $stage --strip-components=1 "*/bin/clang*.exe" "*/bin/llvm-lib.exe" "*/lib/clang/*" if ($LASTEXITCODE -ne 0) { throw "tar extraction failed" } if (Test-Path $llvmDir) { Remove-Item $llvmDir -Recurse -Force } New-Item -ItemType Directory -Path (Split-Path $llvmDir -Parent) -Force | Out-Null Move-Item $stage $llvmDir if (-not (Test-Path $clangCl)) { throw "clang-cl.exe missing after extraction" } Write-Host "[arm64] clang ready: $clangCl"