diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml new file mode 100644 index 0000000..8d72118 --- /dev/null +++ b/.github/workflows/build.yml @@ -0,0 +1,255 @@ +name: Build All Platforms + +on: + push: + branches: + - main + pull_request: + branches: + - main + # Callable so release.yml can reuse this exact matrix on a tag instead of + # duplicating it. The artifacts uploaded here are visible to the calling + # workflow's later jobs, since a reusable workflow shares the same run. + workflow_call: + +jobs: + + linux: + name: Linux Build (${{ matrix.arch }}, ${{ matrix.variant }}) + runs-on: ${{ matrix.runner }} + strategy: + fail-fast: false + matrix: + # One arch+variant per runner, matching the windows-x64 job's shape. + # CUDA is amd64-only (build_linux.sh header). + # + # cpu is musl and fully static; arm64-cpu cross-compiles from amd64. + # vulkan/cuda are glibc - the Vulkan/CUDA loaders the user installs are + # glibc binaries and a musl process cannot load them. arm64-vulkan + # therefore needs a native arm64 runner: there is no glibc cross + # toolchain in that image, and emulating the Rust build would be far + # too slow. + include: + - { arch: amd64, variant: cpu, runner: ubuntu-latest } + - { arch: amd64, variant: vulkan, runner: ubuntu-latest } + - { arch: amd64, variant: cuda, runner: ubuntu-latest } + - { arch: arm64, variant: cpu, runner: ubuntu-latest } + - { arch: arm64, variant: vulkan, runner: ubuntu-24.04-arm } + steps: + - uses: actions/checkout@v4 + + # ---------------------- + # build_linux.sh cross-compiles a full static ONNX Runtime + LLVM + # per arch inside Docker; that easily outgrows the ~14GB GitHub + # guarantees free on standard runners, so reclaim space taken up by + # preinstalled toolchains this build never touches before starting. + - name: Free up disk space + run: | + df -h / + sudo rm -rf /usr/share/dotnet /usr/local/lib/android /opt/ghc \ + /opt/hostedtoolcache/CodeQL /usr/local/.ghcup \ + /usr/local/share/boost "${AGENT_TOOLSDIRECTORY:-}" || true + docker image prune -af || true + df -h / + + - name: Build Linux (${{ matrix.arch }}, ${{ matrix.variant }}) + run: | + chmod +x build_linux.sh + ./build_linux.sh --arch ${{ matrix.arch }} --variant ${{ matrix.variant }} + + - name: Upload artifacts + uses: actions/upload-artifact@v4 + with: + name: linux-${{ matrix.arch }}-${{ matrix.variant }} + # cpu variants are packaged tarballs; the glibc GPU variants are a + # directory holding the binary plus the ONNX Runtime .so files. + path: | + dist/packages/*-linux-${{ matrix.arch }}-${{ matrix.variant }}* + dist/*-linux-${{ matrix.arch }}-${{ matrix.variant }}-glibc/** + + windows-x64: + name: Windows Build (x64, ${{ matrix.variant }}) + runs-on: windows-latest + # The CUDA variant compiles ORT's provider for five GPU architectures with + # -rdc=true, which ran past two hours and was killed mid-build. Trimming + # CMAKE_CUDA_ARCHITECTURES is the lever if this gets too slow or costly. + timeout-minutes: 360 + env: + # Keep in sync with $CUDA_VERSION in build_windows.ps1. + CUDA_VERSION: "13.3.0" + # CUDA installs into a directory named by major.minor (v12.3), NOT the + # full patch version, and the silent-install component names use the + # same major.minor form (nvcc_12.3). + CUDA_MM: "13.3" + strategy: + fail-fast: false + matrix: + variant: [cpu, vulkan, cuda] + steps: + - uses: actions/checkout@v4 + + - name: Prepare Directories + run: | + mkdir dist + mkdir packages + + # ---------------------- + # CUDA toolkit. + # + # This used to cache C:\ProgramData\chocolatey\lib\cuda, which holds + # chocolatey's package metadata - NOT the toolkit, which installs to + # C:\Program Files\NVIDIA GPU Computing Toolkit. So every run after the + # first got a cache hit, skipped the install, found no nvcc, and fell + # into build_windows.ps1's fallback network installer, which hangs. + # Cache the real install directory instead. + - name: Cache CUDA Toolkit + if: matrix.variant == 'cuda' + uses: actions/cache@v4 + id: cuda-cache + with: + path: C:\Program Files\NVIDIA GPU Computing Toolkit\CUDA\v${{ env.CUDA_MM }} + key: windows-cuda-${{ env.CUDA_VERSION }} + + - name: Install CUDA Toolkit (if not cached) + if: matrix.variant == 'cuda' && steps.cuda-cache.outputs.cache-hit != 'true' + shell: pwsh + run: | + $ErrorActionPreference = "Stop" + $ver = "${{ env.CUDA_VERSION }}" + $mm = "${{ env.CUDA_MM }}" + $root = "C:\Program Files\NVIDIA GPU Computing Toolkit\CUDA\v$mm" + # Local (self-contained) installer, not the network one: the network + # installer pulls components at install time and is what stalls in CI. + # CUDA 13.x dropped the driver-version suffix from the installer name. + $url = "https://developer.download.nvidia.com/compute/cuda/$ver/local_installers/cuda_${ver}_windows.exe" + $exe = "$env:RUNNER_TEMP\cuda_installer.exe" + Write-Host "Downloading $url" + Invoke-WebRequest -Uri $url -OutFile $exe -UseBasicParsing + # $args is an automatic variable in PowerShell; use our own name. + $comps = @("nvcc", "cudart", "cublas_dev", "curand_dev", "cufft_dev", + "cusparse_dev", "cusolver_dev", "thrust", + "visual_studio_integration") | ForEach-Object { "${_}_$mm" } + $installArgs = "-s " + ($comps -join " ") + Write-Host "Running installer: $installArgs" + $p = Start-Process -FilePath $exe -ArgumentList $installArgs -PassThru + if (-not $p.WaitForExit(45 * 60 * 1000)) { + $p.Kill() + throw "CUDA installer timed out after 45 minutes" + } + if ($p.ExitCode -ne 0) { throw "CUDA installer failed with exit code $($p.ExitCode)" } + if (-not (Test-Path "$root\bin\nvcc.exe")) { + Write-Host "Contents of C:\Program Files\NVIDIA GPU Computing Toolkit\CUDA:" + Get-ChildItem "C:\Program Files\NVIDIA GPU Computing Toolkit\CUDA" -ErrorAction SilentlyContinue | ForEach-Object { Write-Host " $($_.Name)" } + throw "nvcc.exe not found under $root" + } + + # CUDA 13 splits pieces the toolkit installer's component list does + # not cover into separate redistributables: crt/host_config.h (pulled + # in by cuda_runtime.h) and nvvm/bin/cicc.exe (nvcc's %CICC_PATH%). + # Without them nvcc fails with "Cannot open include file: + # 'crt/host_config.h'" and "The system cannot find the path + # specified". Overlay them from NVIDIA's manifest so the per-component + # versions stay correct. This lands in $root, so the cache keeps it. + $manifest = Invoke-RestMethod -UseBasicParsing ` + -Uri "https://developer.download.nvidia.com/compute/cuda/redist/redistrib_$ver.json" + foreach ($comp in @("cuda_crt", "libnvvm", "libnvptxcompiler", "libnvfatbin")) { + $rel = $manifest.$comp."windows-x86_64".relative_path + if (-not $rel) { throw "no windows-x86_64 payload for $comp in CUDA $ver manifest" } + Write-Host "overlaying $comp -> $rel" + $zip = "$env:RUNNER_TEMP\$comp.zip" + Invoke-WebRequest -UseBasicParsing -OutFile $zip ` + -Uri "https://developer.download.nvidia.com/compute/cuda/redist/$rel" + Expand-Archive -Path $zip -DestinationPath "$env:RUNNER_TEMP\$comp" -Force + # each archive holds one top-level dir laid out toolkit-relative + $top = (Get-ChildItem "$env:RUNNER_TEMP\$comp" -Directory)[0].FullName + Copy-Item -Recurse -Force "$top\*" "$root\" + } + foreach ($need in @("include\crt\host_config.h", "nvvm\bin\cicc.exe")) { + if (-not (Test-Path "$root\$need")) { throw "$need still missing after overlay" } + } + + # refreshenv only affects the shell of the step that runs it; every later + # step is a fresh process. Persist via GITHUB_ENV/GITHUB_PATH so + # build_windows.ps1 finds nvcc instead of trying to install CUDA itself. + - name: Expose CUDA to later steps + if: matrix.variant == 'cuda' + shell: pwsh + run: | + $root = "C:\Program Files\NVIDIA GPU Computing Toolkit\CUDA\v${{ env.CUDA_MM }}" + if (-not (Test-Path "$root\bin\nvcc.exe")) { throw "CUDA missing at $root" } + # The MSBuild CUDA targets that the VS integration installs resolve + # CudaToolkitDir from CUDA_PATH_V_. A silent component + # install never sets it, so nvcc is found but every .cu compile dies + # with "The CUDA Toolkit directory '' does not exist". + $vname = "CUDA_PATH_V" + ("${{ env.CUDA_MM }}" -replace '\.','_') + "CUDA_PATH=$root" | Out-File -Append -Encoding utf8 $env:GITHUB_ENV + "CUDAToolkit_ROOT=$root" | Out-File -Append -Encoding utf8 $env:GITHUB_ENV + "$vname=$root" | Out-File -Append -Encoding utf8 $env:GITHUB_ENV + "CudaToolkitDir=$root" | Out-File -Append -Encoding utf8 $env:GITHUB_ENV + Write-Host "set $vname=$root" + "$root\bin" | Out-File -Append -Encoding utf8 $env:GITHUB_PATH + & "$root\bin\nvcc.exe" --version + + - name: Prepare AI models + shell: cmd + run: | + set "KOKORO_CACHE=%USERPROFILE%\.cache\k" + mkdir "%KOKORO_CACHE%" + curl -L https://github.com/DavidValin/kokoro-micro/raw/main/models/0.bin -o "%KOKORO_CACHE%\0.bin" + curl -L https://github.com/DavidValin/kokoro-micro/raw/main/models/0.onnx -o "%KOKORO_CACHE%\0.onnx" + + # ---------------------- + # Build (Windows, ${{ matrix.variant }}) + # Each variant is its own job on its own fresh runner - no variant + # shares a runner/session with another, unlike when cpu/vulkan/cuda + # ran as sequential steps in one job. + + - name: Build Windows ${{ matrix.variant }} + shell: pwsh + run: ./build_windows.ps1 ${{ matrix.variant }} + + # ---------------------- + # Upload artifacts + - name: Upload ${{ matrix.variant }} Artifact + uses: actions/upload-artifact@v4 + with: + name: windows-x64-${{ matrix.variant }} + path: target-cross\${{ matrix.variant }} + + macos: + name: macOS Build (${{ matrix.arch }}) + runs-on: ${{ matrix.runner }} + strategy: + fail-fast: false + matrix: + include: + # arm64 only. macos-13 (the last Intel image) no longer schedules, + # and cross-compiling x86_64 from arm64 - which does work - dies at + # ort-sys: pyke ships no prebuilt ONNX Runtime for x86_64-apple-darwin. + # Restoring the Intel leg means building ORT from source on macOS. + # build_macos.sh still accepts --arch x86_64 for local builds. + - { arch: arm64, runner: macos-14 } + steps: + - uses: actions/checkout@v4 + + # ---------------------- + # build_macos.sh gates whisper-metal on arm64 (ggml's Metal backend is + # not useful on Intel GPUs) and builds the CPU path on x86_64. + # build.rs fetches the whisper/kokoro/supersonic2 models into $HOME + # itself, so no model-prep step is needed here. + - name: Build macOS (${{ matrix.arch }}) + run: | + chmod +x build_macos.sh + ./build_macos.sh --arch ${{ matrix.arch }} + + - name: Verify linkage and deployment target + run: | + bin=$(ls dist/vtmate-*-macos-${{ matrix.arch }}) + otool -L "$bin" + otool -l "$bin" | grep -A3 LC_BUILD_VERSION + + - name: Upload artifacts + uses: actions/upload-artifact@v4 + with: + name: macos-${{ matrix.arch }} + path: dist/vtmate-*-macos-${{ matrix.arch }} diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml new file mode 100644 index 0000000..1f12e57 --- /dev/null +++ b/.github/workflows/release.yml @@ -0,0 +1,104 @@ +name: Release + +# Triggers when a tag is pushed. A guard job verifies the tagged +# commit is reachable from the main branch before building. +on: + push: + tags: + - '*' + +permissions: + contents: write + +jobs: + guard: + name: Verify tag is on main + runs-on: ubuntu-latest + outputs: + ok: ${{ steps.check.outputs.ok }} + steps: + - uses: actions/checkout@v4 + with: + fetch-depth: 0 + - id: check + run: | + if git branch -r --contains "$GITHUB_REF" | grep -qE '(^|/)main$'; then + echo "ok=true" >> "$GITHUB_OUTPUT" + else + echo "Tag $GITHUB_REF is not reachable from main. Skipping release." >&2 + echo "ok=false" >> "$GITHUB_OUTPUT" + fi + + # Reuses build.yml rather than restating the matrix. Every platform quirk - + # the musl toolchain image, the CUDA component overlay, the prebuilt ONNX + # Runtime, the arm64 runner - lives in one place and cannot drift between + # the CI and release paths. + build: + name: Build + needs: guard + if: needs.guard.outputs.ok == 'true' + uses: ./.github/workflows/build.yml + + release: + name: Create release & upload assets + needs: [guard, build] + if: needs.guard.outputs.ok == 'true' && needs.build.result == 'success' + runs-on: ubuntu-latest + steps: + # No merge-multiple: each artifact keeps its own directory. The Windows + # CUDA variant ships DLLs alongside the exe, and flattening everything + # into one directory would collide those. + - name: Download all build artifacts + uses: actions/download-artifact@v4 + with: + path: artifacts + + - name: Show what was built + run: find artifacts -type f | sort + + - name: Package one asset per variant + run: | + set -euo pipefail + TAG="${GITHUB_REF_NAME}" + mkdir -p assets + + for dir in artifacts/*/; do + name="$(basename "$dir")" + + # The Linux jobs already produce .tar.gz + .sha256 via + # build_linux.sh's packaging step - take those as they are. + if find "$dir" -name '*.tar.gz' | grep -q .; then + find "$dir" \( -name '*.tar.gz' -o -name '*.sha256' \) \ + -exec cp {} assets/ \; + echo "packaged (pre-built archive): $name" + continue + fi + + # Windows: a directory holding the exe, plus the ONNX Runtime, + # cuDNN and cuBLAS DLLs for the CUDA variant. + if find "$dir" -name '*.exe' | grep -q .; then + ( cd "$dir" && zip -qr "${GITHUB_WORKSPACE}/assets/vtmate-${TAG}-${name}.zip" . ) + echo "packaged (zip): $name" + continue + fi + + # macOS: a bare binary. + tar -czf "assets/vtmate-${TAG}-${name}.tar.gz" -C "$dir" . + echo "packaged (tar): $name" + done + + echo "--- assets ---" + ls -lh assets + + - name: Fail if no assets were produced + run: | + count=$(find assets -type f | wc -l) + echo "asset count: $count" + [ "$count" -gt 0 ] || { echo "No assets to publish" >&2; exit 1; } + + - name: Create release and upload assets + uses: softprops/action-gh-release@v2 + with: + name: ${{ github.ref_name }} + generate_release_notes: true + files: assets/* diff --git a/.gitignore b/.gitignore index a265721..6eb656d 100644 --- a/.gitignore +++ b/.gitignore @@ -1,5 +1,7 @@ +assets/openblas* target target-cross dist .DS_Store -NEXT_STEPS.txt \ No newline at end of file +NEXT_STEPS.txt +deps diff --git a/Cargo.toml b/Cargo.toml index e942a5d..ed564ae 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -2,6 +2,7 @@ name = "vtmate" version = "0.4.3" edition = "2024" +build = "build.rs" authors = ["David Valin "] homepage = "https://github.com/DavidValin/vtmate" @@ -24,6 +25,7 @@ serde_json = "1" urlencoding = "2" url = "2" whisper-rs = { version = "0.15.1", default-features = false } +ort = { version = "2.0.0-rc.11", features = [ "std", "ndarray", "tls-rustls" ] } hound = "3" crossterm = "0.27" tokio = { version = "1", features = ["rt", "macros"] } @@ -50,11 +52,22 @@ once_cell = "1" flate2 = "1" tar = "0.4" - [features] -whisper-openblas = ["whisper-rs/openblas"] -whisper-vulkan = ["whisper-rs/vulkan"] -whisper-cuda = ["whisper-rs/cuda"] -whisper-hipblas = ["whisper-rs/hipblas"] -whisper-metal = ["whisper-rs/metal"] -whisper-logs = ["whisper-rs/log_backend", "whisper-rs/tracing_backend"] +default = [] +whisper-openblas = ["whisper-rs/openblas"] +whisper-vulkan = ["whisper-rs/vulkan"] +whisper-cuda = ["whisper-rs/cuda"] +whisper-hipblas = ["whisper-rs/hipblas"] +whisper-metal = ["whisper-rs/metal"] +whisper-logs = ["whisper-rs/log_backend", "whisper-rs/tracing_backend"] +ort-download-binaries = ["ort/download-binaries"] +ort-cuda = ["ort/cuda"] + +[profile.release] +opt-level = 3 +codegen-units = 1 +lto = "thin" +panic = "abort" +strip = "symbols" +incremental = false + diff --git a/build.rs b/build.rs index 2b80344..1fa8b10 100755 --- a/build.rs +++ b/build.rs @@ -181,6 +181,90 @@ fn init_expected_hashes() -> HashMap<&'static str, &'static str> { static EXPECTED_HASHES: Lazy> = Lazy::new(init_expected_hashes); fn main() { + + // ----------------------------- + // Optional: Link prebuilt Whisper/GGML/OpenBLAS if available + // ----------------------------- + if let Ok(lib_dir) = env::var("WHISPER_PREBUILT_LIB") { + println!("cargo:rerun-if-env-changed=WHISPER_PREBUILT_LIB"); + println!("cargo:rustc-link-search=native={}", lib_dir); + println!("cargo:rustc-link-lib=static=whisper"); + println!("cargo:rustc-link-lib=static=ggml"); + println!("cargo:rustc-link-lib=static=openblas"); + println!("cargo:rustc-link-lib=pthread"); + + let include_dir = Path::new(&lib_dir).join("..").join("include"); + println!("cargo:include={}", include_dir.display()); + } else { + println!( + "cargo:warning=WHISPER_PREBUILT_LIB not set, skipping prebuilt Whisper/GGML/OpenBLAS linking" + ); + } + + // ----------------------------- + // Link built eSpeak NG from PowerShell build + // ----------------------------- + if let Ok(espeak_dir) = env::var("ESPEAK_NG_DIR") { + println!("cargo:rerun-if-env-changed=ESPEAK_NG_DIR"); + + let espeak_lib_dir = Path::new(&espeak_dir).join("lib"); + println!( + "cargo:rustc-link-search=native={}", + espeak_lib_dir.display() + ); + println!("cargo:rustc-link-lib=static=espeak-ng"); + + let espeak_include_dir = Path::new(&espeak_dir).join("include"); + println!("cargo:include={}", espeak_include_dir.display()); + } else { + println!("cargo:warning=ESPEAK_NG_DIR not set, skipping prebuilt eSpeak NG linking"); + } + + // ----------------------------- + // Optionally link ONNX Runtime + // ----------------------------- + // Look for ONNX Runtime library location + if let Ok(ort_lib_dir) = env::var("ORT_LIB_LOCATION") { + let lib_path = Path::new(&ort_lib_dir); + + // Tell Cargo where to search for native libraries + println!("cargo:rustc-link-search=native={}", lib_path.display()); + + // Iterate over all library files in the directory + if cfg!(windows) { + // On Windows, link all .lib files statically + // for entry in fs::read_dir(lib_path).expect("Failed to read ORT_LIB_LOCATION") { + // let entry = entry.expect("Failed to read entry in ORT_LIB_LOCATION"); + // let path = entry.path(); + // if let Some(ext) = path.extension() { + // if ext == "lib" { + // let stem = path.file_stem().unwrap().to_string_lossy(); + // println!("cargo:rustc-link-lib=static={}", stem); + // } + // } + // } + } else if cfg!(unix) { + // On Unix/macOS, link all .a (static) or .so/.dylib (dynamic) files + // for entry in fs::read_dir(lib_path).expect("Failed to read ORT_LIB_LOCATION") { + // let entry = entry.expect("Failed to read entry in ORT_LIB_LOCATION"); + // let path = entry.path(); + // if let Some(ext) = path.extension() { + // match ext.to_str() { + // Some("a") => { + // let stem = path.file_stem().unwrap().to_string_lossy(); + // println!("cargo:rustc-link-lib=static={}", stem); + // } + // _ => {} + // } + // } + // } + } + + // Set include path for ONNX Runtime headers + let ort_include_dir = lib_path.join("..").join("include"); + println!("cargo:include={}", ort_include_dir.display()); + } + let out_dir = env::var("OUT_DIR").expect("OUT_DIR not set"); let is_release = env::var("PROFILE").unwrap_or_default() == "release"; let dest = Path::new(&out_dir).join("embedded"); diff --git a/build_linux.sh b/build_linux.sh index 59d1245..9ddc5dc 100755 --- a/build_linux.sh +++ b/build_linux.sh @@ -1,6 +1,37 @@ #!/usr/bin/env bash set -euo pipefail +# ========================================================== +# build_linux.sh - fully static Linux builds (musl) +# ========================================================== +# +# Builds vtmate for: +# arch: amd64, arm64 +# variant: cpu, vulkan, cuda (cuda is amd64-only) +# +# OpenBLAS is linked into every variant on every arch. +# +# Both arches cross-compile from a single linux/amd64 container using +# prebuilt musl.cc cross toolchains (x86_64-linux-musl-cross / +# aarch64-linux-musl-cross). Nothing runs under QEMU: the aarch64 gcc/g++ +# themselves are ordinary amd64 host binaries that happen to emit aarch64 +# code, so cross-compiling arm64 is exactly as fast as amd64 and doesn't +# need --platform=linux/arm64 or binfmt_misc at all. glslc (Vulkan shader +# compiler) is a build-time host tool too, so the same amd64-hosted glslc +# compiles shaders for both target arches; only its SPIR-V *output* (which +# is architecture-independent) ends up in the arm64 binary. +# +# Vulkan note: whisper-rs-sys links libvulkan as a normal (non-static) lib +# on Linux, same as vulkan-1.dll on Windows - the ICD loader model requires +# a runtime-resolved driver, so "fully static" Vulkan builds still carry +# that one dynamic dependency. That's expected and matches the existing +# Windows static-check policy (scripts/check-static.sh explicitly allows +# Vulkan/CUDA loaders through). +# +# This script is a from-scratch rewrite, not a copy of build_linux.sh - see +# the two build_linux__variants() functions below for exactly what +# each container does. + BIN_NAME="vtmate" PROJECT_ROOT="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" DIST_DIR="${PROJECT_ROOT}/dist" @@ -10,13 +41,14 @@ ESPEAK_ARCHIVE="${ASSETS_DIR}/espeak-ng-data.tar.gz" DO_PACKAGE=1 DOCKER_NO_CACHE=1 -SEL_ARCH="all" # amd64,arm64,all +SEL_ARCH="all" # amd64,arm64,all +SEL_VARIANT="all" # cpu,vulkan,cuda,all -# Linux variant toggles -WITH_CUDA="${WITH_CUDA:-1}" # amd64 only -WITH_ROCM="${WITH_ROCM:-0}" # amd64 only -LINUX_WITH_OPENBLAS="${LINUX_WITH_OPENBLAS:-1}" -LINUX_WITH_VULKAN="${LINUX_WITH_VULKAN:-1}" +# Linux variant toggles - on by default, so a bare invocation still produces +# cpu+vulkan+(cuda on amd64) in one pass. --variant overrides these. +WITH_CPU="${WITH_CPU:-1}" # amd64 + arm64 +WITH_CUDA="${WITH_CUDA:-1}" # amd64 only +WITH_VULKAN="${WITH_VULKAN:-1}" # amd64 + arm64 # Host cache mounts (Linux Docker) HOST_HOME="${HOME}" @@ -25,18 +57,25 @@ HOST_WHISPER_MODELS="${HOST_HOME}/.whisper-models" CONT_K_CACHE="/root/.cache/k" CONT_WHISPER_MODELS="/root/.whisper-models" +# ----------------------------- +# Helper functions +# ----------------------------- usage() { cat <<'USAGE' Usage: - ./build_linux.sh [--arch ] [--skip-package] [--cache|--no-cache] + ./build_linux.sh [--arch ] [--variant ] [--skip-package] [--cache|--no-cache] ---arch comma-separated: amd64,arm64,all +--arch comma-separated: amd64,arm64,all +--variant comma-separated: cpu,vulkan,cuda,all (cuda is amd64 only) + When given, it overrides the WITH_* env toggles below. CI uses + this to build one arch+variant per runner. Env: - WITH_CUDA=0|1 (amd64) default 1 - WITH_ROCM=0|1 (amd64) default 0 - LINUX_WITH_OPENBLAS=0|1 default 1 - LINUX_WITH_VULKAN=0|1 default 1 + WITH_CPU=0|1 (amd64 + arm64) default 1 + WITH_CUDA=0|1 (amd64 only) default 1 + WITH_VULKAN=0|1 (amd64 + arm64) default 1 + +OpenBLAS is always enabled, on every arch and every variant. USAGE } @@ -58,14 +97,31 @@ want_arch() { [[ "${SEL_ARCH}" == "all" ]] && return 0; list_has "${SEL_ARCH}" " while [[ $# -gt 0 ]]; do case "$1" in --arch) SEL_ARCH="$(normalize_list "${2-}")"; shift 2 ;; + --variant) SEL_VARIANT="$(normalize_list "${2-}")"; shift 2 ;; --skip-package) DO_PACKAGE=0; shift ;; --cache) DOCKER_NO_CACHE=0; shift ;; --no-cache) DOCKER_NO_CACHE=1; shift ;; -h|--help) usage; exit 0 ;; - *) echo "Unknown arg: $1"; usage; exit 1 ;; + *) + echo "⚠ Ignoring unknown arg: $1" + shift + ;; esac done +# --variant, when given, is authoritative: it replaces the WITH_* toggles so +# one runner can build exactly one arch+variant. +if [[ "${SEL_VARIANT}" != "all" ]]; then + WITH_CPU=0; WITH_VULKAN=0; WITH_CUDA=0 + list_has "${SEL_VARIANT}" cpu && WITH_CPU=1 + list_has "${SEL_VARIANT}" vulkan && WITH_VULKAN=1 + list_has "${SEL_VARIANT}" cuda && WITH_CUDA=1 + if [[ "${WITH_CPU}${WITH_VULKAN}${WITH_CUDA}" == "000" ]]; then + echo "ERROR: --variant '${SEL_VARIANT}' selected no known variant (cpu,vulkan,cuda)" + exit 1 + fi +fi + VERSION="$( awk -F\" ' $1 ~ /^[[:space:]]*version[[:space:]]*=[[:space:]]*/ { print $2; exit } @@ -77,22 +133,22 @@ mkdir -p "${DIST_DIR}" "${PKG_DIR}" "${PROJECT_ROOT}/target-cross" "${ASSETS_DIR mkdir -p "${HOST_K_CACHE}" "${HOST_WHISPER_MODELS}" echo "Version: ${VERSION}" -echo "Linux: arch=${SEL_ARCH}" -echo "Linux amd64: WITH_CUDA=${WITH_CUDA} WITH_ROCM=${WITH_ROCM}" -echo "Linux variants: OPENBLAS=${LINUX_WITH_OPENBLAS} VULKAN=${LINUX_WITH_VULKAN}" -echo "Cache mounts:" -echo " ${HOST_K_CACHE} -> ${CONT_K_CACHE}" -echo " ${HOST_WHISPER_MODELS} -> ${CONT_WHISPER_MODELS}" - -# Features -FEATURES_COMMON="whisper-logs" +echo "Linux: arch=${SEL_ARCH} variant=${SEL_VARIANT}" +echo "WITH_CPU=${WITH_CPU} WITH_CUDA=${WITH_CUDA} (amd64 only) WITH_VULKAN=${WITH_VULKAN}" +echo "OpenBLAS: always ON" + +# Features - OpenBLAS is part of every variant's feature set, unconditionally. +FEATURES_COMMON="whisper-openblas" FEATURES_CPU="${FEATURES_COMMON}" -FEATURES_OPENBLAS="${FEATURES_COMMON},whisper-openblas" FEATURES_VULKAN="${FEATURES_COMMON},whisper-vulkan" -FEATURES_CUDA="${FEATURES_COMMON},whisper-cuda" -FEATURES_ROCM="${FEATURES_COMMON},whisper-hipblas" +# ort-cuda registers ONNX Runtime's CUDA execution provider on the Rust +# side; without it ORT inference stays on CPU even though the C++ build +# sets -Donnxruntime_USE_CUDA=ON. +FEATURES_CUDA="${FEATURES_COMMON},whisper-cuda,ort-cuda" +# ----------------------------- # Packaging helpers +# ----------------------------- sha256_file() { local file="$1" out="$2" if command -v shasum >/dev/null 2>&1; then @@ -112,7 +168,8 @@ sha256_file() { make_tgz() { local src="$1" tgz="$2"; tar -C "$(dirname "$src")" -czf "$tgz" "$(basename "$src")"; } package_one() { local src="$1" - [[ -f "$src" ]] || return 0 + # -e so a directory artifact is packaged too; make_tgz handles both. + [[ -e "$src" ]] || return 0 local base tgz sha base="$(basename "$src")" tgz="${PKG_DIR}/${base}.tar.gz" @@ -121,7 +178,9 @@ package_one() { sha256_file "$tgz" "$sha" } +# ----------------------------- # Docker helpers +# ----------------------------- docker_ok=0 command -v docker >/dev/null 2>&1 && docker_ok=1 can_run_amd64() { docker run --rm --platform=linux/amd64 alpine:3.19 uname -m >/dev/null 2>&1; } @@ -141,12 +200,10 @@ ensure_espeak_data_archive() { echo "ERROR: Docker not found and ${ESPEAK_ARCHIVE} is missing." exit 1 fi - local tmp img df tmp="$(mktemp -d)" df="${tmp}/Dockerfile.espeak.asset" - img="local/${BIN_NAME}-espeak-asset:${VERSION}-$$" - + img="local/${BIN_NAME}-espeak-asset:cache" cat > "$df" <<'DOCKERFILE' FROM ubuntu:noble ENV DEBIAN_FRONTEND=noninteractive @@ -158,10 +215,8 @@ DOCKERFILE local build_args=(--pull) [[ "${DOCKER_NO_CACHE}" -eq 1 ]] && build_args+=(--no-cache) - docker build "${build_args[@]}" --platform=linux/amd64 -f "$df" -t "$img" "$tmp" - rm -f "${ESPEAK_ARCHIVE}" docker run --rm --platform=linux/amd64 \ -v "${ASSETS_DIR}:/out" -w /out \ "$img" \ @@ -175,75 +230,405 @@ DOCKERFILE docker image rm -f "$img" >/dev/null 2>&1 || true rm -rf "$tmp" >/dev/null 2>&1 || true - [[ -f "${ESPEAK_ARCHIVE}" ]] || { echo "ERROR: failed to generate ${ESPEAK_ARCHIVE}"; exit 1; } echo "✔ Generated: ${ESPEAK_ARCHIVE}" } +# ----------------------------- +# Linux copy helper +# ----------------------------- ARTIFACTS=() -add_artifact() { [[ -f "$1" ]] && ARTIFACTS+=("$1"); } +# -e, not -f: the glibc GPU variants produce a directory (binary + the ONNX +# Runtime .so files beside it), not a single file. The explicit `return 0` also +# stops a non-match from tripping `set -e` and aborting the whole build. +add_artifact() { [[ -e "$1" ]] && ARTIFACTS+=("$1"); return 0; } linux_copy_out() { local arch="$1" target="$2" variant="$3" local src_dir="${PROJECT_ROOT}/target-cross/linux-${arch}-${variant}/${target}/release" local out="${DIST_DIR}/${BIN_NAME}-${VERSION}-linux-${arch}-${variant}" + [[ -f "${src_dir}/${BIN_NAME}" ]] || return 0 cp "${src_dir}/${BIN_NAME}" "$out" chmod +x "$out" || true add_artifact "$out" echo "✔ Built: $out" } -build_linux_amd64_docker_variants() { +# ========================================================== +# AMD64 (x86_64-unknown-linux-musl) - cpu, vulkan, cuda +# ========================================================== +build_linux_amd64_variants() { [[ "$docker_ok" -eq 1 ]] || { echo "Skipping linux/amd64: docker not found."; return 0; } [[ "${FORCE_AMD64_DOCKER}" -eq 1 ]] || { echo "Skipping linux/amd64: cannot run linux/amd64 containers."; return 0; } local tmp df img CACHE_BUST tmp="$(mktemp -d)" df="${tmp}/Dockerfile.linux.amd64" - img="local/${BIN_NAME}-linux-amd64:${VERSION}-$$" + img="local/${BIN_NAME}-linux-amd64:cache-cuda${WITH_CUDA}" CACHE_BUST="$(date +%s)" - cat > "$df" < "$df" <<'DOCKERFILE' +# musl cross toolchain, taken from the official musl.cc container image +# rather than downloaded from musl.cc: that host drops GitHub Actions runner +# IPs (every fetch attempt hit the wget timeout), and hosting the tarballs on +# this repo's releases is not wanted. Same GCC 11.2.1 build, digest-pinned. +FROM muslcc/x86_64:x86_64-linux-musl@sha256:173c042a23a544defa3364d4472b30b36af1d827a74cd8dab7683b8500004334 AS musltc + FROM ubuntu:noble ENV DEBIAN_FRONTEND=noninteractive -ARG CACHE_BUST=${CACHE_BUST} +ARG CACHE_BUST +ARG WITH_CUDA=0 + +ENV WITH_CUDA=${WITH_CUDA} +# ---------------------------------------------------------- +# Build dependencies +# ---------------------------------------------------------- RUN apt-get update && apt-get install -y --no-install-recommends \ - ca-certificates curl git xz-utils \ - build-essential pkg-config \ - cmake ninja-build \ - clang libclang-dev llvm-dev \ - perl \ - libssl-dev \ - libasound2-dev \ - libxdo-dev \ - libx11-dev \ - libopenblas-dev \ - libvulkan-dev vulkan-tools vulkan-utility-libraries-dev \ - spirv-tools glslang-tools \ - && rm -rf /var/lib/apt/lists/* + build-essential pkg-config curl wget ca-certificates git \ + libclang-dev clang \ + cmake python3 python3-pip perl autoconf automake libtool \ + gfortran zlib1g-dev libbz2-dev liblzma-dev libssl-dev \ + glslc \ +&& rm -rf /var/lib/apt/lists/* + +RUN if [ "$WITH_CUDA" = "1" ]; then \ + apt-get update && apt-get install -y --no-install-recommends nvidia-cuda-toolkit && \ + rm -rf /var/lib/apt/lists/* ; \ + fi +# cuDNN: required by the ONNX Runtime CUDA execution provider, and NOT part of +# the CUDA toolkit (apt installs neither). ORT locates it via $CUDNN_PATH +# (cmake/external/cuDNN.cmake), expecting include/ and lib/ underneath. +# The _cuda12 build matches the CUDA 12.x that nvidia-cuda-toolkit provides on +# noble; if that apt package ever moves to CUDA 13, switch to _cuda13. +ARG CUDNN_VERSION=9.16.0.29 +RUN if [ "$WITH_CUDA" = "1" ]; then \ + set -eux; \ + f=cudnn-linux-x86_64-${CUDNN_VERSION}_cuda12-archive; \ + wget -nv --timeout=60 --tries=3 -O /tmp/cudnn.tar.xz \ + "https://developer.download.nvidia.com/compute/cudnn/redist/cudnn/linux-x86_64/$f.tar.xz"; \ + mkdir -p /usr/local/cudnn; \ + tar -xJf /tmp/cudnn.tar.xz -C /tmp; \ + cp -a /tmp/$f/include /usr/local/cudnn/; \ + cp -a /tmp/$f/lib /usr/local/cudnn/; \ + rm -rf /tmp/cudnn.tar.xz /tmp/$f; \ + test -f /usr/local/cudnn/include/cudnn.h; \ + fi +ENV CUDNN_PATH=/usr/local/cudnn + +# musl cross toolchain (prebuilt from musl.cc). This is an ordinary amd64 +# host binary that emits amd64/musl code - no emulation involved. +# ---------------------------------------------------------- +# The image carries the toolchain at its root, but /bin there also holds +# busybox applets, so it is copied into the usual prefix and deliberately NOT +# put on PATH. Prefixed symlinks in /usr/local/bin give the tool names the +# rest of this file already uses; gcc still resolves its sysroot and libexec +# relative to its real location. +COPY --from=musltc /bin /opt/x86_64-linux-musl-cross/bin +COPY --from=musltc /include /opt/x86_64-linux-musl-cross/include +COPY --from=musltc /lib /opt/x86_64-linux-musl-cross/lib +COPY --from=musltc /libexec /opt/x86_64-linux-musl-cross/libexec +COPY --from=musltc /share /opt/x86_64-linux-musl-cross/share +COPY --from=musltc /x86_64-linux-musl /opt/x86_64-linux-musl-cross/x86_64-linux-musl +RUN set -eux; \ + for t in gcc g++ cpp cc gfortran ar ranlib strip nm objdump objcopy ld as \ + readelf addr2line c++filt gcov size strings gcc-ar gcc-nm gcc-ranlib; do \ + if [ -x /opt/x86_64-linux-musl-cross/bin/$t ]; then \ + ln -sf /opt/x86_64-linux-musl-cross/bin/$t /usr/local/bin/x86_64-linux-musl-$t; \ + fi; \ + done; \ + x86_64-linux-musl-gcc --version; \ + x86_64-linux-musl-gfortran --version; \ + x86_64-linux-musl-g++ --version + +# bindgen (espeak-rs-sys, whisper-rs-sys) loads libclang at build-script run +# time; without this it panics with "Unable to find libclang". Resolve the +# directory rather than hardcoding an LLVM version, and fail here if absent. +RUN set -eux; \ + d="$(dirname "$(find /usr/lib -name 'libclang.so*' 2>/dev/null | head -1)")"; \ + test -n "$d" -a -d "$d"; \ + ln -sfn "$d" /usr/local/libclang; \ + ls -l /usr/local/libclang/ | head -3 +ENV LIBCLANG_PATH=/usr/local/libclang + +# Install Rust and add the musl target +# ---------------------------------------------------------- +RUN curl -sSf https://sh.rustup.rs | sh -s -- -y +ENV PKG_CONFIG_ALLOW_CROSS=1 +ENV PKG_CONFIG_PATH=/usr/local/lib/pkgconfig +ENV PATH="/root/.cargo/bin:${PATH}" +RUN rustup target add x86_64-unknown-linux-musl +RUN rustup update stable + +# ---------------------------------------------------------- +# C/C++ Compiler / Linker config +# ---------------------------------------------------------- +ENV CC_x86_64_unknown_linux_musl=x86_64-linux-musl-gcc +ENV CXX_x86_64_unknown_linux_musl=x86_64-linux-musl-g++ +ENV CC=x86_64-linux-musl-gcc +ENV CXX=x86_64-linux-musl-g++ +ENV LD=x86_64-linux-musl-g++ +ENV LDFLAGS="-lgfortran -lm -lpthread -lquadmath" +ENV AR=ar +ENV RANLIB=ranlib +ENV FC=x86_64-linux-musl-gfortran +ENV FFLAGS="-static-libgfortran" +ENV CFLAGS="--sysroot=/opt/x86_64-linux-musl-cross/x86_64-linux-musl" +ENV CXXFLAGS="--sysroot=/opt/x86_64-linux-musl-cross/x86_64-linux-musl" +ENV CMAKE_FIND_LIBRARY_SUFFIXES=".a" +ENV CMAKE_EXE_LINKER_FLAGS=-static +ENV BINDGEN_EXTRA_CLANG_ARGS="-I/opt/x86_64-linux-musl-cross/x86_64-linux-musl/include" + +# ---------------------------------------------------------- +# Build openssl for musl (amd64) +# ---------------------------------------------------------- RUN set -eux; \ - apt-get update; \ - apt-get install -y --no-install-recommends glslc || true; \ - rm -rf /var/lib/apt/lists/*; \ - (command -v glslc >/dev/null 2>&1 && echo "glslc installed") || echo "glslc not available" + curl -LO https://www.openssl.org/source/openssl-3.1.3.tar.gz \ + && tar xf openssl-3.1.3.tar.gz \ + && cd openssl-3.1.3 \ + && ./Configure linux-x86_64 no-shared no-tests no-async no-secure-memory no-engine --openssldir=/usr/local/ssl --libdir=/usr/local/lib --prefix=/usr/local \ + && make -j$(nproc) \ + && make install_sw \ + && cd .. && rm -rf openssl-3.1.3 openssl-3.1.3.tar.gz + +ENV OPENSSL_STATIC=1 +ENV OPENSSL_DIR=/usr/local +ENV OPENSSL_LIB_DIR=/usr/local/lib +ENV OPENSSL_INCLUDE_DIR=/usr/local/include + +# ---------------------------------------------------------- +# Build OpenMP for musl (amd64) - cmake picks up CC/CXX above +# ---------------------------------------------------------- +ENV OPENMP_DIR=/openmp +ENV OPENMP_PREFIX=/usr/local +ENV LLVM_SRC_URL="https://github.com/llvm/llvm-project/releases/download/llvmorg-22.1.0/llvm-project-22.1.0.src.tar.xz" + +RUN mkdir -p $OPENMP_DIR +WORKDIR $OPENMP_DIR +RUN wget -q -O llvm-project-22.1.0.src.tar.xz "$LLVM_SRC_URL" \ + && tar xf llvm-project-22.1.0.src.tar.xz \ + && rm llvm-project-22.1.0.src.tar.xz + +RUN CFLAGS="--sysroot=/opt/x86_64-linux-musl-cross/x86_64-linux-musl -fopenmp" \ + CXXFLAGS="--sysroot=/opt/x86_64-linux-musl-cross/x86_64-linux-musl -fopenmp" \ + cmake -S $OPENMP_DIR/llvm-project-22.1.0.src/openmp \ + -B /openmp/build \ + -DCMAKE_INSTALL_PREFIX=$OPENMP_PREFIX \ + -DLIBOMP_ENABLE_SHARED=OFF \ + -DLIBOMP_ENABLE_STATIC=ON \ + -DCMAKE_BUILD_TYPE=Release + +RUN cmake --build /openmp/build --parallel $(nproc) --target install + +# ---------------------------------------------------------- +# Build static OpenBLAS for musl (amd64) +# ---------------------------------------------------------- +RUN git clone --depth 1 https://github.com/xianyi/OpenBLAS.git /openblas + +RUN cd /openblas && \ + set -eux; \ + make -j$(nproc) \ + HOSTCC=gcc \ + CFLAGS="--sysroot=/opt/x86_64-linux-musl-cross/x86_64-linux-musl" \ + LDFLAGS="-lgfortran -lm -lpthread -lquadmath -lpthread" \ + USE_STATIC=1 \ + STATIC_ONLY=1 \ + NO_SHARED=1 \ + USE_OPENMP=0 \ + USE_THREAD=1 \ + TARGET=GENERIC \ + NO_AVX=1 \ + VERBOSE=1 \ + libs netlib -ARG WITH_CUDA=1 -RUN if [ "\$WITH_CUDA" = "1" ]; then \ - apt-get update && apt-get install -y --no-install-recommends nvidia-cuda-toolkit && \ - rm -rf /var/lib/apt/lists/* ; \ - fi +RUN set -eux; \ + cd /openblas && \ + make install HOSTCC=gcc PREFIX=/usr/local STATIC_ONLY=1 NO_SHARED=1 && \ + cd / && rm -rf /openblas -ARG WITH_ROCM=0 -RUN if [ "\$WITH_ROCM" = "1" ]; then \ - apt-get update && apt-get install -y --no-install-recommends rocm-hip-sdk hipblas rocblas && \ - rm -rf /var/lib/apt/lists/* ; \ - fi +ENV OPENBLAS_PATH=/usr/local +ENV BLAS_LIBRARIES=/usr/local/lib/libopenblas.a +ENV BLAS_INCLUDE_DIRS=/usr/local/include + +# ---------------------------------------------------------- +# Build espeak-ng musl version (amd64) +# ---------------------------------------------------------- +RUN set -eux; \ + git clone --depth 1 https://github.com/espeak-ng/espeak-ng.git /espeak-ng; \ + cmake -S /espeak-ng -B /espeak-ng/build \ + -DCMAKE_BUILD_TYPE=Release \ + -DCMAKE_CXX_FLAGS="-std=c++17" \ + -DCMAKE_EXE_LINKER_FLAGS="-static" \ + -DCOMPILE_INTONATIONS=OFF \ + -DENABLE_TESTS=OFF \ + -DBUILD_SHARED_LIBS=OFF \ + -DCMAKE_POSITION_INDEPENDENT_CODE=ON \ + -DCMAKE_SKIP_RPATH=ON \ + -DCMAKE_INSTALL_RPATH="" \ + -DCMAKE_INSTALL_RPATH_USE_LINK_PATH=OFF \ + -DCMAKE_INSTALL_PREFIX=/usr/local; \ + cmake --build /espeak-ng/build -j$(nproc); \ + cmake --install /espeak-ng/build; \ + rm -rf /espeak-ng + +ENV ESPEAK_NG_DIR="/usr/local/lib" + +# ---------------------------------------------------------- +# musl locale compatibility shim for FlatBuffers (strtoll_l) +# ---------------------------------------------------------- +RUN printf "%s\n" \ +"#pragma once" \ +"" \ +"#include " \ +"#include " \ +"" \ +"// musl compatibility shim for *_l functions used by FlatBuffers" \ +"#if !defined(__GLIBC__)" \ +"" \ +"static inline long long strtoll_l(const char* nptr, char** endptr, int base, locale_t loc) {" \ +" (void)loc;" \ +" return strtoll(nptr, endptr, base);" \ +"}" \ +"" \ +"static inline unsigned long long strtoull_l(const char* nptr, char** endptr, int base, locale_t loc) {" \ +" (void)loc;" \ +" return strtoull(nptr, endptr, base);" \ +"}" \ +"" \ +"#endif" \ +> /usr/local/include/musl_locale_compat.h + +# ---------------------------------------------------------- +# Build protobuf, musl static (amd64). This is the only protoc used on +# this image: a static x86_64-musl binary still runs fine on this amd64 +# host (no dynamic loader needed), so it doubles as the codegen tool. +# ---------------------------------------------------------- +RUN git clone --depth 1 -b v3.21.12 https://github.com/protocolbuffers/protobuf.git /protobuf + +RUN set -eux; \ + cmake -S /protobuf -B /protobuf/build \ + -DBUILD_SHARED_LIBS=OFF \ + -DCMAKE_INSTALL_PREFIX=/usr/local \ + -DCMAKE_EXE_LINKER_FLAGS="-static" \ + -DCMAKE_BUILD_TYPE=Release \ + -Dprotobuf_BUILD_TESTS=OFF; \ + cmake --build /protobuf/build -j$(nproc); \ + cmake --install /protobuf/build; \ + rm -rf /protobuf + +# ----------------------------- +# Build ONNX Runtime for this arch (amd64 musl, CUDA optional) +# ----------------------------- +ENV ONNX_DIR=/onnxruntime +ENV ONNX_SRC=/onnxruntime-src + +RUN set -eux; \ + mkdir -p "$ONNX_DIR"; \ + # Pinned, not main: ORT main pins protobuf v33.6, whose generated headers + # include google/protobuf/runtime_version.h, while this image builds and + # links protobuf v3.21.12. v1.24.1 pins protobuf v21.12, matching, and is + # the newest ORT API (24) that ort-sys 2.0.0-rc.12 knows about. + git clone --depth 1 -b v1.24.1 https://github.com/microsoft/onnxruntime.git $ONNX_SRC; \ + # ORT decides between std::chrono and the vendored HowardHinnant/date + # purely on __cplusplus >= 202002L, but GCC 11 (this musl toolchain) has + # no std::chrono operator<< for time_point until GCC 13, so the C++20 + # branch fails to compile ostream_sink.cc. Force the date branch, which + # ORT already supports and fetches (deps.txt pins date v3.0.1). + sed -i "s|#define ORT_USE_CXX20_STD_CHRONO __cplusplus >= 202002L|#define ORT_USE_CXX20_STD_CHRONO 0|" \ + $ONNX_SRC/include/onnxruntime/core/common/logging/logging.h; \ + grep -q "define ORT_USE_CXX20_STD_CHRONO 0" $ONNX_SRC/include/onnxruntime/core/common/logging/logging.h + +WORKDIR $ONNX_SRC + +# execinfo.h (backtrace) isn't available under musl. +RUN find . -type f -print0 | xargs -0 -r sed -i "/#include /d" + +RUN mkdir -p build +WORKDIR $ONNX_SRC/build + +RUN set -eux; \ + cmake ../cmake \ + -B $ONNX_DIR \ + -DCMAKE_SYSTEM_PROCESSOR=AMD64 \ + -DCMAKE_CXX_STANDARD=20 \ + -DCMAKE_CXX_STANDARD_REQUIRED=ON \ + -DCMAKE_C_FLAGS="-march=x86-64 -include /usr/local/include/musl_locale_compat.h" \ + -DCMAKE_CXX_FLAGS="-march=x86-64 -std=c++17 -include /usr/local/include/musl_locale_compat.h" \ + -DCMAKE_EXE_LINKER_FLAGS="-static" \ + -DBUILD_SHARED_LIBS=OFF \ + -DCMAKE_C_COMPILER=$CC \ + -DCMAKE_CXX_COMPILER=$CXX \ + -DCMAKE_LINKER=$LD \ + -DCMAKE_COMPILE_WARNING_AS_ERROR=OFF \ + -Donnxruntime_BUILD_UNIT_TESTS=OFF \ + -Donnxruntime_ENABLE_EXTERNAL_CUSTOM_OP_SCHEMAS=OFF \ + -Donnxruntime_RUN_ONNX_TESTS=OFF \ + -DPython_EXECUTABLE=/usr/bin/python3 \ + -Donnxruntime_USE_VCPKG=OFF \ + -Donnxruntime_USE_MIMALLOC=OFF \ + -Donnxruntime_ENABLE_PYTHON=OFF \ + -Donnxruntime_BUILD_CSHARP=OFF \ + -Donnxruntime_BUILD_JAVA=OFF \ + -Donnxruntime_BUILD_NODEJS=OFF \ + -Donnxruntime_BUILD_OBJC=OFF \ + -Donnxruntime_BUILD_SHARED_LIB=OFF \ + -Donnxruntime_BUILD_APPLE_FRAMEWORK=OFF \ + -Donnxruntime_USE_DNNL=OFF \ + -Donnxruntime_USE_NNAPI_BUILTIN=OFF \ + -Donnxruntime_USE_VSINPU=OFF \ + -Donnxruntime_USE_RKNPU=OFF \ + -Donnxruntime_USE_VITISAI=OFF \ + -Donnxruntime_USE_TENSORRT=OFF \ + -Donnxruntime_USE_NV=OFF \ + -Donnxruntime_USE_TENSORRT_BUILTIN_PARSER=ON \ + -Donnxruntime_USE_TENSORRT_INTERFACE=OFF \ + -Donnxruntime_USE_CUDA_INTERFACE=OFF \ + -Donnxruntime_USE_NV_INTERFACE=OFF \ + -Donnxruntime_USE_OPENVINO_INTERFACE=OFF \ + -Donnxruntime_USE_VITISAI_INTERFACE=OFF \ + -Donnxruntime_USE_QNN_INTERFACE=OFF \ + -Donnxruntime_USE_MIGRAPHX_INTERFACE=OFF \ + -Donnxruntime_USE_MIGRAPHX=OFF \ + -Donnxruntime_DISABLE_RTTI=OFF \ + -Donnxruntime_DISABLE_EXCEPTIONS=OFF \ + -Donnxruntime_MINIMAL_BUILD=OFF \ + -Donnxruntime_ENABLE_LTO=OFF \ + -Donnxruntime_USE_ACL=OFF \ + -Donnxruntime_USE_ARMNN=OFF \ + -Donnxruntime_USE_JSEP=OFF \ + -Donnxruntime_USE_WEBGPU=OFF \ + -Donnxruntime_USE_EXTERNAL_DAWN=OFF \ + -Donnxruntime_WGSL_TEMPLATE=static \ + -Donnxruntime_ENABLE_TRAINING=OFF \ + -Donnxruntime_ENABLE_TRAINING_OPS=OFF \ + -Donnxruntime_ENABLE_TRAINING_APIS=OFF \ + -Donnxruntime_ENABLE_CPU_FP16_OPS=OFF \ + -Donnxruntime_USE_NCCL=OFF \ + -Donnxruntime_BUILD_BENCHMARKS=OFF \ + -Donnxruntime_ENABLE_CUDA_LINE_NUMBER_INFO=OFF \ + -Donnxruntime_USE_CUDA_NHWC_OPS=OFF \ + -Donnxruntime_BUILD_WEBASSEMBLY_STATIC_LIB=OFF \ + -Donnxruntime_ENABLE_WEBASSEMBLY_THREADS=OFF \ + -Donnxruntime_USE_XNNPACK=OFF \ + -Donnxruntime_USE_WEBNN=OFF \ + -Donnxruntime_USE_CANN=OFF \ + -Donnxruntime_CUDA_MINIMAL=OFF \ + -Donnxruntime_USE_CUDA=$WITH_CUDA \ + -Donnxruntime_USE_KLEIDIAI=OFF \ + -DCMAKE_INSTALL_PREFIX=$ONNX_DIR \ + -DCMAKE_BUILD_TYPE=Release \ + -Donnxruntime_USE_SYSTEM_PROTOBUF=ON \ + -DCMAKE_CUDA_COMPILER=/usr/bin/nvcc \ + -DCUDAToolkit_ROOT=/usr \ + -DProtobuf_INCLUDE_DIR=/usr/local/include \ + -DProtobuf_LIBRARIES=/usr/local/lib/libprotobuf.a \ + -DProtobuf_PROTOC_EXECUTABLE=/usr/local/bin/protoc; \ + cmake --build $ONNX_DIR --config Release + +ENV ORT_STRATEGY=system +ENV ORT_LIB_LOCATION=$ONNX_DIR -RUN curl -sSf https://sh.rustup.rs | sh -s -- -y -ENV PATH="/root/.cargo/bin:\${PATH}" -RUN rustup target add x86_64-unknown-linux-gnu WORKDIR /work DOCKERFILE @@ -251,188 +636,964 @@ DOCKERFILE [[ "${DOCKER_NO_CACHE}" -eq 1 ]] && build_args+=(--no-cache) echo "== Linux amd64 build (Docker image) ==" - docker build "${build_args[@]}" --platform=linux/amd64 \ - --build-arg WITH_CUDA="${WITH_CUDA}" \ - --build-arg WITH_ROCM="${WITH_ROCM}" \ - --build-arg CACHE_BUST="${CACHE_BUST}" \ - -f "$df" -t "$img" "$tmp" + if docker image inspect "$img" >/dev/null 2>&1; then + echo "Docker image '$img' already exists. Skipping build." + else + docker build "${build_args[@]}" --platform=linux/amd64 \ + --build-arg WITH_CUDA="${WITH_CUDA}" \ + --build-arg CACHE_BUST="${CACHE_BUST}" \ + -f "$df" -t "$img" "$tmp" + fi - echo "== Linux amd64 cargo builds (cpu + optional variants) ==" + echo "== Linux amd64 cargo builds (cpu=${WITH_CPU} vulkan=${WITH_VULKAN} cuda=${WITH_CUDA}) ==" docker run --rm --platform=linux/amd64 \ -v "${PROJECT_ROOT}:/work" -w /work \ -v "${HOST_K_CACHE}:${CONT_K_CACHE}" \ -v "${HOST_WHISPER_MODELS}:${CONT_WHISPER_MODELS}" \ - -e LINUX_WITH_OPENBLAS="${LINUX_WITH_OPENBLAS}" \ - -e LINUX_WITH_VULKAN="${LINUX_WITH_VULKAN}" \ + -e WITH_CPU="${WITH_CPU}" \ + -e WITH_VULKAN="${WITH_VULKAN}" \ -e WITH_CUDA="${WITH_CUDA}" \ - -e WITH_ROCM="${WITH_ROCM}" \ + -e CMAKE_SKIP_RPATH=ON \ + -e CMAKE_INSTALL_RPATH_USE_LINK_PATH=OFF \ "$img" \ bash -lc ' set -euo pipefail - target=x86_64-unknown-linux-gnu + + ARCH=amd64 + target=x86_64-unknown-linux-musl + + # rust-toolchain.toml pins the toolchain, but the image added the musl + # target to whatever was default at image build time (stable). Cargo + # switches to the pinned toolchain here and then finds no musl std. + # Re-add the target from inside /work so rustup reads rust-toolchain.toml. + cd /work + rustup target add "$target" + + # Build ALSA as a static library for musl (needed by cpal) + if [ ! -f /usr/local/lib/libasound.a ]; then + echo "--- Building ALSA static library for musl ---" + apt-get update -qq && apt-get install -y --no-install-recommends autoconf automake libtool + curl -sL -o /tmp/alsa.tar.gz \ + "https://github.com/alsa-project/alsa-lib/archive/refs/tags/v1.2.12.tar.gz" + tar xzf /tmp/alsa.tar.gz -C /tmp + mv /tmp/alsa-lib-1.2.12 /tmp/alsa-lib + cd /tmp/alsa-lib + autoreconf -fi + ./configure \ + --host=x86_64-linux-musl \ + --build=x86_64-linux-gnu \ + --prefix=/usr/local \ + --enable-shared=no \ + --enable-static=yes \ + --with-pkg-config-plugindir=/usr/local/lib/pkgconfig \ + CC=x86_64-linux-musl-gcc \ + CFLAGS="--sysroot=/opt/x86_64-linux-musl-cross/x86_64-linux-musl -O3" \ + LDFLAGS="-L/opt/x86_64-linux-musl-cross/x86_64-linux-musl/lib" + make -j$(nproc) + make install + cd / + rm -rf /tmp/alsa-lib /tmp/alsa.tar.gz + else + echo "--- ALSA already built, skipping ---" + fi + + # Build abseil + re2 static libraries for musl (needed by ort-sys) + if [ ! -f /usr/local/lib/libre2.a ]; then + echo "--- Building abseil + re2 static libraries for musl ---" + curl -sL -o /tmp/re2.tar.gz \ + "https://github.com/google/re2/archive/refs/tags/2024-07-02.tar.gz" + tar xzf /tmp/re2.tar.gz -C /tmp + mv /tmp/re2-2024-07-02 /tmp/re2 + + curl -sL -o /tmp/absl.tar.gz \ + "https://github.com/abseil/abseil-cpp/archive/refs/tags/20240722.0.tar.gz" + tar xzf /tmp/absl.tar.gz -C /tmp + mv /tmp/abseil-cpp-20240722.0 /tmp/absl + cmake -S /tmp/absl -B /tmp/absl-build \ + -DCMAKE_INSTALL_PREFIX=/usr/local \ + -DCMAKE_BUILD_TYPE=Release \ + -DBUILD_SHARED_LIBS=OFF \ + -DCMAKE_C_COMPILER=x86_64-linux-musl-gcc \ + -DCMAKE_CXX_COMPILER=x86_64-linux-musl-g++ \ + -DCMAKE_CXX_FLAGS="--sysroot=/opt/x86_64-linux-musl-cross/x86_64-linux-musl" \ + -DCMAKE_C_FLAGS="--sysroot=/opt/x86_64-linux-musl-cross/x86_64-linux-musl" \ + -DABSL_BUILD_TESTING=OFF + cmake --build /tmp/absl-build --parallel $(nproc) + cmake --install /tmp/absl-build --prefix /usr/local + + make -C /tmp/re2 -j$(nproc) \ + CXX=x86_64-linux-musl-g++ \ + CXXFLAGS="--sysroot=/opt/x86_64-linux-musl-cross/x86_64-linux-musl -O3 -static -I/usr/local/include" \ + LDFLAGS="-static -L/usr/local/lib" \ + AR=x86_64-linux-musl-ar \ + static + cp /tmp/re2/obj/libre2.a /usr/local/lib/ + mkdir -p $ONNX_DIR/_deps/re2-build/ + cp /usr/local/lib/libre2.a $ONNX_DIR/_deps/re2-build/ + rm -rf /tmp/re2 /tmp/re2.tar.gz /tmp/absl /tmp/absl.tar.gz /tmp/absl-build + else + echo "--- abseil/re2 already built, skipping ---" + fi build_variant() { local variant="$1" local feats="$2" - local ctd="/work/target-cross/linux-amd64-${variant}" - echo "---- Building linux/amd64 [$variant] features: $feats" - CARGO_TARGET_DIR="$ctd" cargo build --release --target "$target" --no-default-features --features "$feats" + local ctd="/work/target-cross/linux-${ARCH}-${variant}" + + echo "---- Building linux/${ARCH} [$variant] features: $feats" + export CARGO_PROFILE_RELEASE_CODEGEN_UNITS=1 + export CARGO_PROFILE_RELEASE_DEBUG=false + export CARGO_PROFILE_RELEASE_STRIP=symbols + export CARGO_PROFILE_RELEASE_INCREMENTAL=false + export CARGO_TARGET_X86_64_UNKNOWN_LINUX_MUSL_LINKER=x86_64-linux-musl-g++ + export RUSTC_LINKER=x86_64-linux-musl-g++ + + ABSL_LIBS="" + for f in /usr/local/lib/libabsl_*.a; do + ABSL_LIBS="$ABSL_LIBS -C link-arg=$f" + done + + export RUSTFLAGS="-C target-feature=+crt-static -C target-cpu=x86-64-v3 -C codegen-units=1 -C opt-level=3 -C link-arg=-L/opt/x86_64-linux-musl-cross/x86_64-linux-musl/lib -C link-arg=-Wl,--start-group -C link-arg=/usr/local/lib/libopenblas.a -C link-arg=/usr/local/lib/libprotobuf.a -C link-arg=/usr/local/lib/libomp.a ${ABSL_LIBS} -C link-arg=-Wl,--end-group -C link-arg=-lm -C link-arg=-lc -C link-arg=-lgfortran -C link-arg=-lpthread -C link-arg=-lgcc" + + cd /work + CARGO_TARGET_DIR="$ctd" \ + cargo build --release --target "$target" --features "$feats" } - build_variant cpu "'"${FEATURES_CPU}"'" - - if [ "${LINUX_WITH_OPENBLAS}" = "1" ]; then - if [ -d /usr/include/x86_64-linux-gnu/openblas-pthread ]; then - export BLAS_INCLUDE_DIRS=/usr/include/x86_64-linux-gnu/openblas-pthread - elif [ -d /usr/include/x86_64-linux-gnu/openblas ]; then - export BLAS_INCLUDE_DIRS=/usr/include/x86_64-linux-gnu/openblas - elif [ -d /usr/include/openblas ]; then - export BLAS_INCLUDE_DIRS=/usr/include/openblas - else - export BLAS_INCLUDE_DIRS=/usr/include - fi - build_variant openblas "'"${FEATURES_OPENBLAS}"'" + if [ "${WITH_CPU}" = "1" ]; then + build_variant cpu "'"${FEATURES_CPU}"'" fi - if [ "${LINUX_WITH_VULKAN}" = "1" ]; then - if command -v glslc >/dev/null 2>&1; then - build_variant vulkan "'"${FEATURES_VULKAN}"'" - else - echo "WARN: glslc missing; skipping linux/amd64 vulkan variant" - fi + if [ "${WITH_VULKAN}" = "1" ]; then + build_variant vulkan "'"${FEATURES_VULKAN}"'" fi if [ "${WITH_CUDA}" = "1" ]; then build_variant cuda "'"${FEATURES_CUDA}"'" fi - - if [ "${WITH_ROCM}" = "1" ]; then - build_variant rocm "'"${FEATURES_ROCM}"'" - fi ' - linux_copy_out "amd64" "x86_64-unknown-linux-gnu" "cpu" - [[ "${LINUX_WITH_OPENBLAS}" == "1" ]] && linux_copy_out "amd64" "x86_64-unknown-linux-gnu" "openblas" - if [[ "${LINUX_WITH_VULKAN}" == "1" ]] && [[ -f "${PROJECT_ROOT}/target-cross/linux-amd64-vulkan/x86_64-unknown-linux-gnu/release/${BIN_NAME}" ]]; then - linux_copy_out "amd64" "x86_64-unknown-linux-gnu" "vulkan" - fi - [[ "${WITH_CUDA}" == "1" ]] && linux_copy_out "amd64" "x86_64-unknown-linux-gnu" "cuda" - [[ "${WITH_ROCM}" == "1" ]] && linux_copy_out "amd64" "x86_64-unknown-linux-gnu" "rocm" + [[ "${WITH_CPU}" == "1" ]] && linux_copy_out "amd64" "x86_64-unknown-linux-musl" "cpu" + [[ "${WITH_VULKAN}" == "1" ]] && linux_copy_out "amd64" "x86_64-unknown-linux-musl" "vulkan" + [[ "${WITH_CUDA}" == "1" ]] && linux_copy_out "amd64" "x86_64-unknown-linux-musl" "cuda" + true docker image rm -f "$img" >/dev/null 2>&1 || true rm -rf "$tmp" >/dev/null 2>&1 || true } -build_linux_arm64_docker_variants() { +# ========================================================== +# ARM64 (aarch64-unknown-linux-musl) - cpu, vulkan (no CUDA) +# ========================================================== +# Cross-compiled from the SAME linux/amd64 host as the amd64 build above +# (see the file header comment) using the musl.cc aarch64 cross toolchain. +# The one wrinkle vs. the amd64 image: a cross-built aarch64 protoc can't +# run on this amd64 host to generate ONNX Runtime's protobuf code, so this +# image builds protobuf twice from the same source tag - once natively +# (host protoc, used only as a codegen tool) and once cross-compiled +# (target libprotobuf.a, used for linking). +build_linux_arm64_variants() { [[ "$docker_ok" -eq 1 ]] || { echo "Skipping linux/arm64: docker not found."; return 0; } + [[ "${FORCE_AMD64_DOCKER}" -eq 1 ]] || { echo "Skipping linux/arm64: cannot run linux/amd64 containers (needed for cross-compilation)."; return 0; } local tmp df img CACHE_BUST tmp="$(mktemp -d)" df="${tmp}/Dockerfile.linux.arm64" - img="local/${BIN_NAME}-linux-arm64:${VERSION}-$$" + img="local/${BIN_NAME}-linux-arm64:cache" CACHE_BUST="$(date +%s)" - cat > "$df" < "$df" <<'DOCKERFILE' +# musl cross toolchain, taken from the official musl.cc container image +# rather than downloaded from musl.cc: that host drops GitHub Actions runner +# IPs (every fetch attempt hit the wget timeout), and hosting the tarballs on +# this repo's releases is not wanted. Same GCC 11.2.1 build, digest-pinned. +FROM muslcc/x86_64:aarch64-linux-musl@sha256:2d106ab72b2b5ea5ac7696a6b3d7c5f4f98d24f99230fb784d991c453034c017 AS musltc + FROM ubuntu:noble ENV DEBIAN_FRONTEND=noninteractive -ARG CACHE_BUST=${CACHE_BUST} +ARG CACHE_BUST +# ---------------------------------------------------------- +# Build dependencies (host tools; glslc/protoc-host run natively here) +# ---------------------------------------------------------- RUN apt-get update && apt-get install -y --no-install-recommends \ - ca-certificates curl git xz-utils \ - build-essential pkg-config \ - cmake ninja-build \ - clang libclang-dev llvm-dev \ - perl \ - libssl-dev \ - libasound2-dev \ - libxdo-dev \ - libx11-dev \ - libopenblas-dev \ - libvulkan-dev vulkan-tools vulkan-utility-libraries-dev \ - spirv-tools glslang-tools \ - && rm -rf /var/lib/apt/lists/* + build-essential pkg-config curl wget ca-certificates git \ + libclang-dev clang \ + cmake python3 python3-pip perl autoconf automake libtool \ + gfortran zlib1g-dev libbz2-dev liblzma-dev libssl-dev \ + glslc \ +&& rm -rf /var/lib/apt/lists/* + +# musl cross toolchain for aarch64 (prebuilt from musl.cc). Its gcc/g++ are +# ordinary amd64 host binaries that emit aarch64 code - no QEMU needed. +# ---------------------------------------------------------- +# The image carries the toolchain at its root, but /bin there also holds +# busybox applets, so it is copied into the usual prefix and deliberately NOT +# put on PATH. Prefixed symlinks in /usr/local/bin give the tool names the +# rest of this file already uses; gcc still resolves its sysroot and libexec +# relative to its real location. +COPY --from=musltc /bin /opt/aarch64-linux-musl-cross/bin +COPY --from=musltc /include /opt/aarch64-linux-musl-cross/include +COPY --from=musltc /lib /opt/aarch64-linux-musl-cross/lib +COPY --from=musltc /libexec /opt/aarch64-linux-musl-cross/libexec +COPY --from=musltc /share /opt/aarch64-linux-musl-cross/share +COPY --from=musltc /aarch64-linux-musl /opt/aarch64-linux-musl-cross/aarch64-linux-musl +RUN set -eux; \ + for t in gcc g++ cpp cc gfortran ar ranlib strip nm objdump objcopy ld as \ + readelf addr2line c++filt gcov size strings gcc-ar gcc-nm gcc-ranlib; do \ + if [ -x /opt/aarch64-linux-musl-cross/bin/$t ]; then \ + ln -sf /opt/aarch64-linux-musl-cross/bin/$t /usr/local/bin/aarch64-linux-musl-$t; \ + fi; \ + done; \ + aarch64-linux-musl-gcc --version; \ + aarch64-linux-musl-gfortran --version; \ + aarch64-linux-musl-g++ --version + +# bindgen (espeak-rs-sys, whisper-rs-sys) loads libclang at build-script run +# time; without this it panics with "Unable to find libclang". Resolve the +# directory rather than hardcoding an LLVM version, and fail here if absent. +RUN set -eux; \ + d="$(dirname "$(find /usr/lib -name 'libclang.so*' 2>/dev/null | head -1)")"; \ + test -n "$d" -a -d "$d"; \ + ln -sfn "$d" /usr/local/libclang; \ + ls -l /usr/local/libclang/ | head -3 +ENV LIBCLANG_PATH=/usr/local/libclang + +# Install Rust and add the musl target +# ---------------------------------------------------------- +RUN curl -sSf https://sh.rustup.rs | sh -s -- -y +ENV PKG_CONFIG_ALLOW_CROSS=1 +ENV PKG_CONFIG_PATH=/usr/local/lib/pkgconfig +ENV PATH="/root/.cargo/bin:${PATH}" +RUN rustup target add aarch64-unknown-linux-musl +RUN rustup update stable + +# ---------------------------------------------------------- +# C/C++ Compiler / Linker config +# ---------------------------------------------------------- +ENV CC_aarch64_unknown_linux_musl=aarch64-linux-musl-gcc +ENV CXX_aarch64_unknown_linux_musl=aarch64-linux-musl-g++ +ENV CC=aarch64-linux-musl-gcc +ENV CXX=aarch64-linux-musl-g++ +ENV LD=aarch64-linux-musl-g++ +# No -lquadmath here: libquadmath is x86-only, the aarch64 musl toolchain +# does not ship it, and this ENV is inherited by every later build (OpenSSL +# first), not just the Fortran-linking OpenBLAS one. +ENV LDFLAGS="-lgfortran -lm -lpthread" +ENV AR=ar +ENV RANLIB=ranlib +ENV FC=aarch64-linux-musl-gfortran +ENV FFLAGS="-static-libgfortran" +ENV CFLAGS="--sysroot=/opt/aarch64-linux-musl-cross/aarch64-linux-musl" +ENV CXXFLAGS="--sysroot=/opt/aarch64-linux-musl-cross/aarch64-linux-musl" + +# Some build scripts (openssl-sys header expansion) compile for the HOST +# triple. cc-rs falls back to the generic CC/CFLAGS above, handing the aarch64 +# cross compiler an x86_64 job and an aarch64 sysroot - it then rejects -m64. +# Host-triple overrides take precedence in cc-rs, so point them at system gcc. +# -O2 rather than "": an empty value loses to the generic CFLAGS above, so +# host gcc kept receiving the aarch64 --sysroot and could not find /usr/include +# ("no include path in which to search for stdint.h" building ring). +ENV CC_x86_64_unknown_linux_gnu=gcc +ENV CXX_x86_64_unknown_linux_gnu=g++ +ENV CFLAGS_x86_64_unknown_linux_gnu="-O2" +ENV CXXFLAGS_x86_64_unknown_linux_gnu="-O2" +ENV HOST_CC=gcc +ENV HOST_CXX=g++ +ENV HOST_CFLAGS="-O2" +ENV HOST_CXXFLAGS="-O2" +ENV CMAKE_FIND_LIBRARY_SUFFIXES=".a" +ENV CMAKE_EXE_LINKER_FLAGS=-static +ENV BINDGEN_EXTRA_CLANG_ARGS="-I/opt/aarch64-linux-musl-cross/aarch64-linux-musl/include" + +# ---------------------------------------------------------- +# Build openssl for musl (arm64) +# ---------------------------------------------------------- +RUN set -eux; \ + curl -LO https://www.openssl.org/source/openssl-3.1.3.tar.gz \ + && tar xf openssl-3.1.3.tar.gz \ + && cd openssl-3.1.3 \ + && ./Configure linux-aarch64 no-shared no-tests no-async no-secure-memory no-engine --openssldir=/usr/local/ssl --libdir=/usr/local/lib --prefix=/usr/local \ + && make -j$(nproc) \ + && make install_sw \ + && cd .. && rm -rf openssl-3.1.3 openssl-3.1.3.tar.gz + +ENV OPENSSL_STATIC=1 +ENV OPENSSL_DIR=/usr/local +ENV OPENSSL_LIB_DIR=/usr/local/lib +ENV OPENSSL_INCLUDE_DIR=/usr/local/include + +# ---------------------------------------------------------- +# Build OpenMP for musl (arm64) - cmake picks up CC/CXX above +# ---------------------------------------------------------- +ENV OPENMP_DIR=/openmp +ENV OPENMP_PREFIX=/usr/local +ENV LLVM_SRC_URL="https://github.com/llvm/llvm-project/releases/download/llvmorg-22.1.0/llvm-project-22.1.0.src.tar.xz" + +RUN mkdir -p $OPENMP_DIR +WORKDIR $OPENMP_DIR +RUN wget -q -O llvm-project-22.1.0.src.tar.xz "$LLVM_SRC_URL" \ + && tar xf llvm-project-22.1.0.src.tar.xz \ + && rm llvm-project-22.1.0.src.tar.xz + +RUN CFLAGS="--sysroot=/opt/aarch64-linux-musl-cross/aarch64-linux-musl -fopenmp" \ + CXXFLAGS="--sysroot=/opt/aarch64-linux-musl-cross/aarch64-linux-musl -fopenmp" \ + cmake -S $OPENMP_DIR/llvm-project-22.1.0.src/openmp \ + -B /openmp/build \ + -DCMAKE_INSTALL_PREFIX=$OPENMP_PREFIX \ + -DLIBOMP_ENABLE_SHARED=OFF \ + -DLIBOMP_ENABLE_STATIC=ON \ + -DCMAKE_BUILD_TYPE=Release + +RUN cmake --build /openmp/build --parallel $(nproc) --target install + +# ---------------------------------------------------------- +# Build static OpenBLAS for musl (arm64) +# ---------------------------------------------------------- +RUN git clone --depth 1 https://github.com/xianyi/OpenBLAS.git /openblas + +RUN cd /openblas && \ + set -eux; \ + make -j$(nproc) \ + HOSTCC=gcc \ + CFLAGS="--sysroot=/opt/aarch64-linux-musl-cross/aarch64-linux-musl" \ + LDFLAGS="-lgfortran -lm -lpthread -lpthread" \ + USE_STATIC=1 \ + STATIC_ONLY=1 \ + NO_SHARED=1 \ + USE_OPENMP=0 \ + USE_THREAD=1 \ + TARGET=ARMV8 \ + VERBOSE=1 \ + libs netlib RUN set -eux; \ - apt-get update; \ - apt-get install -y --no-install-recommends glslc || true; \ - rm -rf /var/lib/apt/lists/*; \ - (command -v glslc >/dev/null 2>&1 && echo "glslc installed") || echo "glslc not available" + cd /openblas && \ + make install HOSTCC=gcc PREFIX=/usr/local STATIC_ONLY=1 NO_SHARED=1 && \ + cd / && rm -rf /openblas + +ENV OPENBLAS_PATH=/usr/local +ENV BLAS_LIBRARIES=/usr/local/lib/libopenblas.a +ENV BLAS_INCLUDE_DIRS=/usr/local/include + +# ---------------------------------------------------------- +# Build espeak-ng musl version (arm64) +# ---------------------------------------------------------- +RUN set -eux; \ + git clone --depth 1 https://github.com/espeak-ng/espeak-ng.git /espeak-ng; \ + cmake -S /espeak-ng -B /espeak-ng/build \ + -DCMAKE_BUILD_TYPE=Release \ + -DCMAKE_CXX_FLAGS="-std=c++17" \ + -DCMAKE_EXE_LINKER_FLAGS="-static" \ + -DCOMPILE_INTONATIONS=OFF \ + -DENABLE_TESTS=OFF \ + -DBUILD_SHARED_LIBS=OFF \ + -DCMAKE_POSITION_INDEPENDENT_CODE=ON \ + -DCMAKE_SKIP_RPATH=ON \ + -DCMAKE_INSTALL_RPATH="" \ + -DCMAKE_INSTALL_RPATH_USE_LINK_PATH=OFF \ + -DCMAKE_INSTALL_PREFIX=/usr/local; \ + cmake --build /espeak-ng/build -j$(nproc); \ + cmake --install /espeak-ng/build; \ + rm -rf /espeak-ng + +ENV ESPEAK_NG_DIR="/usr/local/lib" + +# ---------------------------------------------------------- +# musl locale compatibility shim for FlatBuffers (strtoll_l) +# ---------------------------------------------------------- +RUN printf "%s\n" \ +"#pragma once" \ +"" \ +"#include " \ +"#include " \ +"" \ +"#if !defined(__GLIBC__)" \ +"" \ +"static inline long long strtoll_l(const char* nptr, char** endptr, int base, locale_t loc) {" \ +" (void)loc;" \ +" return strtoll(nptr, endptr, base);" \ +"}" \ +"" \ +"static inline unsigned long long strtoull_l(const char* nptr, char** endptr, int base, locale_t loc) {" \ +" (void)loc;" \ +" return strtoull(nptr, endptr, base);" \ +"}" \ +"" \ +"#endif" \ +> /usr/local/include/musl_locale_compat.h + +# ---------------------------------------------------------- +# protobuf, built TWICE from the same source tag: +# 1) natively (host gcc/g++, dynamic OK) -> a protoc that can actually run +# on this amd64 host, used only for ONNX Runtime codegen below. +# 2) cross (aarch64-linux-musl, static) -> the libprotobuf.a that actually +# gets linked into the target binary. +# ---------------------------------------------------------- +RUN git clone --depth 1 -b v3.21.12 https://github.com/protocolbuffers/protobuf.git /protobuf + +RUN set -eux; \ + cmake -S /protobuf -B /protobuf/build-host \ + -DBUILD_SHARED_LIBS=OFF \ + -DCMAKE_INSTALL_PREFIX=/opt/host-protoc \ + -DCMAKE_BUILD_TYPE=Release \ + -Dprotobuf_BUILD_TESTS=OFF; \ + cmake --build /protobuf/build-host -j$(nproc); \ + cmake --install /protobuf/build-host + +RUN set -eux; \ + cmake -S /protobuf -B /protobuf/build-target \ + -DBUILD_SHARED_LIBS=OFF \ + -DCMAKE_INSTALL_PREFIX=/usr/local \ + -DCMAKE_C_COMPILER=$CC \ + -DCMAKE_CXX_COMPILER=$CXX \ + -DCMAKE_EXE_LINKER_FLAGS="-static" \ + -DCMAKE_BUILD_TYPE=Release \ + -Dprotobuf_BUILD_TESTS=OFF \ + -Dprotobuf_BUILD_PROTOC_BINARIES=OFF; \ + cmake --build /protobuf/build-target -j$(nproc); \ + cmake --install /protobuf/build-target; \ + rm -rf /protobuf + +# ----------------------------- +# Build ONNX Runtime for this arch (arm64 musl, no CUDA) +# ----------------------------- +ENV ONNX_DIR=/onnxruntime +ENV ONNX_SRC=/onnxruntime-src + +RUN set -eux; \ + mkdir -p "$ONNX_DIR"; \ + # Pinned, not main: ORT main pins protobuf v33.6, whose generated headers + # include google/protobuf/runtime_version.h, while this image builds and + # links protobuf v3.21.12. v1.24.1 pins protobuf v21.12, matching, and is + # the newest ORT API (24) that ort-sys 2.0.0-rc.12 knows about. + git clone --depth 1 -b v1.24.1 https://github.com/microsoft/onnxruntime.git $ONNX_SRC; \ + # ORT decides between std::chrono and the vendored HowardHinnant/date + # purely on __cplusplus >= 202002L, but GCC 11 (this musl toolchain) has + # no std::chrono operator<< for time_point until GCC 13, so the C++20 + # branch fails to compile ostream_sink.cc. Force the date branch, which + # ORT already supports and fetches (deps.txt pins date v3.0.1). + sed -i "s|#define ORT_USE_CXX20_STD_CHRONO __cplusplus >= 202002L|#define ORT_USE_CXX20_STD_CHRONO 0|" \ + $ONNX_SRC/include/onnxruntime/core/common/logging/logging.h; \ + grep -q "define ORT_USE_CXX20_STD_CHRONO 0" $ONNX_SRC/include/onnxruntime/core/common/logging/logging.h; \ + mlas=$ONNX_SRC/onnxruntime/core/mlas/lib; \ + for f in activate_fp16 cast_kernel_neon dwconv eltwise_kernel_neon_fp16 \ + halfgemm_kernel_neon_fp16 hqnbitgemm_kernel_neon_fp16 pooling_fp16 \ + rotary_embedding_kernel_neon_fp16 softmax_kernel_neon_fp16; do \ + [ -f "$mlas/$f.cpp" ] && sed -i '1i #pragma GCC target("arch=armv8.2-a+fp16")' "$mlas/$f.cpp"; \ + done; \ + for f in sbconv_kernel_neon sbgemm_kernel_neon; do \ + [ -f "$mlas/$f.cpp" ] && sed -i '1i #pragma GCC target("arch=armv8.2-a+bf16")' "$mlas/$f.cpp"; \ + done; \ + sed -i '1i #pragma GCC target("arch=armv8.2-a+i8mm")' $mlas/sqnbitgemm_kernel_neon_int8_i8mm.cpp; \ + sed -i '1i #pragma GCC target("arch=armv8.2-a+dotprod")' $mlas/sqnbitgemm_kernel_neon_int8.cpp; \ + head -1 $mlas/sqnbitgemm_kernel_neon_int8_i8mm.cpp | grep -q i8mm; \ + head -1 $mlas/sbgemm_kernel_neon.cpp | grep -q bf16 + +WORKDIR $ONNX_SRC + +# execinfo.h (backtrace) isn't available under musl. +RUN find . -type f -print0 | xargs -0 -r sed -i "/#include /d" + +RUN mkdir -p build +WORKDIR $ONNX_SRC/build + +RUN set -eux; \ + cmake ../cmake \ + -B $ONNX_DIR \ + -DCMAKE_SYSTEM_NAME=Linux \ + -DCMAKE_SYSTEM_PROCESSOR=aarch64 \ + -DCMAKE_CXX_STANDARD=20 \ + -DCMAKE_CXX_STANDARD_REQUIRED=ON \ + -DCMAKE_C_FLAGS="-include /usr/local/include/musl_locale_compat.h" \ + -DCMAKE_CXX_FLAGS="-std=c++17 -include /usr/local/include/musl_locale_compat.h" \ + -DCMAKE_EXE_LINKER_FLAGS="-static" \ + -DBUILD_SHARED_LIBS=OFF \ + -DCMAKE_C_COMPILER=$CC \ + -DCMAKE_CXX_COMPILER=$CXX \ + -DCMAKE_LINKER=$LD \ + -DCMAKE_COMPILE_WARNING_AS_ERROR=OFF \ + -Donnxruntime_BUILD_UNIT_TESTS=OFF \ + -Donnxruntime_ENABLE_EXTERNAL_CUSTOM_OP_SCHEMAS=OFF \ + -Donnxruntime_RUN_ONNX_TESTS=OFF \ + -DPython_EXECUTABLE=/usr/bin/python3 \ + -Donnxruntime_USE_VCPKG=OFF \ + -Donnxruntime_USE_MIMALLOC=OFF \ + -Donnxruntime_ENABLE_PYTHON=OFF \ + -Donnxruntime_BUILD_CSHARP=OFF \ + -Donnxruntime_BUILD_JAVA=OFF \ + -Donnxruntime_BUILD_NODEJS=OFF \ + -Donnxruntime_BUILD_OBJC=OFF \ + -Donnxruntime_BUILD_SHARED_LIB=OFF \ + -Donnxruntime_BUILD_APPLE_FRAMEWORK=OFF \ + -Donnxruntime_USE_DNNL=OFF \ + -Donnxruntime_USE_NNAPI_BUILTIN=OFF \ + -Donnxruntime_USE_VSINPU=OFF \ + -Donnxruntime_USE_RKNPU=OFF \ + -Donnxruntime_USE_VITISAI=OFF \ + -Donnxruntime_USE_TENSORRT=OFF \ + -Donnxruntime_USE_NV=OFF \ + -Donnxruntime_USE_TENSORRT_BUILTIN_PARSER=ON \ + -Donnxruntime_USE_TENSORRT_INTERFACE=OFF \ + -Donnxruntime_USE_CUDA_INTERFACE=OFF \ + -Donnxruntime_USE_NV_INTERFACE=OFF \ + -Donnxruntime_USE_OPENVINO_INTERFACE=OFF \ + -Donnxruntime_USE_VITISAI_INTERFACE=OFF \ + -Donnxruntime_USE_QNN_INTERFACE=OFF \ + -Donnxruntime_USE_MIGRAPHX_INTERFACE=OFF \ + -Donnxruntime_USE_MIGRAPHX=OFF \ + -Donnxruntime_DISABLE_RTTI=OFF \ + -Donnxruntime_DISABLE_EXCEPTIONS=OFF \ + -Donnxruntime_MINIMAL_BUILD=OFF \ + -Donnxruntime_ENABLE_LTO=OFF \ + -Donnxruntime_USE_ACL=OFF \ + -Donnxruntime_USE_ARMNN=OFF \ + -Donnxruntime_USE_JSEP=OFF \ + -Donnxruntime_USE_WEBGPU=OFF \ + -Donnxruntime_USE_EXTERNAL_DAWN=OFF \ + -Donnxruntime_WGSL_TEMPLATE=static \ + -Donnxruntime_ENABLE_TRAINING=OFF \ + -Donnxruntime_ENABLE_TRAINING_OPS=OFF \ + -Donnxruntime_ENABLE_TRAINING_APIS=OFF \ + -Donnxruntime_ENABLE_CPU_FP16_OPS=OFF \ + -Donnxruntime_USE_NCCL=OFF \ + -Donnxruntime_BUILD_BENCHMARKS=OFF \ + -Donnxruntime_USE_XNNPACK=OFF \ + -Donnxruntime_USE_WEBNN=OFF \ + -Donnxruntime_USE_CANN=OFF \ + -Donnxruntime_USE_CUDA=OFF \ + -Donnxruntime_USE_KLEIDIAI=OFF \ + -DCMAKE_INSTALL_PREFIX=$ONNX_DIR \ + -DCMAKE_BUILD_TYPE=Release \ + -Donnxruntime_USE_SYSTEM_PROTOBUF=ON \ + -DProtobuf_INCLUDE_DIR=/usr/local/include \ + -DProtobuf_LIBRARIES=/usr/local/lib/libprotobuf.a \ + -DProtobuf_PROTOC_EXECUTABLE=/opt/host-protoc/bin/protoc; \ + cmake --build $ONNX_DIR --config Release + +ENV ORT_STRATEGY=system +ENV ORT_LIB_LOCATION=$ONNX_DIR -RUN curl -sSf https://sh.rustup.rs | sh -s -- -y -ENV PATH="/root/.cargo/bin:\${PATH}" -RUN rustup target add aarch64-unknown-linux-gnu WORKDIR /work DOCKERFILE local build_args=(--pull) [[ "${DOCKER_NO_CACHE}" -eq 1 ]] && build_args+=(--no-cache) - echo "== Linux arm64 build (Docker image) ==" - docker build "${build_args[@]}" --platform=linux/arm64 \ - --build-arg CACHE_BUST="${CACHE_BUST}" \ - -f "$df" -t "$img" "$tmp" + echo "== Linux arm64 build (Docker image, cross-compiled on linux/amd64) ==" + if docker image inspect "$img" >/dev/null 2>&1; then + echo "Docker image '$img' already exists. Skipping build." + else + docker build "${build_args[@]}" --platform=linux/amd64 \ + --build-arg CACHE_BUST="${CACHE_BUST}" \ + -f "$df" -t "$img" "$tmp" + fi - echo "== Linux arm64 cargo builds (cpu + optional variants) ==" - docker run --rm --platform=linux/arm64 \ + echo "== Linux arm64 cargo builds (cpu=${WITH_CPU} vulkan=${WITH_VULKAN}) ==" + docker run --rm --platform=linux/amd64 \ -v "${PROJECT_ROOT}:/work" -w /work \ -v "${HOST_K_CACHE}:${CONT_K_CACHE}" \ -v "${HOST_WHISPER_MODELS}:${CONT_WHISPER_MODELS}" \ - -e LINUX_WITH_OPENBLAS="${LINUX_WITH_OPENBLAS}" \ - -e LINUX_WITH_VULKAN="${LINUX_WITH_VULKAN}" \ + -e WITH_CPU="${WITH_CPU}" \ + -e WITH_VULKAN="${WITH_VULKAN}" \ + -e CMAKE_SKIP_RPATH=ON \ + -e CMAKE_INSTALL_RPATH_USE_LINK_PATH=OFF \ "$img" \ bash -lc ' set -euo pipefail - target=aarch64-unknown-linux-gnu + + ARCH=arm64 + target=aarch64-unknown-linux-musl + + # rust-toolchain.toml pins the toolchain, but the image added the musl + # target to whatever was default at image build time (stable). Cargo + # switches to the pinned toolchain here and then finds no musl std. + # Re-add the target from inside /work so rustup reads rust-toolchain.toml. + cd /work + rustup target add "$target" + + + if [ ! -f /usr/local/lib/libasound.a ]; then + echo "--- Building ALSA static library for musl ---" + apt-get update -qq && apt-get install -y --no-install-recommends autoconf automake libtool + curl -sL -o /tmp/alsa.tar.gz \ + "https://github.com/alsa-project/alsa-lib/archive/refs/tags/v1.2.12.tar.gz" + tar xzf /tmp/alsa.tar.gz -C /tmp + mv /tmp/alsa-lib-1.2.12 /tmp/alsa-lib + cd /tmp/alsa-lib + autoreconf -fi + ./configure \ + --host=aarch64-linux-musl \ + --build=x86_64-linux-gnu \ + --prefix=/usr/local \ + --enable-shared=no \ + --enable-static=yes \ + --with-pkg-config-plugindir=/usr/local/lib/pkgconfig \ + CC=aarch64-linux-musl-gcc \ + CFLAGS="--sysroot=/opt/aarch64-linux-musl-cross/aarch64-linux-musl -O3" \ + LDFLAGS="-L/opt/aarch64-linux-musl-cross/aarch64-linux-musl/lib" + make -j$(nproc) + make install + cd / + rm -rf /tmp/alsa-lib /tmp/alsa.tar.gz + else + echo "--- ALSA already built, skipping ---" + fi + + if [ ! -f /usr/local/lib/libre2.a ]; then + echo "--- Building abseil + re2 static libraries for musl ---" + curl -sL -o /tmp/re2.tar.gz \ + "https://github.com/google/re2/archive/refs/tags/2024-07-02.tar.gz" + tar xzf /tmp/re2.tar.gz -C /tmp + mv /tmp/re2-2024-07-02 /tmp/re2 + + curl -sL -o /tmp/absl.tar.gz \ + "https://github.com/abseil/abseil-cpp/archive/refs/tags/20240722.0.tar.gz" + tar xzf /tmp/absl.tar.gz -C /tmp + mv /tmp/abseil-cpp-20240722.0 /tmp/absl + cmake -S /tmp/absl -B /tmp/absl-build \ + -DCMAKE_INSTALL_PREFIX=/usr/local \ + -DCMAKE_BUILD_TYPE=Release \ + -DBUILD_SHARED_LIBS=OFF \ + -DCMAKE_SYSTEM_NAME=Linux \ + -DCMAKE_SYSTEM_PROCESSOR=aarch64 \ + -DCMAKE_C_COMPILER=aarch64-linux-musl-gcc \ + -DCMAKE_CXX_COMPILER=aarch64-linux-musl-g++ \ + -DCMAKE_CXX_FLAGS="--sysroot=/opt/aarch64-linux-musl-cross/aarch64-linux-musl" \ + -DCMAKE_C_FLAGS="--sysroot=/opt/aarch64-linux-musl-cross/aarch64-linux-musl" \ + -DABSL_BUILD_TESTING=OFF + cmake --build /tmp/absl-build --parallel $(nproc) + cmake --install /tmp/absl-build --prefix /usr/local + + make -C /tmp/re2 -j$(nproc) \ + CXX=aarch64-linux-musl-g++ \ + CXXFLAGS="--sysroot=/opt/aarch64-linux-musl-cross/aarch64-linux-musl -O3 -static -I/usr/local/include" \ + LDFLAGS="-static -L/usr/local/lib" \ + AR=aarch64-linux-musl-ar \ + static + cp /tmp/re2/obj/libre2.a /usr/local/lib/ + mkdir -p $ONNX_DIR/_deps/re2-build/ + cp /usr/local/lib/libre2.a $ONNX_DIR/_deps/re2-build/ + rm -rf /tmp/re2 /tmp/re2.tar.gz /tmp/absl /tmp/absl.tar.gz /tmp/absl-build + else + echo "--- abseil/re2 already built, skipping ---" + fi build_variant() { local variant="$1" local feats="$2" - local ctd="/work/target-cross/linux-arm64-${variant}" - echo "---- Building linux/arm64 [$variant] features: $feats" - CARGO_TARGET_DIR="$ctd" cargo build --release --target "$target" --no-default-features --features "$feats" + local ctd="/work/target-cross/linux-${ARCH}-${variant}" + + echo "---- Building linux/${ARCH} [$variant] features: $feats" + export CARGO_PROFILE_RELEASE_CODEGEN_UNITS=1 + export CARGO_PROFILE_RELEASE_DEBUG=false + export CARGO_PROFILE_RELEASE_STRIP=symbols + export CARGO_PROFILE_RELEASE_INCREMENTAL=false + export CARGO_TARGET_AARCH64_UNKNOWN_LINUX_MUSL_LINKER=aarch64-linux-musl-g++ + export RUSTC_LINKER=aarch64-linux-musl-g++ + + ABSL_LIBS="" + for f in /usr/local/lib/libabsl_*.a; do + ABSL_LIBS="$ABSL_LIBS -C link-arg=$f" + done + + export RUSTFLAGS="-C target-feature=+crt-static -C codegen-units=1 -C opt-level=3 -C link-arg=-L/opt/aarch64-linux-musl-cross/aarch64-linux-musl/lib -C link-arg=-Wl,--start-group -C link-arg=/usr/local/lib/libopenblas.a -C link-arg=/usr/local/lib/libprotobuf.a -C link-arg=/usr/local/lib/libomp.a ${ABSL_LIBS} -C link-arg=-Wl,--end-group -C link-arg=-lm -C link-arg=-lc -C link-arg=-lgfortran -C link-arg=-lpthread -C link-arg=-lgcc" + + cd /work + CARGO_TARGET_DIR="$ctd" \ + cargo build --release --target "$target" --features "$feats" } - build_variant cpu "'"${FEATURES_CPU}"'" - - if [ "${LINUX_WITH_OPENBLAS}" = "1" ]; then - if [ -d /usr/include/aarch64-linux-gnu/openblas-pthread ]; then - export BLAS_INCLUDE_DIRS=/usr/include/aarch64-linux-gnu/openblas-pthread - elif [ -d /usr/include/aarch64-linux-gnu/openblas ]; then - export BLAS_INCLUDE_DIRS=/usr/include/aarch64-linux-gnu/openblas - elif [ -d /usr/include/openblas ]; then - export BLAS_INCLUDE_DIRS=/usr/include/openblas - else - export BLAS_INCLUDE_DIRS=/usr/include - fi - build_variant openblas "'"${FEATURES_OPENBLAS}"'" + # Scope the cross toolchain to the target triple for the cargo build and + # take it out of the generic environment. The image-wide CC/CFLAGS are + # needed by the C library builds above, but cargo also compiles build + # scripts and their deps for the HOST: with a global CC=aarch64-... , + # openssl-sys emitted aarch64 objects into the host artifact dir and the + # host link failed ("Relocations in generic ELF (EM: 183) ... file in + # wrong format"); a global CFLAGS handed host gcc the aarch64 sysroot so + # it could not find /usr/include (ring: "no include path ... stdint.h"). + # Target builds still resolve CC_/CFLAGS_. + t_="$(echo "$target" | tr - _)" + T_="$(echo "$target" | tr "a-z-" "A-Z_")" + export CFLAGS_${t_}="$CFLAGS" + export CXXFLAGS_${t_}="$CXXFLAGS" + export LDFLAGS_${t_}="$LDFLAGS" + # OPENSSL_DIR/PKG_CONFIG_PATH point at the aarch64 OpenSSL in /usr/local. + # openssl-sys reads them for HOST builds too, linking aarch64 libssl.a + # into a host rlib -> "Relocations in generic ELF (EM: 183) ... file in + # wrong format" when ort-sys built its build script. Scope them to the + # target; the host then uses the system OpenSSL from libssl-dev. + export ${T_}_OPENSSL_DIR="$OPENSSL_DIR" + export ${T_}_OPENSSL_LIB_DIR="$OPENSSL_LIB_DIR" + export ${T_}_OPENSSL_STATIC="$OPENSSL_STATIC" + export PKG_CONFIG_PATH_${t_}="$PKG_CONFIG_PATH" + export PKG_CONFIG_ALLOW_CROSS=1 + unset CFLAGS CXXFLAGS LDFLAGS CC CXX LD + unset OPENSSL_DIR OPENSSL_LIB_DIR OPENSSL_STATIC PKG_CONFIG_PATH + + if [ "${WITH_CPU}" = "1" ]; then + build_variant cpu "'"${FEATURES_CPU}"'" fi - if [ "${LINUX_WITH_VULKAN}" = "1" ]; then - if command -v glslc >/dev/null 2>&1; then - build_variant vulkan "'"${FEATURES_VULKAN}"'" - else - echo "WARN: glslc missing; skipping linux/arm64 vulkan variant" - fi + if [ "${WITH_VULKAN}" = "1" ]; then + build_variant vulkan "'"${FEATURES_VULKAN}"'" fi ' - linux_copy_out "arm64" "aarch64-unknown-linux-gnu" "cpu" - [[ "${LINUX_WITH_OPENBLAS}" == "1" ]] && linux_copy_out "arm64" "aarch64-unknown-linux-gnu" "openblas" - if [[ "${LINUX_WITH_VULKAN}" == "1" ]] && [[ -f "${PROJECT_ROOT}/target-cross/linux-arm64-vulkan/aarch64-unknown-linux-gnu/release/${BIN_NAME}" ]]; then - linux_copy_out "arm64" "aarch64-unknown-linux-gnu" "vulkan" + [[ "${WITH_CPU}" == "1" ]] && linux_copy_out "arm64" "aarch64-unknown-linux-musl" "cpu" + [[ "${WITH_VULKAN}" == "1" ]] && linux_copy_out "arm64" "aarch64-unknown-linux-musl" "vulkan" + true + + docker image rm -f "$img" >/dev/null 2>&1 || true + rm -rf "$tmp" >/dev/null 2>&1 || true +} + +# ========================================================== +# GLIBC variants (vulkan, cuda) - amd64 and arm64 +# +# These CANNOT be musl. The Vulkan loader, its ICD drivers and NVIDIA's +# libcuda.so.1 are glibc binaries on every mainstream distro, and a musl +# process cannot load them: statically it has no dlopen at all +# ("Dynamic loading not supported"), and dynamically the glibc .so fails to +# relocate ("__strncpy_chk: symbol not found"). So the GPU variants are built +# against glibc, with everything of ours still linked statically - only libc, +# the GPU loaders, and ONNX Runtime stay dynamic. +# +# ONNX Runtime comes from Microsoft's prebuilt package rather than source: +# it removes a multi-hour build, and for CUDA it is the only practical option +# (the from-source CUDA EP build does not fit CI time limits at all). +# ========================================================== +build_linux_glibc_variant() { + local arch="$1" variant="$2" + local tmp df img target ort_url ort_dir + + local plat multiarch + case "${arch}" in + amd64) target="x86_64-unknown-linux-gnu"; plat="linux/amd64" + multiarch="x86_64-linux-gnu" ;; + # No cross toolchain here: this leg runs on a native arm64 runner, so the + # image and the build are both aarch64. + arm64) target="aarch64-unknown-linux-gnu"; plat="linux/arm64" + multiarch="aarch64-linux-gnu" ;; + *) echo "ERROR: unsupported glibc arch ${arch}"; return 1 ;; + esac + + # CUDA 12 to match Ubuntu's nvidia-cuda-toolkit, so the ORT package and the + # ggml/whisper CUDA code agree on a major version. + case "${arch}-${variant}" in + amd64-vulkan) ort_url="https://github.com/microsoft/onnxruntime/releases/download/v1.24.1/onnxruntime-linux-x64-1.24.1.tgz"; ort_dir="onnxruntime-linux-x64-1.24.1" ;; + arm64-vulkan) ort_url="https://github.com/microsoft/onnxruntime/releases/download/v1.24.1/onnxruntime-linux-aarch64-1.24.1.tgz"; ort_dir="onnxruntime-linux-aarch64-1.24.1" ;; + amd64-cuda) ort_url="https://github.com/microsoft/onnxruntime/releases/download/v1.24.1/onnxruntime-linux-x64-gpu-1.24.1.tgz"; ort_dir="onnxruntime-linux-x64-gpu-1.24.1" ;; + *) echo "ERROR: no prebuilt ONNX Runtime for ${arch}-${variant}"; return 1 ;; + esac + + tmp="$(mktemp -d)" + df="${tmp}/Dockerfile.linux.glibc.${arch}.${variant}" + img="local/${BIN_NAME}-linux-glibc-${arch}-${variant}:cache" + + cat > "$df" <<'DOCKERFILE' +FROM ubuntu:24.04 +ENV DEBIAN_FRONTEND=noninteractive +ARG ARCH +ARG VARIANT +ARG ORT_URL +ARG ORT_DIR + +# noble, not jammy: ggml's Vulkan backend needs glslc to compile its shaders +# and 22.04 ships only glslang-tools. The cost is a glibc 2.39 floor. +RUN apt-get update && apt-get install -y --no-install-recommends \ + build-essential pkg-config curl wget ca-certificates git \ + cmake python3 gfortran \ + libssl-dev zlib1g-dev libasound2-dev \ + libopenblas-dev libclang-dev clang \ + libvulkan-dev glslc \ + && rm -rf /var/lib/apt/lists/* + +RUN if [ "$VARIANT" = "cuda" ]; then \ + apt-get update && apt-get install -y --no-install-recommends nvidia-cuda-toolkit && \ + rm -rf /var/lib/apt/lists/* ; \ + fi + +# cuDNN for the ONNX Runtime CUDA execution provider - not part of the toolkit. +ARG CUDNN_VERSION=9.16.0.29 +RUN if [ "$VARIANT" = "cuda" ]; then \ + set -eux; \ + f=cudnn-linux-x86_64-${CUDNN_VERSION}_cuda12-archive; \ + wget -nv --timeout=60 --tries=3 -O /tmp/cudnn.tar.xz \ + "https://developer.download.nvidia.com/compute/cudnn/redist/cudnn/linux-x86_64/$f.tar.xz"; \ + mkdir -p /usr/local/cudnn; \ + tar -xJf /tmp/cudnn.tar.xz -C /tmp; \ + cp -a /tmp/$f/include /usr/local/cudnn/; \ + cp -a /tmp/$f/lib /usr/local/cudnn/; \ + rm -rf /tmp/cudnn.tar.xz /tmp/$f; \ + test -f /usr/local/cudnn/include/cudnn.h; \ + fi +ENV CUDNN_PATH=/usr/local/cudnn + +# Prebuilt ONNX Runtime (shared). Its .so files ship beside the binary. +RUN set -eux; \ + wget -nv --timeout=60 --tries=3 -O /tmp/ort.tgz "$ORT_URL"; \ + mkdir -p /opt; \ + tar xzf /tmp/ort.tgz -C /opt; \ + rm -f /tmp/ort.tgz; \ + ln -sfn "/opt/${ORT_DIR}" /opt/onnxruntime; \ + test -f /opt/onnxruntime/lib/libonnxruntime.so +ENV ORT_STRATEGY=system +ENV ORT_LIB_LOCATION=/opt/onnxruntime/lib +ENV ORT_PREFER_DYNAMIC_LINK=1 +ENV ONNXRUNTIME_INCLUDE_DIR=/opt/onnxruntime/include +ENV ONNXRUNTIME_LIB_DIR=/opt/onnxruntime/lib + +# whisper-rs-sys requires BLAS_INCLUDE_DIRS when built with OpenBLAS, and the +# Debian layout puts the headers in the multiarch directory. +# +# The static archive is copied somewhere that holds no .so: apt ships both +# libopenblas.a and libopenblas.so in the same directory, and the plain +# "-lopenblas" that whisper-rs-sys emits would otherwise resolve to the shared +# one. A -L path is searched before the default directories, so this keeps +# OpenBLAS statically linked. +ARG MULTIARCH +RUN set -eux; \ + mkdir -p /opt/blas-static; \ + cp "/usr/lib/${MULTIARCH}/libopenblas.a" /opt/blas-static/; \ + test -f /usr/include/${MULTIARCH}/cblas.h +ENV BLAS_INCLUDE_DIRS=/usr/include/${MULTIARCH} +ENV BLAS_LIBRARIES=/opt/blas-static/libopenblas.a +ENV OPENBLAS_PATH=/usr + +RUN curl -sSf https://sh.rustup.rs | sh -s -- -y +ENV PATH=/root/.cargo/bin:$PATH + +RUN d="$(dirname "$(find /usr/lib -name 'libclang.so*' 2>/dev/null | head -1)")"; \ + test -n "$d" -a -d "$d"; \ + ln -sfn "$d" /usr/local/libclang +ENV LIBCLANG_PATH=/usr/local/libclang + +WORKDIR /work +DOCKERFILE + + local build_args=(--pull) + [[ "${DOCKER_NO_CACHE}" -eq 1 ]] && build_args+=(--no-cache) + + echo "== Linux ${arch} ${variant} (glibc) image ==" + docker build "${build_args[@]}" --platform="${plat}" \ + --build-arg ARCH="${arch}" \ + --build-arg VARIANT="${variant}" \ + --build-arg ORT_URL="${ort_url}" \ + --build-arg ORT_DIR="${ort_dir}" \ + --build-arg MULTIARCH="${multiarch}" \ + -f "$df" -t "$img" "$tmp" + + local feats + case "${variant}" in + vulkan) feats="${FEATURES_VULKAN}" ;; + cuda) feats="${FEATURES_CUDA}" ;; + esac + + echo "== Linux ${arch} ${variant} (glibc) cargo build ==" + docker run --rm --platform="${plat}" \ + -v "${PROJECT_ROOT}:/work" -w /work \ + -v "${HOST_K_CACHE}:${CONT_K_CACHE}" \ + -v "${HOST_WHISPER_MODELS}:${CONT_WHISPER_MODELS}" \ + -e TARGET="${target}" \ + -e FEATS="${feats}" \ + -e ARCH="${arch}" \ + -e VARIANT="${variant}" \ + "$img" \ + bash -lc ' + set -euo pipefail + cd /work + rustup target add "$TARGET" + + ctd="/work/target-cross/linux-${ARCH}-${VARIANT}" + + # Not +crt-static: this binary must be able to dlopen the Vulkan/CUDA + # loaders the user installs. Our own libraries still link statically; + # $ORIGIN lets it find the ONNX Runtime .so shipped alongside it. + export RUSTFLAGS="-C codegen-units=1 -C opt-level=3 \ + -C link-arg=-Wl,-rpath,\$ORIGIN \ + -C link-arg=-L/opt/onnxruntime/lib \ + -L native=/opt/blas-static \ + -C link-arg=-lgfortran" + + export CARGO_PROFILE_RELEASE_CODEGEN_UNITS=1 + export CARGO_PROFILE_RELEASE_DEBUG=false + export CARGO_PROFILE_RELEASE_STRIP=symbols + export CARGO_PROFILE_RELEASE_INCREMENTAL=false + + CARGO_TARGET_DIR="$ctd" cargo build --release --target "$TARGET" --features "$FEATS" + ' + + # binary + the ONNX Runtime shared libraries it needs at run time + local src_dir="${PROJECT_ROOT}/target-cross/linux-${arch}-${variant}/${target}/release" + local out_dir="${DIST_DIR}/${BIN_NAME}-${VERSION}-linux-${arch}-${variant}-glibc" + if [[ -f "${src_dir}/${BIN_NAME}" ]]; then + rm -rf "${out_dir}"; mkdir -p "${out_dir}" + cp "${src_dir}/${BIN_NAME}" "${out_dir}/" + chmod +x "${out_dir}/${BIN_NAME}" || true + docker run --rm --platform="${plat}" -v "${out_dir}:/out" "$img" \ + bash -lc 'cp -a /opt/onnxruntime/lib/libonnxruntime*.so* /out/ 2>/dev/null || true' + add_artifact "${out_dir}" + echo "✔ Built: ${out_dir}" + else + echo "ERROR: ${src_dir}/${BIN_NAME} not produced" + return 1 fi docker image rm -f "$img" >/dev/null 2>&1 || true rm -rf "$tmp" >/dev/null 2>&1 || true } -# Run +# ----------------------------- +# Run builds +# ----------------------------- ensure_espeak_data_archive -if want_arch amd64; then build_linux_amd64_docker_variants; fi -if want_arch arm64; then build_linux_arm64_docker_variants; fi +# musl covers cpu only. vulkan/cuda cannot be musl - the GPU loaders the user +# installs are glibc - so they go through build_linux_glibc_variant. +_wv="${WITH_VULKAN}"; _wc="${WITH_CUDA}" +WITH_VULKAN=0; WITH_CUDA=0 +if [[ "${WITH_CPU}" == "1" ]]; then + if want_arch amd64; then build_linux_amd64_variants; fi + if want_arch arm64; then build_linux_arm64_variants; fi +fi +WITH_VULKAN="${_wv}"; WITH_CUDA="${_wc}" + +if [[ "${WITH_VULKAN}" == "1" ]]; then + want_arch amd64 && build_linux_glibc_variant amd64 vulkan + want_arch arm64 && build_linux_glibc_variant arm64 vulkan +fi +if [[ "${WITH_CUDA}" == "1" ]]; then + want_arch amd64 && build_linux_glibc_variant amd64 cuda +fi +true + +# ----------------------------- +# Check static build +# ----------------------------- +for f in dist/${BIN_NAME}-*-linux-*; do + [[ -f "$f" ]] || continue + echo "Checking $f" + + if ldd "$f" 2>&1 | grep -q "not a dynamic"; then + echo "✔ Statically linked (ldd says not a dynamic ELF)" + else + echo "ldd output (libvulkan.so.1 is expected/allowed on vulkan builds):" + ldd "$f" || true + fi + + if nm "$f" 2>/dev/null | grep -q "openblas"; then + echo "✔ OpenBLAS symbols found (static link confirmed)" + else + echo "⚠ No OpenBLAS symbols found in $f" + fi + + echo "---------------------------------" +done -# Package +# ----------------------------- +# Packaging +# ----------------------------- if [[ "${DO_PACKAGE}" -eq 1 ]]; then echo "== Packaging tar.gz + SHA256 ==" for f in "${ARTIFACTS[@]}"; do diff --git a/build_macos.sh b/build_macos.sh index dbb7056..d44ebb1 100755 --- a/build_macos.sh +++ b/build_macos.sh @@ -4,30 +4,27 @@ set -euo pipefail BIN_NAME="vtmate" PROJECT_ROOT="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" DIST_DIR="${PROJECT_ROOT}/dist" -PKG_DIR="${DIST_DIR}/packages" ASSETS_DIR="${PROJECT_ROOT}/assets" ESPEAK_ARCHIVE="${ASSETS_DIR}/espeak-ng-data.tar.gz" -DO_PACKAGE=1 - -# macOS optional -MAC_WITH_OPENBLAS="${MAC_WITH_OPENBLAS:-0}" - usage() { cat <<'USAGE' Usage: - ./build_macos.sh [--skip-package] + ./build_macos.sh [--arch arm64|x86_64] -Env: - MAC_WITH_OPENBLAS=0|1 (macos) default 0 Notes: - - macOS builds always enable Metal. + - macOS build + - Metal enabled + - No OpenBLAS + - Produces a single binary USAGE } +ARCH_SEL="" + while [[ $# -gt 0 ]]; do case "$1" in - --skip-package) DO_PACKAGE=0; shift ;; + --arch) ARCH_SEL="${2-}"; shift 2 ;; -h|--help) usage; exit 0 ;; *) echo "Unknown arg: $1"; usage; exit 1 ;; esac @@ -46,52 +43,24 @@ VERSION="$( )" [[ -n "${VERSION}" ]] || { echo "Failed to read version from Cargo.toml"; exit 1; } -mkdir -p "${DIST_DIR}" "${PKG_DIR}" "${PROJECT_ROOT}/target-cross" "${ASSETS_DIR}" +mkdir -p "${DIST_DIR}" "${ASSETS_DIR}" echo "Version: ${VERSION}" -echo "macOS: Metal always, MAC_WITH_OPENBLAS=${MAC_WITH_OPENBLAS}" -FEATURES_COMMON="whisper-logs" -FEATURES_MACOS_METAL="${FEATURES_COMMON},whisper-metal" -FEATURES_MACOS_METAL_OPENBLAS="${FEATURES_COMMON},whisper-metal,whisper-openblas" -sha256_file() { - local file="$1" out="$2" - if command -v shasum >/dev/null 2>&1; then - (cd "$(dirname "$file")" && shasum -a 256 "$(basename "$file")") > "$out" - return 0 - fi - if command -v openssl >/dev/null 2>&1; then - local line hash - line="$(openssl dgst -sha256 "$file")" - hash="${line##* }" - echo "${hash} $(basename "$file")" > "$out" - return 0 - fi - echo "ERROR: No SHA256 tool found." - exit 1 -} -make_tgz() { local src="$1" tgz="$2"; tar -C "$(dirname "$src")" -czf "$tgz" "$(basename "$src")"; } -package_one() { - local src="$1" - [[ -f "$src" ]] || return 0 - local base tgz sha - base="$(basename "$src")" - tgz="${PKG_DIR}/${base}.tar.gz" - sha="${PKG_DIR}/${base}.tar.gz.sha256" - make_tgz "$src" "$tgz" - sha256_file "$tgz" "$sha" -} +# --- Embedded eSpeak asset generation --- -# Embedded eSpeak asset generation (via Docker if missing) docker_ok=0 command -v docker >/dev/null 2>&1 && docker_ok=1 + ensure_espeak_data_archive() { if [[ -f "${ESPEAK_ARCHIVE}" ]]; then echo "✔ Found embedded asset: ${ESPEAK_ARCHIVE}" return 0 fi + echo "== Generating embedded asset: ${ESPEAK_ARCHIVE} ==" + if [[ "$docker_ok" -ne 1 ]]; then echo "ERROR: Docker not found and ${ESPEAK_ARCHIVE} is missing." exit 1 @@ -136,37 +105,54 @@ ensure_espeak_data_archive command -v cargo >/dev/null 2>&1 || { echo "ERROR: cargo not found"; exit 1; } -ARTIFACTS=() -add_artifact() { [[ -f "$1" ]] && ARTIFACTS+=("$1"); } - -arch="$(uname -m)" +arch="${ARCH_SEL:-$(uname -m)}" -echo "== macOS build [metal] features: ${FEATURES_MACOS_METAL} ==" -cargo build --release --no-default-features --features "${FEATURES_MACOS_METAL}" -out="${DIST_DIR}/${BIN_NAME}-${VERSION}-macos-${arch}-metal" -cp "${PROJECT_ROOT}/target/release/${BIN_NAME}" "$out" -chmod +x "$out" || true -add_artifact "$out" -echo "✔ Built: $out" - -if [[ "${MAC_WITH_OPENBLAS}" == "1" ]]; then - echo "== macOS build [metal-openblas] features: ${FEATURES_MACOS_METAL_OPENBLAS} ==" - CARGO_TARGET_DIR="${PROJECT_ROOT}/target-cross/macos-${arch}-metal-openblas" \ - cargo build --release --no-default-features --features "${FEATURES_MACOS_METAL_OPENBLAS}" - out="${DIST_DIR}/${BIN_NAME}-${VERSION}-macos-${arch}-metal-openblas" - cp "${PROJECT_ROOT}/target-cross/macos-${arch}-metal-openblas/release/${BIN_NAME}" "$out" - chmod +x "$out" || true - add_artifact "$out" - echo "✔ Built: $out" -fi +case "${arch}" in + arm64|aarch64) arch="arm64"; RUST_TARGET="aarch64-apple-darwin" ;; + x86_64|amd64) arch="x86_64"; RUST_TARGET="x86_64-apple-darwin" ;; + *) echo "ERROR: unsupported --arch '${arch}' (use arm64 or x86_64)"; exit 1 ;; +esac -if [[ "${DO_PACKAGE}" -eq 1 ]]; then - echo "== Packaging tar.gz + SHA256 ==" - for f in "${ARTIFACTS[@]}"; do - package_one "$f" - done +# ggml's Metal backend targets Apple GPUs; on Intel Macs it is unsupported or +# slower than the CPU path, so Metal is gated on arm64. +if [[ "${arch}" == "arm64" ]]; then + FEATURES="whisper-metal" else - echo "Skipping packaging (--skip-package)" + FEATURES="" fi +# Apple Silicon hosts can emit x86_64 directly - the SDK carries both slices - +# so the Intel build no longer needs an Intel runner. +rustup target add "${RUST_TARGET}" >/dev/null 2>&1 || true + +echo "macOS build: ${arch} (${RUST_TARGET}, features: ${FEATURES:-none})" + +export MACOSX_DEPLOYMENT_TARGET=11.0 + +export CARGO_PROFILE_RELEASE_LTO=false +export CARGO_PROFILE_RELEASE_CODEGEN_UNITS=1 +export CARGO_PROFILE_RELEASE_DEBUG=false +export CARGO_PROFILE_RELEASE_STRIP=symbols +export CARGO_PROFILE_RELEASE_INCREMENTAL=false + +echo "== Building macOS (${arch}) with features: ${FEATURES:-none} ==" + +# espeak-ng's phoneme compiler holds paths in a fixed ~180-byte buffer and +# silently truncates past it ("Bad vowel file: vwl_en_us_nyc/a_raised" as it +# looks for .../a_ra). Adding --target inserts the triple into the path, which +# alone pushed the old target-cross/macos- layout over that limit, so +# keep the build directory short and outside the project tree. +CARGO_TARGET_DIR="${TARGET_ROOT:-${HOME}/t/${arch}}" +mkdir -p "${CARGO_TARGET_DIR}" + +CARGO_TARGET_DIR="${CARGO_TARGET_DIR}" \ +cargo build --release \ + --target "${RUST_TARGET}" \ + --features "${FEATURES}" + +out="${DIST_DIR}/${BIN_NAME}-${VERSION}-macos-${arch}" +cp "${CARGO_TARGET_DIR}/${RUST_TARGET}/release/${BIN_NAME}" "$out" +chmod +x "$out" || true + +echo "✔ Built: $out" echo "✔ macOS build complete" diff --git a/build_windows.ps1 b/build_windows.ps1 new file mode 100755 index 0000000..1299bd6 --- /dev/null +++ b/build_windows.ps1 @@ -0,0 +1,1842 @@ +# ========================================================== +# PowerShell Build Script (MSVC) +# ========================================================== +param( + [string]$VARIANT = "cpu" +) + +Set-StrictMode -Version Latest +$ErrorActionPreference = "Stop" + +# ========================================================== +# CONFIG +# ========================================================== +$BIN_BASE = "vtmate" +$PROJECT_ROOT = Split-Path -Parent $MyInvocation.MyCommand.Path +$DIST_DIR = Join-Path $PROJECT_ROOT "dist" +$TARGET_DIR = Join-Path $PROJECT_ROOT "target-cross" +$VENDOR_DIR = Join-Path $PROJECT_ROOT "vendor" + +# cargo's own target dir. whisper-rs-sys' nested CMake ExternalProject for +# ggml-vulkan's shader compiler builds many directories deep under it (e.g. +# .../build/whisper-rs-sys-/out/build/.../CMakeFiles/CMakeScratch/ +# TryCompile-/cmTC_.dir/Debug/...), and the hash segments cargo +# and CMake generate vary in length between runs - keeping this near the +# drive root instead of under PROJECT_ROOT\target maximizes the headroom +# under Windows' 260-char MAX_PATH. +$env:CARGO_TARGET_DIR = Join-Path (Split-Path -Qualifier $PROJECT_ROOT) "c" + +# MSBuild's FileTracker component (the .tlog bookkeeping files under +# cmTC_*.dir\...) does not support long paths at all, independent of +# CARGO_TARGET_DIR or the OS's own long-path support. Tracking is a pure +# incremental-rebuild optimization, irrelevant to a one-shot CI build, so +# disable it outright rather than depend on path-length budgets holding. +$env:TrackFileAccess = "false" + +$ESPEAK_SRC = Join-Path $VENDOR_DIR "espeak-ng" +$ESPEAK_BUILD = Join-Path $ESPEAK_SRC "build-msvc" +$ESPEAK_INSTALL = Join-Path $ESPEAK_BUILD "install" + +$PROTOC_SRC = Join-Path $PROJECT_ROOT "protobuf" +$PROTOC_BUILD = Join-Path $PROJECT_ROOT "protobuf\build" +$PROTOC_INSTALL = Join-Path $PROJECT_ROOT "protobuf\install" + +$OPENBLAS_DIR = Join-Path $VENDOR_DIR "openblas" + +$ONNX_SRC = Join-Path $VENDOR_DIR "onnxruntime" +$ONNX_BUILD = Join-Path $ONNX_SRC "build-static" +$UPLOAD_ENABLED = $true + +# ========================================================== +# CLEAN OLD BUILDS +# ========================================================== +Remove-Item -Recurse -Force -ErrorAction SilentlyContinue $OPENBLAS_DIR, $ONNX_SRC, $ONNX_BUILD, $env:CARGO_TARGET_DIR, $TARGET_DIR, $DIST_DIR + +# ========================================================== +# LOCATE VISUAL STUDIO / LOAD MSVC ENVIRONMENT +# Located via vswhere rather than a hardcoded install path: the edition +# and version of Visual Studio on the GitHub runners changes without +# notice (windows-latest moved off VS2022 Enterprise), but vswhere itself +# lives at a fixed location on every machine that has VS installed. +# ========================================================== +$vswhere = Join-Path ${env:ProgramFiles(x86)} "Microsoft Visual Studio\Installer\vswhere.exe" +if (-not (Test-Path $vswhere)) { + Write-Error "vswhere.exe not found at $vswhere - is Visual Studio installed?" + exit 1 +} + +$vsArgs = @("-latest", "-products", "*", + "-requires", "Microsoft.VisualStudio.Component.VC.Tools.x86.x64") +$VS_PATH = & $vswhere @vsArgs -property installationPath +if (-not $VS_PATH) { + Write-Error "No Visual Studio install with the C++ x64 toolset was found." + exit 1 +} +$VS_VERSION = & $vswhere @vsArgs -property installationVersion +$VS_MAJOR = ($VS_VERSION -split '\.')[0] +Write-Host "Visual Studio: $VS_PATH (version $VS_VERSION)" + +if (-not (Get-Command cl.exe -ErrorAction SilentlyContinue)) { + Write-Host "=== Loading MSVC environment (x64) ===" + + $vsdev = Join-Path $VS_PATH "Common7\Tools\VsDevCmd.bat" + if (-not (Test-Path $vsdev)) { + Write-Error "VsDevCmd.bat not found at $vsdev" + exit 1 + } + + # Import the environment VsDevCmd sets up into this PowerShell session. + cmd /c "`"$vsdev`" -arch=amd64 -host_arch=amd64 && set" | ForEach-Object { + if ($_ -match "^(.*?)=(.*)$") { + Set-Item -Path "Env:$($matches[1])" -Value $matches[2] + } + } + + if (-not (Get-Command cl.exe -ErrorAction SilentlyContinue)) { + Write-Error "Loaded VsDevCmd from $vsdev but cl.exe is still not on PATH." + exit 1 + } +} +Write-Host "cl.exe: $((Get-Command cl.exe).Source)" + +# ========================================================== +# CHECK REQUIRED TOOLS +# ========================================================== +foreach ($tool in "cl.exe","cmake","git","cargo","powershell") { + if (-not (Get-Command $tool -ErrorAction SilentlyContinue)) { + Write-Error "ERROR: Required tool $tool not found." + exit 1 + } +} + +# ========================================================== +# RESOLVE CMAKE GENERATOR +# The generator name is version-specific ("Visual Studio 17 2022", +# "Visual Studio 18 ...") and the runner image upgrades VS without warning, +# so ask the installed CMake which generator matches the installed VS major +# version instead of hardcoding one. +# ========================================================== +$genMatch = cmake --help | + Select-String -Pattern "Visual Studio $VS_MAJOR [0-9]{4}" | + Select-Object -First 1 +if (-not $genMatch) { + Write-Error "Installed CMake has no generator for Visual Studio $VS_MAJOR. Upgrade CMake." + exit 1 +} +$CMAKE_GENERATOR = $genMatch.Matches[0].Value +Write-Host "CMake generator: $CMAKE_GENERATOR" + +# cmake-rs (espeak-rs-sys, whisper-rs-sys) reads CMAKE_GENERATOR from the +# environment and supplies -Ax64 / -Thost=x64 itself, so this single export +# keeps the crate sub-builds on the same toolchain as ours. +$env:CMAKE_GENERATOR = $CMAKE_GENERATOR + +$env:CARGO_BUILD_JOBS = 1 + +# ========================================================== +# DETERMINE VARIANT +# ========================================================== +switch ($VARIANT) { + "cpu" { + $WITH_OPENBLAS = $true + $WITH_CUDA = $false + $WITH_VULKAN = $false + } + "vulkan" { + $WITH_OPENBLAS = $true + $WITH_CUDA = $false + $WITH_VULKAN = $true + } + "cuda" { + $WITH_OPENBLAS = $true + $WITH_CUDA = $true + $WITH_VULKAN = $false + } + default { + Write-Error "ERROR: Unknown variant $VARIANT" + exit 1 + } +} + +Write-Host "`n============================================" +Write-Host "Building variant: $VARIANT" +if ($WITH_OPENBLAS) { Write-Host "OpenBLAS: ENABLED" } +if ($WITH_CUDA) { Write-Host "CUDA: ENABLED" } +if ($WITH_VULKAN) { Write-Host "Vulkan: ENABLED" } +Write-Host "============================================`n" + +# ========================================================== +# CREATE REQUIRED DIRECTORIES +# ========================================================== +foreach ($dir in $TARGET_DIR, $DIST_DIR, $VENDOR_DIR) { + if (-not (Test-Path $dir)) { New-Item -ItemType Directory -Force -Path $dir | Out-Null } +} + +# ========================================================== +# ENSURE CUDA TOOLKIT IF REQUIRED (BUILD-TIME) +# ========================================================== +if ($WITH_CUDA) { + $nvcc = Get-Command nvcc -ErrorAction SilentlyContinue + if (-not $nvcc) { + Write-Host "CUDA not detected. Installing CUDA Toolkit for build..." + # CUDA 12.3 rejects MSVC >= 19.40 (crt/host_config.h), so it cannot + # build against VS 2026 on current runners. 13.3 allows VS 2019-2026. + $CUDA_VERSION = "13.3.0" + $CUDA_MM = "13.3" + $cuda_root = "C:\Program Files\NVIDIA GPU Computing Toolkit\CUDA\v$CUDA_MM" + $CUDA_INSTALLER = "$env:TEMP\cuda_installer.exe" + $CUDA_URL = "https://developer.download.nvidia.com/compute/cuda/$CUDA_VERSION/network_installers/cuda_${CUDA_VERSION}_windows_network.exe" + + Invoke-WebRequest -Uri $CUDA_URL -OutFile $CUDA_INSTALLER -UseBasicParsing + + if (-not (Test-Path $CUDA_INSTALLER)) { + Write-Error "Failed to download CUDA installer." + exit 1 + } + + # -Wait blocks forever if the installer stalls or prompts. In CI that + # burns the whole job timeout with no output, so bound it explicitly. + $arguments = "--silent --toolkit --installpath `"$cuda_root`"" + $proc = Start-Process -FilePath $CUDA_INSTALLER -ArgumentList $arguments -PassThru + if (-not $proc.WaitForExit(45 * 60 * 1000)) { + try { $proc.Kill() } catch {} + Write-Error "CUDA installer timed out after 45 minutes (network installer stalled?)" + exit 1 + } + if ($proc.ExitCode -ne 0) { + Write-Error "CUDA installation failed with exit code $($proc.ExitCode)" + exit 1 + } + + # Set environment variables + $env:CUDA_PATH = $cuda_root + $env:CUDAToolkit_ROOT = $cuda_root + $env:Path = "$cuda_root\bin;$env:Path" + + # Verify nvcc + if (-not (Get-Command nvcc -ErrorAction SilentlyContinue)) { + Write-Error "CUDA installed but nvcc not found in PATH." + exit 1 + } + + Write-Host "CUDA successfully installed for build." + } + else { + Write-Host "CUDA already present." + $cuda_root = Split-Path -Parent (Split-Path -Parent $nvcc.Source) + $env:CUDA_PATH = $cuda_root + $env:CUDAToolkit_ROOT = $cuda_root + $env:Path = "$cuda_root\bin;$env:Path" + Write-Host "CUDA_PATH = $env:CUDA_PATH" + } + + # ------------------------------------------------------ + # cuDNN (required by the ONNX Runtime CUDA execution provider). + # Pulled from NVIDIA's public redist mirror - no developer login needed. + # ------------------------------------------------------ + if (-not $env:CUDNN_HOME -or -not (Test-Path $env:CUDNN_HOME)) { + # The _cudaXX suffix must match the installed CUDA major version. + # This tracks CUDA 13.x above; going back to CUDA 12.x means _cuda12. + $CUDNN_VERSION = "9.16.0.29" + $CUDNN_NAME = "cudnn-windows-x86_64-${CUDNN_VERSION}_cuda13-archive" + $CUDNN_URL = "https://developer.download.nvidia.com/compute/cudnn/redist/cudnn/windows-x86_64/$CUDNN_NAME.zip" + $CUDNN_ZIP = "$env:TEMP\cudnn.zip" + $CUDNN_ROOT = "C:\cudnn\$CUDNN_VERSION" + + Write-Host "cuDNN not detected. Downloading $CUDNN_NAME ..." + Invoke-WebRequest -Uri $CUDNN_URL -OutFile $CUDNN_ZIP -UseBasicParsing + if (-not (Test-Path $CUDNN_ZIP)) { + Write-Error "Failed to download cuDNN." + exit 1 + } + + New-Item -ItemType Directory -Force -Path (Split-Path -Parent $CUDNN_ROOT) | Out-Null + Expand-Archive -Path $CUDNN_ZIP -DestinationPath (Split-Path -Parent $CUDNN_ROOT) -Force + # The archive unpacks to / - normalise to a version-only path. + $extracted = Join-Path (Split-Path -Parent $CUDNN_ROOT) $CUDNN_NAME + if (Test-Path $CUDNN_ROOT) { Remove-Item -Recurse -Force $CUDNN_ROOT } + Rename-Item -Path $extracted -NewName $CUDNN_VERSION -Force + Remove-Item -Force $CUDNN_ZIP + + $env:CUDNN_HOME = $CUDNN_ROOT + Write-Host "cuDNN installed to $env:CUDNN_HOME" + } + else { + Write-Host "cuDNN already present." + } + + $env:CUDNN_PATH = $env:CUDNN_HOME + $env:Path = "$env:CUDNN_HOME\bin;$env:Path" + + $CUDNN_LIB_DIR = Join-Path $env:CUDNN_HOME "lib\x64" + if (-not (Test-Path (Join-Path $CUDNN_LIB_DIR "cudnn.lib"))) { + Write-Error "cudnn.lib not found under $CUDNN_LIB_DIR" + exit 1 + } + Write-Host "CUDNN_HOME = $env:CUDNN_HOME" +} +else { + Remove-Item Env:CUDAToolkit_ROOT -ErrorAction SilentlyContinue + Remove-Item Env:CUDA_PATH -ErrorAction SilentlyContinue + Remove-Item Env:CUDA_HOME -ErrorAction SilentlyContinue + Remove-Item Env:CUDA_ROOT -ErrorAction SilentlyContinue +} + +# ========================================================== +# ENSURE VULKAN SDK IF REQUIRED (BUILD-TIME) +# ggml-vulkan needs the loader import lib *and* glslc to compile its +# shaders; neither is present on a stock windows-latest runner. +# ========================================================== +if ($WITH_VULKAN) { + $sdkOk = $env:VULKAN_SDK -and (Test-Path $env:VULKAN_SDK) + if (-not $sdkOk) { + Write-Host "Vulkan SDK not detected. Installing Vulkan SDK for build..." + $VULKAN_VERSION = "1.3.296.0" + $vulkan_root = "C:\VulkanSDK\$VULKAN_VERSION" + $VULKAN_INSTALLER = "$env:TEMP\vulkan_sdk.exe" + $VULKAN_URL = "https://sdk.lunarg.com/sdk/download/$VULKAN_VERSION/windows/VulkanSDK-$VULKAN_VERSION-Installer.exe" + + Invoke-WebRequest -Uri $VULKAN_URL -OutFile $VULKAN_INSTALLER -UseBasicParsing + if (-not (Test-Path $VULKAN_INSTALLER)) { + Write-Error "Failed to download Vulkan SDK installer." + exit 1 + } + + $arguments = "--root `"$vulkan_root`" --accept-licenses --default-answer --confirm-command install" + $proc = Start-Process -FilePath $VULKAN_INSTALLER -ArgumentList $arguments -Wait -PassThru + if ($proc.ExitCode -ne 0) { + Write-Error "Vulkan SDK installation failed with exit code $($proc.ExitCode)" + exit 1 + } + + $env:VULKAN_SDK = $vulkan_root + Write-Host "Vulkan SDK successfully installed for build." + } + else { + Write-Host "Vulkan SDK already present." + } + + $env:Path = "$env:VULKAN_SDK\Bin;$env:Path" + Write-Host "VULKAN_SDK = $env:VULKAN_SDK" + + # ggml-vulkan shells out to glslc at build time - fail loudly here rather + # than 40 minutes later inside whisper-rs-sys. + if (-not (Get-Command glslc -ErrorAction SilentlyContinue)) { + Write-Error "Vulkan SDK installed but glslc not found in PATH." + exit 1 + } +} +else { + Remove-Item Env:VULKAN_SDK -ErrorAction SilentlyContinue +} + +# ========================================================== +# BUILD ESPEAK-NG STATIC +# ========================================================== +$ESPEAK_LIB = Join-Path $ESPEAK_INSTALL "lib" "espeak-ng.lib" + +if (-not (Test-Path $ESPEAK_LIB)) { + + Write-Host "" + Write-Host "=== Building eSpeak NG (MSVC) ===" + + # Clone repository if source doesn't exist + if (-not (Test-Path $ESPEAK_SRC)) { + New-Item -ItemType Directory -Force -Path $VENDOR_DIR | Out-Null + git clone https://github.com/espeak-ng/espeak-ng $ESPEAK_SRC + if ($LASTEXITCODE -ne 0) { exit 1 } + } + + # Change directory to source + Push-Location $ESPEAK_SRC + + # Configure with CMake + cmake -S . ` + -B $ESPEAK_BUILD ` + -G $CMAKE_GENERATOR ` + -A x64 ` + -DCMAKE_BUILD_TYPE=Release ` + -DCMAKE_CXX_STANDARD=17 ` + -DCMAKE_CXX_STANDARD_REQUIRED=ON ` + -DCMAKE_MSVC_RUNTIME_LIBRARY=MultiThreaded ` + -DCMAKE_C_FLAGS="/MT /D_CRT_NONSTDC_NO_DEPRECATE /D_CRT_SECURE_NO_WARNINGS" ` + -DCMAKE_CXX_FLAGS="/MT /D_CRT_NONSTDC_NO_DEPRECATE /D_CRT_SECURE_NO_WARNINGS" ` + -DCMAKE_C_FLAGS_RELEASE="/MT /D_CRT_NONSTDC_NO_DEPRECATE /D_CRT_SECURE_NO_WARNINGS" ` + -DCMAKE_CXX_FLAGS_RELEASE="/MT /D_CRT_NONSTDC_NO_DEPRECATE /D_CRT_SECURE_NO_WARNINGS" ` + -DCMAKE_C_FLAGS_RELWITHDEBINFO="/MT /D_CRT_NONSTDC_NO_DEPRECATE /D_CRT_SECURE_NO_WARNINGS" ` + -DCMAKE_CXX_FLAGS_RELWITHDEBINFO="/MT /D_CRT_NONSTDC_NO_DEPRECATE /D_CRT_SECURE_NO_WARNINGS" ` + -DCMAKE_C_FLAGS_DEBUG="/MTd /D_CRT_NONSTDC_NO_DEPRECATE /D_CRT_SECURE_NO_WARNINGS" ` + -DCMAKE_CXX_FLAGS_DEBUG="/MTd /D_CRT_NONSTDC_NO_DEPRECATE /D_CRT_SECURE_NO_WARNINGS" ` + -DCMAKE_INSTALL_PREFIX="$ESPEAK_INSTALL" ` + -DBUILD_SHARED_LIBS=OFF ` + -DESPEAKNG_BUILD_TESTS=OFF ` + -DESPEAKNG_BUILD_EXAMPLES=OFF ` + -DCMAKE_EXE_LINKER_FLAGS="/DEFAULTLIB:legacy_stdio_definitions.lib /DEFAULTLIB:OLDNAMES.lib" ` + -DCMAKE_STATIC_LINKER_FLAGS="/DEFAULTLIB:legacy_stdio_definitions.lib /DEFAULTLIB:OLDNAMES.lib" + if ($LASTEXITCODE -ne 0) { exit 1 } + + # Build and install + cmake --build $ESPEAK_BUILD --config Release --target INSTALL + if ($LASTEXITCODE -ne 0) { exit 1 } + + Pop-Location +} + +# ========================================================== +# BUILD OPENBLAS STATIC AND LINK +# ========================================================== +if ($WITH_OPENBLAS) { + Write-Host "=== Windows build [OpenBLAS] variant ===" + $PREBUILT_OPENBLAS_DIR = Join-Path $PROJECT_ROOT "assets\openblas-windows-portable" + $LIB_DIR = Join-Path $PREBUILT_OPENBLAS_DIR "lib" + $INCLUDE_DIR = Join-Path $PREBUILT_OPENBLAS_DIR "include\openblas" + $FINAL_LIB = Join-Path $LIB_DIR "openblas.lib" + $RENAMED_LIB = Join-Path $LIB_DIR "libopenblas.lib" + + if (Test-Path $RENAMED_LIB) { + Write-Host "OpenBLAS library already built — reusing $RENAMED_LIB" + } else { + Write-Host "OpenBLAS library not found — building from source..." + + $tmp_build = Join-Path $env:TEMP "openblas_build" + Remove-Item -Recurse -Force -ErrorAction SilentlyContinue $tmp_build + New-Item -ItemType Directory -Force -Path $tmp_build | Out-Null + + $src_dir = Join-Path $tmp_build "OpenBLAS" + git clone --depth 1 --branch v0.3.30 https://github.com/xianyi/OpenBLAS $src_dir + + Push-Location $src_dir + cmake -S . -B build -G $CMAKE_GENERATOR -A x64 ` + -DBUILD_SHARED_LIBS=OFF ` + -DCMAKE_MSVC_RUNTIME_LIBRARY=MultiThreaded ` + -DCMAKE_CXX_STANDARD=17 ` + -DCMAKE_CXX_STANDARD_REQUIRED=ON ` + -DCMAKE_C_FLAGS="/MT /D_CRT_NONSTDC_NO_DEPRECATE /D_CRT_SECURE_NO_WARNINGS" ` + -DCMAKE_CXX_FLAGS="/MT /D_CRT_NONSTDC_NO_DEPRECATE /D_CRT_SECURE_NO_WARNINGS" ` + -DCMAKE_C_FLAGS_RELEASE="/MT /D_CRT_NONSTDC_NO_DEPRECATE /D_CRT_SECURE_NO_WARNINGS" ` + -DCMAKE_CXX_FLAGS_RELEASE="/MT /D_CRT_NONSTDC_NO_DEPRECATE /D_CRT_SECURE_NO_WARNINGS" ` + -DCMAKE_C_FLAGS_RELWITHDEBINFO="/MT /D_CRT_NONSTDC_NO_DEPRECATE /D_CRT_SECURE_NO_WARNINGS" ` + -DCMAKE_CXX_FLAGS_RELWITHDEBINFO="/MT /D_CRT_NONSTDC_NO_DEPRECATE /D_CRT_SECURE_NO_WARNINGS" ` + -DCMAKE_C_FLAGS_DEBUG="/MTd /D_CRT_NONSTDC_NO_DEPRECATE /D_CRT_SECURE_NO_WARNINGS" ` + -DCMAKE_CXX_FLAGS_DEBUG="/MTd /D_CRT_NONSTDC_NO_DEPRECATE /D_CRT_SECURE_NO_WARNINGS" ` + -DCMAKE_EXE_LINKER_FLAGS="/DEFAULTLIB:legacy_stdio_definitions.lib /DEFAULTLIB:OLDNAMES.lib" ` + -DCMAKE_STATIC_LINKER_FLAGS="/DEFAULTLIB:legacy_stdio_definitions.lib /DEFAULTLIB:OLDNAMES.lib" ` + -DNO_LAPACK=ON ` + -DUSE_OPENMP=OFF ` + -DUSE_THREAD=ON ` + -DNUM_THREADS=64 ` + -DCMAKE_INSTALL_PREFIX="$PREBUILT_OPENBLAS_DIR" + + cmake --build build --config Release --target INSTALL + Pop-Location + + # Rename openblas.lib to libopenblas.lib + if (Test-Path $FINAL_LIB) { + Rename-Item -Path $FINAL_LIB -NewName "libopenblas.lib" -Force + Write-Host "Renamed openblas.lib to libopenblas.lib" + } + + Remove-Item -Recurse -Force $tmp_build + Write-Host "OpenBLAS build completed" + } + + # Ensure the variable points to the renamed library + $OPENBLAS_LIB = $RENAMED_LIB + + # Set environment variables + $env:OpenBLAS_DIR = $PREBUILT_OPENBLAS_DIR + $env:OpenBLAS_LIBRARIES = $OPENBLAS_LIB + $env:OpenBLAS_INCLUDE_DIR = $INCLUDE_DIR +} + +# $ONNX_CUDA_FLAG drives ONNX Runtime's CUDA execution provider. +# $ONNX_VULKAN_FLAG and $ONNX_USE_BLAS drive the ggml/whisper backend +# (ORT itself exposes neither a Vulkan EP nor a BLAS switch); they are +# consumed by the GGML_* env + CMAKE_ARGS block further down. +switch ($VARIANT) { + "cpu" { + $ONNX_CUDA_FLAG = "OFF" + $ONNX_VULKAN_FLAG = "OFF" + $ONNX_USE_BLAS = "ON" + } + "vulkan" { + $ONNX_CUDA_FLAG = "OFF" + $ONNX_VULKAN_FLAG = "ON" + $ONNX_USE_BLAS = "ON" + } + "cuda" { + $ONNX_CUDA_FLAG = "ON" + $ONNX_VULKAN_FLAG = "OFF" + $ONNX_USE_BLAS = "ON" + } +} + +# ========================================================== +# PREBUILT ONNX RUNTIME (CUDA VARIANT ONLY) +# +# Building ORT with the CUDA execution provider from source does not fit in +# GitHub's 6h per-job ceiling - a run with only three target architectures was +# still compiling contrib_ops when the limit hit. It also drags in the CUDA 13 +# vs ORT 1.24 incompatibilities (deprecated longlong4 escalated by nvcc's +# -Werror all-warnings, which -Xcompiler flags cannot undo). +# +# Microsoft ships a prebuilt ORT for exactly this pin and CUDA major, so link +# that instead. Unlike cpu/vulkan this variant is therefore NOT a single static +# exe: onnxruntime DLLs ship beside it, as cuDNN/cuBLAS already do. +# ========================================================== +$ORT_PREBUILT = $null +if ($WITH_CUDA) { + $ortVer = "1.24.1" + $ortName = "onnxruntime-win-x64-gpu_cuda13-$ortVer" + $ortZip = "$env:TEMP\$ortName.zip" + Write-Host "Downloading prebuilt $ortName ..." + Invoke-WebRequest -UseBasicParsing -OutFile $ortZip ` + -Uri "https://github.com/microsoft/onnxruntime/releases/download/v$ortVer/$ortName.zip" + New-Item -ItemType Directory -Force -Path $VENDOR_DIR | Out-Null + Expand-Archive -Path $ortZip -DestinationPath $VENDOR_DIR -Force + Remove-Item -Force $ortZip + + # The archive is named ..._cuda13-.zip but unpacks to + # onnxruntime-win-x64-gpu-/ without the _cuda13 part, so locate the + # import library rather than assuming the directory name. + $ortLib = Get-ChildItem -Path $VENDOR_DIR -Recurse -Filter "onnxruntime.lib" -ErrorAction SilentlyContinue | + Where-Object { $_.FullName -like "*onnxruntime-win-x64-gpu*" } | + Select-Object -First 1 + if (-not $ortLib) { + Write-Host "Contents of ${VENDOR_DIR}:" + Get-ChildItem $VENDOR_DIR -Directory | ForEach-Object { Write-Host " $($_.Name)" } + Write-Error "prebuilt ORT: onnxruntime.lib not found after extracting $ortName" + exit 1 + } + $ortDir = Split-Path -Parent (Split-Path -Parent $ortLib.FullName) + $ORT_PREBUILT = $ortDir + Write-Host "Using prebuilt ONNX Runtime at $ORT_PREBUILT" +} + +# Everything from here to EXPORT ENVIRONMENT builds ORT from source, which the +# CUDA variant skips in favour of the prebuilt package fetched above. +if (-not $ORT_PREBUILT) { + +# ========================================================== +# CLONE ONNX RUNTIME +# NOTE: must happen BEFORE absl/re2 are built, because those +# install into $ONNX_BUILD (= $ONNX_SRC\build-static) and a +# later delete of $ONNX_SRC would wipe them out. +# ========================================================== +if (Test-Path $ONNX_SRC) { + Remove-Item -Recurse -Force -ErrorAction SilentlyContinue $ONNX_SRC +} +# v1.23.2, NOT the v1.24.1 that build_linux.sh pins. The abseil build below +# (20250512.0) and the lib set this script expects are tuned for 1.23.2; +# building 1.24.1 fails at link with a missing absl_low_level_hash.lib. +# This used to be a v1.24.1 clone followed by a checkout of v1.23.2 - same +# result, but the tag now says what actually gets built. +git clone --recursive --depth 1 -b v1.23.2 https://github.com/microsoft/onnxruntime $ONNX_SRC +if ($LASTEXITCODE -ne 0) { exit 1 } + +# Update submodules +Push-Location $ONNX_SRC +# No checkout here: the clone above already pins the tag. This used to +# `git checkout tags/v1.23.2`, silently overriding that pin - which is why +# builds reported VER_STRING="1.23.2". +git submodule update --init --recursive --force +if ($LASTEXITCODE -ne 0) { exit 1 } +Pop-Location + +# ========================================================== +# BUILD ABSL AND RE2 BEFORE ONNX RUNTIME CONFIGURE +# ========================================================== +Write-Host "=== Building Abseil (absl) and RE2 static libraries ===" + +# ----------------------------------------------------------- +# Step 1: Build abseil-cpp (absl) from source +# ----------------------------------------------------------- +$AbslVersion = "20250512.0" +$AbslUrl = "https://github.com/abseil/abseil-cpp/archive/refs/tags/$AbslVersion.zip" +$AbslDownloadDir = "$env:TEMP\absl_download" +$AbslSourceDir = "$AbslDownloadDir\abseil-cpp-$AbslVersion" +$AbslBuildDir = "$AbslSourceDir\build" +$AbslInstallDir = "$ONNX_BUILD/_deps/abseil_cpp-build" + +Write-Host "Downloading Abseil (absl) $AbslVersion..." +New-Item -ItemType Directory -Force -Path $AbslDownloadDir | Out-Null +$AbslZipFile = Join-Path $AbslDownloadDir "abseil-cpp-$AbslVersion.zip" +Invoke-WebRequest -Uri $AbslUrl -OutFile $AbslZipFile -UseBasicParsing +Expand-Archive -Path $AbslZipFile -DestinationPath $AbslDownloadDir -Force + +# Use forward-slash paths for cmake to avoid escape sequence issues with backslashes +$AbslInstallDirFS = $AbslInstallDir -replace '\\', '/' + +Write-Host "Configuring Abseil (absl) with CMake..." +New-Item -ItemType Directory -Force -Path $AbslBuildDir | Out-Null +cmake -S "$AbslSourceDir" -B "$AbslBuildDir" ` + -G $CMAKE_GENERATOR ` + -A x64 ` + -DCMAKE_BUILD_TYPE=Release ` + -DCMAKE_INSTALL_PREFIX="$AbslInstallDirFS" ` + -DBUILD_SHARED_LIBS=OFF ` + -DABSL_BUILD_TESTING=OFF ` + -DABSL_PROPAGATE_CXX_STD=ON ` + -DABSL_MSVC_STATIC_RUNTIME=ON ` + -DCMAKE_MSVC_RUNTIME_LIBRARY=MultiThreaded ` + -DCMAKE_CXX_STANDARD=17 ` + -DCMAKE_CXX_STANDARD_REQUIRED=ON ` + -DCMAKE_C_FLAGS="/MT /D_CRT_NONSTDC_NO_DEPRECATE /D_CRT_SECURE_NO_WARNINGS" ` + -DCMAKE_CXX_FLAGS="/MT /D_CRT_NONSTDC_NO_DEPRECATE /D_CRT_SECURE_NO_WARNINGS" ` + -DCMAKE_C_FLAGS_RELEASE="/MT /D_CRT_NONSTDC_NO_DEPRECATE /D_CRT_SECURE_NO_WARNINGS" ` + -DCMAKE_CXX_FLAGS_RELEASE="/MT /D_CRT_NONSTDC_NO_DEPRECATE /D_CRT_SECURE_NO_WARNINGS" ` + -DCMAKE_EXE_LINKER_FLAGS="/DEFAULTLIB:legacy_stdio_definitions.lib /DEFAULTLIB:OLDNAMES.lib" ` + -DCMAKE_STATIC_LINKER_FLAGS="/DEFAULTLIB:legacy_stdio_definitions.lib /DEFAULTLIB:OLDNAMES.lib" ` + -DABSL_ENABLE_INSTALL=ON + if ($LASTEXITCODE -ne 0) { exit 1 } + + Write-Host "Building Abseil (absl)..." + cmake --build "$AbslBuildDir" --config Release --verbose + if ($LASTEXITCODE -ne 0) { exit 1 } + + Write-Host "Installing Abseil (absl) to $AbslInstallDirFS..." + cmake --build "$AbslBuildDir" --config Release --target INSTALL + if ($LASTEXITCODE -ne 0) { exit 1 } + +# ----------------------------------------------------------- +# Step 2: Create Findabsl.cmake for RE2 to locate abseil +# ----------------------------------------------------------- +$AbslCMakePath = "$AbslInstallDir/Findabsl.cmake" +# Use forward slashes to avoid CMake escape sequence interpretation of backslashes +$AbslInclude = ("$AbslInstallDir") -replace '\\', '/' + +Set-Content -Path $AbslCMakePath -Value @" +# UseAbsl.cmake - Imported Abseil targets for RE2 +# Set AbslInclude to your abseil build folder +set(AbslInclude "$AbslInclude") + +# -------------------- +# Base +# -------------------- +if(NOT TARGET absl::base) +add_library(absl::base STATIC IMPORTED) +set_target_properties(absl::base PROPERTIES + IMPORTED_LOCATION "${AbslInclude}/absl/base/Release/absl_base.lib" + INTERFACE_INCLUDE_DIRECTORIES "${AbslInclude}" +) +endif() + +if(NOT TARGET absl::absl_log) +add_library(absl::absl_log STATIC IMPORTED) +set_target_properties(absl::absl_log PROPERTIES + IMPORTED_LOCATION "${AbslInclude}/absl/base/Release/absl_log_severity.lib" + INTERFACE_INCLUDE_DIRECTORIES "${AbslInclude}" + +) +endif() + +if(NOT TARGET absl::malloc_internal) +add_library(absl::malloc_internal STATIC IMPORTED) +set_target_properties(absl::malloc_internal PROPERTIES + IMPORTED_LOCATION "${AbslInclude}/absl/base/Release/absl_malloc_internal.lib" + INTERFACE_INCLUDE_DIRECTORIES "${AbslInclude}" +) +endif() + +if(NOT TARGET absl::raw_logging_internal) +add_library(absl::raw_logging_internal STATIC IMPORTED) +set_target_properties(absl::raw_logging_internal PROPERTIES + IMPORTED_LOCATION "${AbslInclude}/absl/base/Release/absl_raw_logging_internal.lib" + INTERFACE_INCLUDE_DIRECTORIES "${AbslInclude}" +) +endif() + +if(NOT TARGET absl::spinlock_wait) +add_library(absl::spinlock_wait STATIC IMPORTED) +set_target_properties(absl::spinlock_wait PROPERTIES + IMPORTED_LOCATION "${AbslInclude}/absl/base/Release/absl_spinlock_wait.lib" + INTERFACE_INCLUDE_DIRECTORIES "${AbslInclude}" +) +endif() + +if(NOT TARGET absl::strerror) +add_library(absl::strerror STATIC IMPORTED) +set_target_properties(absl::strerror PROPERTIES + IMPORTED_LOCATION "${AbslInclude}/absl/base/Release/absl_strerror.lib" + INTERFACE_INCLUDE_DIRECTORIES "${AbslInclude}" +) +endif() + +if(NOT TARGET absl::throw_delegate) +add_library(absl::throw_delegate STATIC IMPORTED) +set_target_properties(absl::throw_delegate PROPERTIES + IMPORTED_LOCATION "${AbslInclude}/absl/base/Release/absl_throw_delegate.lib" + INTERFACE_INCLUDE_DIRECTORIES "${AbslInclude}" +) +endif() + +if(NOT TARGET absl::tracing_internal) +add_library(absl::tracing_internal STATIC IMPORTED) +set_target_properties(absl::tracing_internal PROPERTIES + IMPORTED_LOCATION "${AbslInclude}/absl/base/Release/absl_tracing_internal.lib" + INTERFACE_INCLUDE_DIRECTORIES "${AbslInclude}" +) +endif() + +if(NOT TARGET absl::hashtablez_sampler) +add_library(absl::hashtablez_sampler STATIC IMPORTED) +set_target_properties(absl::hashtablez_sampler PROPERTIES + IMPORTED_LOCATION "${AbslInclude}/absl/container/Release/absl_hashtablez_sampler.lib" + INTERFACE_INCLUDE_DIRECTORIES "${AbslInclude}" +) +endif() + +if(NOT TARGET absl::raw_hash_set) +add_library(absl::raw_hash_set STATIC IMPORTED) +set_target_properties(absl::raw_hash_set PROPERTIES + IMPORTED_LOCATION "${AbslInclude}/absl/container/Release/absl_raw_hash_set.lib" + INTERFACE_INCLUDE_DIRECTORIES "${AbslInclude}" +) +endif() + +if(NOT TARGET absl::crc_cord_state) +add_library(absl::crc_cord_state STATIC IMPORTED) +set_target_properties(absl::crc_cord_state PROPERTIES + IMPORTED_LOCATION "${AbslInclude}/absl/crc/Release/absl_crc_cord_state.lib" + INTERFACE_INCLUDE_DIRECTORIES "${AbslInclude}" +) +endif() + +if(NOT TARGET absl::crc_cpu_detect) +add_library(absl::crc_cpu_detect STATIC IMPORTED) +set_target_properties(absl::crc_cpu_detect PROPERTIES + IMPORTED_LOCATION "${AbslInclude}/absl/crc/Release/absl_crc_cpu_detect.lib" + INTERFACE_INCLUDE_DIRECTORIES "${AbslInclude}" +) +endif() + +if(NOT TARGET absl::crc_internal) +add_library(absl::crc_internal STATIC IMPORTED) +set_target_properties(absl::crc_internal PROPERTIES + IMPORTED_LOCATION "${AbslInclude}/absl/crc/Release/absl_crc_internal.lib" + INTERFACE_INCLUDE_DIRECTORIES "${AbslInclude}" +) +endif() + +if(NOT TARGET absl::crc32c) +add_library(absl::crc32c STATIC IMPORTED) +set_target_properties(absl::crc32c PROPERTIES + IMPORTED_LOCATION "${AbslInclude}/absl/crc/Release/absl_crc32c.lib" + INTERFACE_INCLUDE_DIRECTORIES "${AbslInclude}" +) +endif() + +if(NOT TARGET absl::debugging_internal) +add_library(absl::debugging_internal STATIC IMPORTED) +set_target_properties(absl::debugging_internal PROPERTIES + IMPORTED_LOCATION "${AbslInclude}/absl/debugging/Release/absl_debugging_internal.lib" + INTERFACE_INCLUDE_DIRECTORIES "${AbslInclude}" +) +endif() + +if(NOT TARGET absl::decode_rust_punycode) +add_library(absl::decode_rust_punycode STATIC IMPORTED) +set_target_properties(absl::decode_rust_punycode PROPERTIES + IMPORTED_LOCATION "${AbslInclude}/absl/debugging/Release/absl_decode_rust_punycode.lib" + INTERFACE_INCLUDE_DIRECTORIES "${AbslInclude}" +) +endif() + +if(NOT TARGET absl::demangle_internal) +add_library(absl::demangle_internal STATIC IMPORTED) +set_target_properties(absl::demangle_internal PROPERTIES + IMPORTED_LOCATION "${AbslInclude}/absl/debugging/Release/absl_demangle_internal.lib" + INTERFACE_INCLUDE_DIRECTORIES "${AbslInclude}" +) +endif() + +if(NOT TARGET absl::demangle_rust) +add_library(absl::demangle_rust STATIC IMPORTED) +set_target_properties(absl::demangle_rust PROPERTIES + IMPORTED_LOCATION "${AbslInclude}/absl/debugging/Release/absl_demangle_rust.lib" + INTERFACE_INCLUDE_DIRECTORIES "${AbslInclude}" +) +endif() + +if(NOT TARGET absl::examine_stack) +add_library(absl::examine_stack STATIC IMPORTED) +set_target_properties(absl::examine_stack PROPERTIES + IMPORTED_LOCATION "${AbslInclude}/absl/debugging/Release/absl_examine_stack.lib" + INTERFACE_INCLUDE_DIRECTORIES "${AbslInclude}" +) +endif() + +if(NOT TARGET absl::leak_check) +add_library(absl::leak_check STATIC IMPORTED) +set_target_properties(absl::leak_check PROPERTIES + IMPORTED_LOCATION "${AbslInclude}/absl/debugging/Release/absl_leak_check.lib" + INTERFACE_INCLUDE_DIRECTORIES "${AbslInclude}" +) +endif() + +if(NOT TARGET absl::stacktrace) +add_library(absl::stacktrace STATIC IMPORTED) +set_target_properties(absl::stacktrace PROPERTIES + IMPORTED_LOCATION "${AbslInclude}/absl/debugging/Release/absl_stacktrace.lib" + INTERFACE_INCLUDE_DIRECTORIES "${AbslInclude}" +) +endif() + +if(NOT TARGET absl::symbolize) +add_library(absl::symbolize STATIC IMPORTED) +set_target_properties(absl::symbolize PROPERTIES + IMPORTED_LOCATION "${AbslInclude}/absl/debugging/Release/absl_symbolize.lib" + INTERFACE_INCLUDE_DIRECTORIES "${AbslInclude}" +) +endif() + +if(NOT TARGET absl::utf8_for_code_point) +add_library(absl::utf8_for_code_point STATIC IMPORTED) +set_target_properties(absl::utf8_for_code_point PROPERTIES + IMPORTED_LOCATION "${AbslInclude}/absl/debugging/Release/absl_utf8_for_code_point.lib" + INTERFACE_INCLUDE_DIRECTORIES "${AbslInclude}" +) +endif() + +if(NOT TARGET absl::flags_commandlineflag_internal) +add_library(absl::flags_commandlineflag_internal STATIC IMPORTED) +set_target_properties(absl::flags_commandlineflag_internal PROPERTIES + IMPORTED_LOCATION "${AbslInclude}/absl/flags/Release/absl_flags_commandlineflag_internal.lib" + INTERFACE_INCLUDE_DIRECTORIES "${AbslInclude}" +) +endif() + +if(NOT TARGET absl::flags_commandlineflag) +add_library(absl::flags_commandlineflag STATIC IMPORTED) +set_target_properties(absl::flags_commandlineflag PROPERTIES + IMPORTED_LOCATION "${AbslInclude}/absl/flags/Release/absl_flags_commandlineflag.lib" + INTERFACE_INCLUDE_DIRECTORIES "${AbslInclude}" +) +endif() + +if(NOT TARGET absl::flags_config) +add_library(absl::flags_config STATIC IMPORTED) +set_target_properties(absl::flags_config PROPERTIES + IMPORTED_LOCATION "${AbslInclude}/absl/flags/Release/absl_flags_config.lib" + INTERFACE_INCLUDE_DIRECTORIES "${AbslInclude}" +) +endif() + +if(NOT TARGET absl::flags_internal) +add_library(absl::flags_internal STATIC IMPORTED) +set_target_properties(absl::flags_internal PROPERTIES + IMPORTED_LOCATION "${AbslInclude}/absl/flags/Release/absl_flags_internal.lib" + INTERFACE_INCLUDE_DIRECTORIES "${AbslInclude}" +) +endif() + +if(NOT TARGET absl::flags_marshalling) +add_library(absl::flags_marshalling STATIC IMPORTED) +set_target_properties(absl::flags_marshalling PROPERTIES + IMPORTED_LOCATION "${AbslInclude}/absl/flags/Release/absl_flags_marshalling.lib" + INTERFACE_INCLUDE_DIRECTORIES "${AbslInclude}" +) +endif() + +if(NOT TARGET absl::flags_private_handle_accessor) +add_library(absl::flags_private_handle_accessor STATIC IMPORTED) +set_target_properties(absl::flags_private_handle_accessor PROPERTIES + IMPORTED_LOCATION "${AbslInclude}/absl/flags/Release/absl_flags_private_handle_accessor.lib" + INTERFACE_INCLUDE_DIRECTORIES "${AbslInclude}" +) +endif() + +if(NOT TARGET absl::flags_program_name) +add_library(absl::flags_program_name STATIC IMPORTED) +set_target_properties(absl::flags_program_name PROPERTIES + IMPORTED_LOCATION "${AbslInclude}/absl/flags/Release/absl_flags_program_name.lib" + INTERFACE_INCLUDE_DIRECTORIES "${AbslInclude}" +) +endif() + +if(NOT TARGET absl::flags_reflection) +add_library(absl::flags_reflection STATIC IMPORTED) +set_target_properties(absl::flags_reflection PROPERTIES + IMPORTED_LOCATION "${AbslInclude}/absl/flags/Release/absl_flags_reflection.lib" + INTERFACE_INCLUDE_DIRECTORIES "${AbslInclude}" +) +endif() + +if(NOT TARGET absl::city) +add_library(absl::city STATIC IMPORTED) +set_target_properties(absl::city PROPERTIES + IMPORTED_LOCATION "${AbslInclude}/absl/hash/Release/absl_city.lib" + INTERFACE_INCLUDE_DIRECTORIES "${AbslInclude}" +) +endif() + +if(NOT TARGET absl::hash) +add_library(absl::hash STATIC IMPORTED) +set_target_properties(absl::hash PROPERTIES + IMPORTED_LOCATION "${AbslInclude}/absl/hash/Release/absl_hash.lib" + INTERFACE_INCLUDE_DIRECTORIES "${AbslInclude}" +) +endif() + +if(NOT TARGET absl::low_level_hash) +add_library(absl::low_level_hash STATIC IMPORTED) +set_target_properties(absl::low_level_hash PROPERTIES + IMPORTED_LOCATION "${AbslInclude}/absl/hash/Release/absl_low_level_hash.lib" + INTERFACE_INCLUDE_DIRECTORIES "${AbslInclude}" +) +endif() + +if(NOT TARGET absl::log_globals) +add_library(absl::log_globals STATIC IMPORTED) +set_target_properties(absl::log_globals PROPERTIES + IMPORTED_LOCATION "${AbslInclude}/absl/log/Release/absl_log_globals.lib" + INTERFACE_INCLUDE_DIRECTORIES "${AbslInclude}" +) +endif() + +if(NOT TARGET absl::log_internal_check_op) +add_library(absl::log_internal_check_op STATIC IMPORTED) +set_target_properties(absl::log_internal_check_op PROPERTIES + IMPORTED_LOCATION "${AbslInclude}/absl/log/Release/absl_log_internal_check_op.lib" + INTERFACE_INCLUDE_DIRECTORIES "${AbslInclude}" +) +endif() + +if(NOT TARGET absl::log_internal_conditions) +add_library(absl::log_internal_conditions STATIC IMPORTED) +set_target_properties(absl::log_internal_conditions PROPERTIES + IMPORTED_LOCATION "${AbslInclude}/absl/log/Release/absl_log_internal_conditions.lib" + INTERFACE_INCLUDE_DIRECTORIES "${AbslInclude}" +) +endif() + +if(NOT TARGET absl::log_internal_fnmatch) +add_library(absl::log_internal_fnmatch STATIC IMPORTED) +set_target_properties(absl::log_internal_fnmatch PROPERTIES + IMPORTED_LOCATION "${AbslInclude}/absl/log/Release/absl_log_internal_fnmatch.lib" + INTERFACE_INCLUDE_DIRECTORIES "${AbslInclude}" +) +endif() + +if(NOT TARGET absl::log_internal_format) +add_library(absl::log_internal_format STATIC IMPORTED) +set_target_properties(absl::log_internal_format PROPERTIES + IMPORTED_LOCATION "${AbslInclude}/absl/log/Release/absl_log_internal_format.lib" + INTERFACE_INCLUDE_DIRECTORIES "${AbslInclude}" +) +endif() + +if(NOT TARGET absl::log_internal_globals) +add_library(absl::log_internal_globals STATIC IMPORTED) +set_target_properties(absl::log_internal_globals PROPERTIES + IMPORTED_LOCATION "${AbslInclude}/absl/log/Release/absl_log_internal_globals.lib" + INTERFACE_INCLUDE_DIRECTORIES "${AbslInclude}" +) +endif() + +if(NOT TARGET absl::log_internal_log_sink_set) +add_library(absl::log_internal_log_sink_set STATIC IMPORTED) +set_target_properties(absl::log_internal_log_sink_set PROPERTIES + IMPORTED_LOCATION "${AbslInclude}/absl/log/Release/absl_log_internal_log_sink_set.lib" + INTERFACE_INCLUDE_DIRECTORIES "${AbslInclude}" +) +endif() + +if(NOT TARGET absl::log_internal_message) +add_library(absl::log_internal_message STATIC IMPORTED) +set_target_properties(absl::log_internal_message PROPERTIES + IMPORTED_LOCATION "${AbslInclude}/absl/log/Release/absl_log_internal_message.lib" + INTERFACE_INCLUDE_DIRECTORIES "${AbslInclude}" +) +endif() + +if(NOT TARGET absl::log_internal_nullguard) +add_library(absl::log_internal_nullguard STATIC IMPORTED) +set_target_properties(absl::log_internal_nullguard PROPERTIES + IMPORTED_LOCATION "${AbslInclude}/absl/log/Release/absl_log_internal_nullguard.lib" + INTERFACE_INCLUDE_DIRECTORIES "${AbslInclude}" +) +endif() + +if(NOT TARGET absl::log_internal_proto) +add_library(absl::log_internal_proto STATIC IMPORTED) +set_target_properties(absl::log_internal_proto PROPERTIES + IMPORTED_LOCATION "${AbslInclude}/absl/log/Release/absl_log_internal_proto.lib" + INTERFACE_INCLUDE_DIRECTORIES "${AbslInclude}" +) +endif() + +if(NOT TARGET absl::log_internal_structured_proto) +add_library(absl::log_internal_structured_proto STATIC IMPORTED) +set_target_properties(absl::log_internal_structured_proto PROPERTIES + IMPORTED_LOCATION "${AbslInclude}/absl/log/Release/absl_log_internal_structured_proto.lib" + INTERFACE_INCLUDE_DIRECTORIES "${AbslInclude}" +) +endif() + +if(NOT TARGET absl::log_sink) +add_library(absl::log_sink STATIC IMPORTED) +set_target_properties(absl::log_sink PROPERTIES + IMPORTED_LOCATION "${AbslInclude}/absl/log/Release/absl_log_sink.lib" + INTERFACE_INCLUDE_DIRECTORIES "${AbslInclude}" +) +endif() + +if(NOT TARGET absl::vlog_config_internal) +add_library(absl::vlog_config_internal STATIC IMPORTED) +set_target_properties(absl::vlog_config_internal PROPERTIES + IMPORTED_LOCATION "${AbslInclude}/absl/log/Release/absl_vlog_config_internal.lib" + INTERFACE_INCLUDE_DIRECTORIES "${AbslInclude}" +) +endif() + +if(NOT TARGET absl::int128) +add_library(absl::int128 STATIC IMPORTED) +set_target_properties(absl::int128 PROPERTIES + IMPORTED_LOCATION "${AbslInclude}/absl/numeric/Release/absl_int128.lib" + INTERFACE_INCLUDE_DIRECTORIES "${AbslInclude}" +) +endif() + +if(NOT TARGET absl::exponential_biased) +add_library(absl::exponential_biased STATIC IMPORTED) +set_target_properties(absl::exponential_biased PROPERTIES + IMPORTED_LOCATION "${AbslInclude}/absl/profiling/Release/absl_exponential_biased.lib" + INTERFACE_INCLUDE_DIRECTORIES "${AbslInclude}" +) +endif() + +if(NOT TARGET absl::cord_internal) +add_library(absl::cord_internal STATIC IMPORTED) +set_target_properties(absl::cord_internal PROPERTIES + IMPORTED_LOCATION "${AbslInclude}/absl/strings/Release/absl_cord_internal.lib" + INTERFACE_INCLUDE_DIRECTORIES "${AbslInclude}" +) +endif() + +if(NOT TARGET absl::cord) +add_library(absl::cord STATIC IMPORTED) +set_target_properties(absl::cord PROPERTIES + IMPORTED_LOCATION "${AbslInclude}/absl/strings/Release/absl_cord.lib" + INTERFACE_INCLUDE_DIRECTORIES "${AbslInclude}" +) +endif() + +if(NOT TARGET absl::cordz_functions) +add_library(absl::cordz_functions STATIC IMPORTED) +set_target_properties(absl::cordz_functions PROPERTIES + IMPORTED_LOCATION "${AbslInclude}/absl/strings/Release/absl_cordz_functions.lib" + INTERFACE_INCLUDE_DIRECTORIES "${AbslInclude}" +) +endif() + +if(NOT TARGET absl::cordz_handle) +add_library(absl::cordz_handle STATIC IMPORTED) +set_target_properties(absl::cordz_handle PROPERTIES + IMPORTED_LOCATION "${AbslInclude}/absl/strings/Release/absl_cordz_handle.lib" + INTERFACE_INCLUDE_DIRECTORIES "${AbslInclude}" +) +endif() + +if(NOT TARGET absl::cordz_info) +add_library(absl::cordz_info STATIC IMPORTED) +set_target_properties(absl::cordz_info PROPERTIES + IMPORTED_LOCATION "${AbslInclude}/absl/strings/Release/absl_cordz_info.lib" + INTERFACE_INCLUDE_DIRECTORIES "${AbslInclude}" +) +endif() + +if(NOT TARGET absl::str_format_internal) +add_library(absl::str_format_internal STATIC IMPORTED) +set_target_properties(absl::str_format_internal PROPERTIES + IMPORTED_LOCATION "${AbslInclude}/absl/strings/Release/absl_str_format_internal.lib" + INTERFACE_INCLUDE_DIRECTORIES "${AbslInclude}" +) +endif() + +if(NOT TARGET absl::string_view) +add_library(absl::string_view STATIC IMPORTED) +set_target_properties(absl::string_view PROPERTIES + IMPORTED_LOCATION "${AbslInclude}/absl/strings/Release/absl_string_view.lib" + INTERFACE_INCLUDE_DIRECTORIES "${AbslInclude}" +) +endif() + +if(NOT TARGET absl::strings_internal) +add_library(absl::strings_internal STATIC IMPORTED) +set_target_properties(absl::strings_internal PROPERTIES + IMPORTED_LOCATION "${AbslInclude}/absl/strings/Release/absl_strings_internal.lib" + INTERFACE_INCLUDE_DIRECTORIES "${AbslInclude}" +) +endif() + +if(NOT TARGET absl::strings) +add_library(absl::strings STATIC IMPORTED) +set_target_properties(absl::strings PROPERTIES + IMPORTED_LOCATION "${AbslInclude}/absl/strings/Release/absl_strings.lib" + INTERFACE_INCLUDE_DIRECTORIES "${AbslInclude}" +) +endif() + +if(NOT TARGET absl::graphcycles_internal) +add_library(absl::graphcycles_internal STATIC IMPORTED) +set_target_properties(absl::graphcycles_internal PROPERTIES + IMPORTED_LOCATION "${AbslInclude}/absl/synchronization/Release/absl_graphcycles_internal.lib" + INTERFACE_INCLUDE_DIRECTORIES "${AbslInclude}" +) +endif() + +if(NOT TARGET absl::kernel_timeout_internal) +add_library(absl::kernel_timeout_internal STATIC IMPORTED) +set_target_properties(absl::kernel_timeout_internal PROPERTIES + IMPORTED_LOCATION "${AbslInclude}/absl/synchronization/Release/absl_kernel_timeout_internal.lib" + INTERFACE_INCLUDE_DIRECTORIES "${AbslInclude}" +) +endif() + +if(NOT TARGET absl::synchronization) +add_library(absl::synchronization STATIC IMPORTED) +set_target_properties(absl::synchronization PROPERTIES + IMPORTED_LOCATION "${AbslInclude}/absl/synchronization/Release/absl_synchronization.lib" + INTERFACE_INCLUDE_DIRECTORIES "${AbslInclude}" +) +endif() + +if(NOT TARGET absl::civil_time) +add_library(absl::civil_time STATIC IMPORTED) +set_target_properties(absl::civil_time PROPERTIES + IMPORTED_LOCATION "${AbslInclude}/absl/time/Release/absl_civil_time.lib" + INTERFACE_INCLUDE_DIRECTORIES "${AbslInclude}" +) +endif() + +if(NOT TARGET absl::time_zone) +add_library(absl::time_zone STATIC IMPORTED) +set_target_properties(absl::time_zone PROPERTIES + IMPORTED_LOCATION "${AbslInclude}/absl/time/Release/absl_time_zone.lib" + INTERFACE_INCLUDE_DIRECTORIES "${AbslInclude}" +) +endif() + +if(NOT TARGET absl::time) +add_library(absl::time STATIC IMPORTED) +set_target_properties(absl::time PROPERTIES + IMPORTED_LOCATION "${AbslInclude}/absl/time/Release/absl_time.lib" + INTERFACE_INCLUDE_DIRECTORIES "${AbslInclude}" +) +endif() + +# -------------------- +# RE2 requires these additional absl targets +# -------------------- +if(NOT TARGET absl::absl_check) +add_library(absl::absl_check STATIC IMPORTED) +set_target_properties(absl::absl_check PROPERTIES + IMPORTED_LOCATION "${AbslInclude}/absl/log/Release/absl_check.lib" + INTERFACE_INCLUDE_DIRECTORIES "${AbslInclude}" +) +endif() + +if(NOT TARGET absl::absl_log) +add_library(absl::absl_log STATIC IMPORTED) +set_target_properties(absl::absl_log PROPERTIES + IMPORTED_LOCATION "${AbslInclude}/absl/log/Release/absl_log.lib" + INTERFACE_INCLUDE_DIRECTORIES "${AbslInclude}" +) +endif() + +if(NOT TARGET absl::flags) +add_library(absl::flags STATIC IMPORTED) +set_target_properties(absl::flags PROPERTIES + IMPORTED_LOCATION "${AbslInclude}/absl/flags/Release/absl_flags.lib" + INTERFACE_INCLUDE_DIRECTORIES "${AbslInclude}" +) +endif() + +if(NOT TARGET absl::str_format) +add_library(absl::str_format STATIC IMPORTED) +set_target_properties(absl::str_format PROPERTIES + IMPORTED_LOCATION "${AbslInclude}/absl/strings/Release/absl_str_format.lib" + INTERFACE_INCLUDE_DIRECTORIES "${AbslInclude}" +) +endif() + +# Header-only targets (no .lib) +if(NOT TARGET absl::core_headers) +add_library(absl::core_headers INTERFACE IMPORTED) +set_target_properties(absl::core_headers PROPERTIES + INTERFACE_INCLUDE_DIRECTORIES "${AbslInclude}" +) +endif() + +if(NOT TARGET absl::fixed_array) +add_library(absl::fixed_array INTERFACE IMPORTED) +set_target_properties(absl::fixed_array PROPERTIES + INTERFACE_INCLUDE_DIRECTORIES "${AbslInclude}" +) +endif() + +if(NOT TARGET absl::flat_hash_map) +add_library(absl::flat_hash_map INTERFACE IMPORTED) +set_target_properties(absl::flat_hash_map PROPERTIES + INTERFACE_INCLUDE_DIRECTORIES "${AbslInclude}" +) +endif() + +if(NOT TARGET absl::flat_hash_set) +add_library(absl::flat_hash_set INTERFACE IMPORTED) +set_target_properties(absl::flat_hash_set PROPERTIES + INTERFACE_INCLUDE_DIRECTORIES "${AbslInclude}" +) +endif() + +if(NOT TARGET absl::inlined_vector) +add_library(absl::inlined_vector INTERFACE IMPORTED) +set_target_properties(absl::inlined_vector PROPERTIES + INTERFACE_INCLUDE_DIRECTORIES "${AbslInclude}" +) +endif() + +if(NOT TARGET absl::optional) +add_library(absl::optional INTERFACE IMPORTED) +set_target_properties(absl::optional PROPERTIES + INTERFACE_INCLUDE_DIRECTORIES "${AbslInclude}" +) +endif() + +if(NOT TARGET absl::span) +add_library(absl::span INTERFACE IMPORTED) +set_target_properties(absl::span PROPERTIES + INTERFACE_INCLUDE_DIRECTORIES "${AbslInclude}" +) +endif() +"@ + +# ----------------------------------------------------------- +# Step 3: Build RE2 from source, linking against pre-built abseil +# ----------------------------------------------------------- +$Re2Version = "2024-07-02" +$Re2Url = "https://github.com/google/re2/archive/refs/tags/$Re2Version.zip" +$Re2DownloadDir = "$env:TEMP\re2_download" +$Re2SourceDir = "$Re2DownloadDir\re2-$Re2Version" +$Re2BuildDir = "$Re2SourceDir\build" +$Re2InstallDir = "$ONNX_BUILD/_deps/onnx-build/Release" + +Write-Host "Downloading RE2 $Re2Version..." +New-Item -ItemType Directory -Force -Path $Re2DownloadDir | Out-Null +$Re2ZipFile = Join-Path $Re2DownloadDir "re2-$Re2Version.zip" +Invoke-WebRequest -Uri $Re2Url -OutFile $Re2ZipFile -UseBasicParsing +Expand-Archive -Path $Re2ZipFile -DestinationPath $Re2DownloadDir -Force + +# Use forward-slash paths for cmake to avoid escape sequence issues with backslashes +$Re2InstallDirFS = $Re2InstallDir -replace '\\', '/' +$AbslModulePath = $AbslInstallDir -replace '\\', '/' + +Write-Host "Configuring RE2 with CMake..." +New-Item -ItemType Directory -Force -Path $Re2BuildDir | Out-Null +cmake -S "$Re2SourceDir" -B "$Re2BuildDir" ` + -G $CMAKE_GENERATOR ` + -A x64 ` + -DCMAKE_BUILD_TYPE=Release ` + -DCMAKE_INSTALL_PREFIX="$Re2InstallDirFS" ` + -DBUILD_SHARED_LIBS=OFF ` + -DRE2_BUILD_TESTING=OFF ` + -DRE2_USE_EXTERNAL_ABSL=ON ` + -DCMAKE_MODULE_PATH="$AbslModulePath" ` + -DCMAKE_MSVC_RUNTIME_LIBRARY=MultiThreaded ` + -DCMAKE_CXX_STANDARD=17 ` + -DCMAKE_CXX_STANDARD_REQUIRED=ON ` + -DCMAKE_C_FLAGS="/MT /D_CRT_NONSTDC_NO_DEPRECATE /D_CRT_SECURE_NO_WARNINGS /I$AbslInstallDirFS/include" ` + -DCMAKE_CXX_FLAGS="/MT /D_CRT_NONSTDC_NO_DEPRECATE /D_CRT_SECURE_NO_WARNINGS /I$AbslInstallDirFS/include" ` + -DCMAKE_C_FLAGS_RELEASE="/MT /D_CRT_NONSTDC_NO_DEPRECATE /D_CRT_SECURE_NO_WARNINGS /I$AbslInstallDirFS/include" ` + -DCMAKE_CXX_FLAGS_RELEASE="/MT /D_CRT_NONSTDC_NO_DEPRECATE /D_CRT_SECURE_NO_WARNINGS /I$AbslInstallDirFS/include" ` + -DCMAKE_EXE_LINKER_FLAGS="/DEFAULTLIB:legacy_stdio_definitions.lib /DEFAULTLIB:OLDNAMES.lib" ` + -DCMAKE_STATIC_LINKER_FLAGS="/DEFAULTLIB:legacy_stdio_definitions.lib /DEFAULTLIB:OLDNAMES.lib" +if ($LASTEXITCODE -ne 0) { exit 1 } + +Write-Host "Building RE2..." +cmake --build "$Re2BuildDir" --config Release --verbose +if ($LASTEXITCODE -ne 0) { exit 1 } + +Write-Host "Installing RE2 to $Re2InstallDirFS..." +cmake --build "$Re2BuildDir" --config Release --target INSTALL +if ($LASTEXITCODE -ne 0) { exit 1 } + +Write-Host "Done installing re2.lib!" +Write-Host "Static library: $Re2InstallDir\lib" +Write-Host "Headers: $Re2InstallDir\include" + +# Copy re2.lib to where ort-sys expects it +$Re2SysDir = "$ONNX_BUILD/_deps/re2-build" +New-Item -ItemType Directory -Path $Re2SysDir -Force +Copy-Item -Path "$Re2InstallDir/lib/re2.lib" -Destination "$Re2SysDir/re2.lib" -Force +New-Item -ItemType Directory -Path "$Re2SysDir/Release" -Force +Copy-Item -Path "$Re2InstallDir/lib/re2.lib" -Destination "$Re2SysDir/Release/re2.lib" -Force +Write-Host "Copied re2.lib to $Re2SysDir/re2.lib and $Re2SysDir/Release/re2.lib" + +# Clean up download artifacts +Remove-Item -Recurse -Force -ErrorAction SilentlyContinue $Re2DownloadDir +Remove-Item -Recurse -Force -ErrorAction SilentlyContinue $AbslDownloadDir + +Write-Host "=== Abseil (absl) and RE2 static libraries built successfully ===" + +# ========================================================== +# BUILD ONNX RUNTIME (Single Block, No Duplicates) +# ========================================================== +Write-Host "=== Building ONNX Runtime ===" + +# ----------------------------- +# Set ONNX flags depending on variant +# ----------------------------- +# NOTE: $ORT_EXTRA_CMAKE_ARGS must be initialised for every variant. Under +# `Set-StrictMode -Version Latest` reading an unassigned variable is a +# terminating error, so leaving it unset for vulkan/cuda crashed the script +# at the `if ($ORT_EXTRA_CMAKE_ARGS)` check below. +$ORT_EXTRA_CMAKE_ARGS = @() + +# Make sure the build directory exists +if (-not (Test-Path $ONNX_BUILD)) { + New-Item -ItemType Directory -Path $ONNX_BUILD | Out-Null +} + +# ----------------------------- +# Configure ONNX Runtime using CMake +# ----------------------------- + +$ONNX_CMAKE_ARGS = @( + "-S", "$ONNX_SRC/cmake", + "-B", "$ONNX_BUILD", + "-G", $CMAKE_GENERATOR, + "-A", "x64", + "-DCMAKE_CXX_STANDARD=17", + "-DCMAKE_CXX_STANDARD_REQUIRED=ON", + "-DCMAKE_BUILD_TYPE=Release", + "-Dabsl_DIR=$AbslInstallDirFS", + "-DBUILD_SHARED_LIBS=OFF", + "-DCMAKE_COMPILE_WARNING_AS_ERROR=OFF", + "-DCMAKE_POSITION_INDEPENDENT_CODE=OFF", + "-Donnxruntime_BUILD_SHARED_LIB=OFF", + "-Donnxruntime_ENABLE_STATIC_ANALYSIS=OFF", + "-DCMAKE_MSVC_RUNTIME_LIBRARY=MultiThreaded", + "-DCMAKE_POLICY_DEFAULT_CMP0091=NEW", + "-DCMAKE_C_FLAGS=/MT /wd4875 /D_CRT_NONSTDC_NO_DEPRECATE /D_CRT_SECURE_NO_WARNINGS", + "-DCMAKE_CXX_FLAGS=/MT /wd4875 /D_CRT_NONSTDC_NO_DEPRECATE /D_CRT_SECURE_NO_WARNINGS", + "-DCMAKE_C_FLAGS_RELEASE=/MT /wd4875 /D_CRT_NONSTDC_NO_DEPRECATE /D_CRT_SECURE_NO_WARNINGS", + "-DCMAKE_CXX_FLAGS_RELEASE=/MT /wd4875 /D_CRT_NONSTDC_NO_DEPRECATE /D_CRT_SECURE_NO_WARNINGS", + "-DCMAKE_C_FLAGS_RELWITHDEBINFO=/MT /wd4875 /D_CRT_NONSTDC_NO_DEPRECATE /D_CRT_SECURE_NO_WARNINGS", + "-DCMAKE_CXX_FLAGS_RELWITHDEBINFO=/MT /wd4875 /D_CRT_NONSTDC_NO_DEPRECATE /D_CRT_SECURE_NO_WARNINGS", + "-DCMAKE_C_FLAGS_DEBUG=/MTd /wd4875 /D_CRT_NONSTDC_NO_DEPRECATE /D_CRT_SECURE_NO_WARNINGS", + "-DCMAKE_CXX_FLAGS_DEBUG=/MTd /wd4875 /D_CRT_NONSTDC_NO_DEPRECATE /D_CRT_SECURE_NO_WARNINGS", + "-DCMAKE_EXE_LINKER_FLAGS=/DEFAULTLIB:legacy_stdio_definitions.lib /DEFAULTLIB:OLDNAMES.lib", + "-DCMAKE_STATIC_LINKER_FLAGS=/DEFAULTLIB:legacy_stdio_definitions.lib /DEFAULTLIB:OLDNAMES.lib", + "-Donnxruntime_BUILD_UNIT_TESTS=OFF", + "-Donnxruntime_USE_AVX=OFF", + "-Donnxruntime_USE_AVX2=OFF", + "-Donnxruntime_USE_AVX512=OFF", + "-Donnxruntime_RUN_ONNX_TESTS=OFF", + "-Donnxruntime_USE_XNNPACK=OFF", + "-Donnxruntime_USE_DML=OFF", + "-DBUILD_TESTING=OFF", + "-DONNX_USE_MSVC_STATIC_RUNTIME=ON", + "-DONNX_USE_PROTOBUF_SHARED_LIBS=OFF", + "-Donnxruntime_USE_FULL_PROTOBUF=OFF", + "-Donnxruntime_MSVC_STATIC_RUNTIME=ON", + "-DABSL_ENABLE_INSTALL=ON", + "-DABSL_MSVC_STATIC_RUNTIME=ON", + "-Donnxruntime_USE_CUDA=$ONNX_CUDA_FLAG" +) + +if ($ORT_EXTRA_CMAKE_ARGS) { + $ONNX_CMAKE_ARGS += $ORT_EXTRA_CMAKE_ARGS +} + +# Conditionally add CUDA-specific options only if CUDA is ON +if ($ONNX_CUDA_FLAG -eq "ON") { + $cuda_root = $env:CUDAToolkit_ROOT + # Point CMake straight at nvcc. Under the Visual Studio generator CMake + # otherwise looks for CUDA MSBuild integration inside the VS install, and + # the toolkit installs none for VS 2026 - so detection returns NOTFOUND + # even though nvcc is on PATH and runs fine. + $nvcc = Join-Path $cuda_root "bin\nvcc.exe" + if (-not (Test-Path $nvcc)) { Write-Error "nvcc.exe not found at $nvcc"; exit 1 } + # CUDA 13 dropped Maxwell/Pascal/Volta, but ORT still defaults to a list + # starting at compute_60, so nvcc aborts with + # "nvcc fatal : Unsupported gpu architecture 'compute_60'". + # Turing (75) is the oldest CUDA 13 supports. + $ONNX_CMAKE_ARGS += @( + "-DCUDAToolkit_ROOT=$cuda_root", + # Turing / Ampere-consumer / Ada. Each extra arch is a full extra + # device-code pass over every .cu with -rdc=true; five arches ran ~4h. + # Add 80 (A100) or 90 (Hopper) back if datacenter GPUs matter. + "-DCMAKE_CUDA_ARCHITECTURES=75;86;89", + # CUDA 13 bundles CCCL, which hard-errors when built with MSVC's + # traditional preprocessor. /Zc:preprocessor would change how the rest + # of ORT's MSVC code preprocesses, so use CCCL's own opt-out instead. + # /WX- because ORT passes nvcc -Werror all-warnings, and MSVC 19.51 + # emits a new warning inside nvcc's own generated cudafe stub: + # "error C2220: the following warning is treated as an error". + # /sdl (which ORT passes) promotes C4996 deprecation warnings to errors, + # and CUDA 13 deprecated longlong4/ulonglong4 in favour of the *_16a / + # *_32a forms that ORT 1.24.1 predates - 72 such errors. /WX- alone does + # not undo /sdl, so disable it and silence the two warnings involved. + "-DCMAKE_CUDA_FLAGS=-DCCCL_IGNORE_MSVC_TRADITIONAL_PREPROCESSOR_WARNING -Xcompiler=/WX- -Xcompiler=/sdl- -Xcompiler=/wd4996 -Xcompiler=/wd4211", + "-DCMAKE_CUDA_COMPILER=$($nvcc -replace '\\','/')", + "-Donnxruntime_CUDNN_HOME=$env:CUDNN_HOME", + "-DCUDNN_HOME=$env:CUDNN_HOME", + "-DCMAKE_CUDA_RUNTIME_LIBRARY=Static" + ) +} + +# Run CMake with the assembled arguments +cmake @ONNX_CMAKE_ARGS +if ($LASTEXITCODE -ne 0) { Write-Error "ONNX Runtime CMake configure failed"; exit 1 } + +# ----------------------------- +# Build ONNX Runtime +# ----------------------------- +cmake --build $ONNX_BUILD --config Release +# Without this check a partial ORT build sails on and only surfaces ~40 +# minutes later as an unrelated-looking LNK1181 on a missing .lib. +if ($LASTEXITCODE -ne 0) { Write-Error "ONNX Runtime build failed"; exit 1 } + +# Combine all onnxruntime .lib files into a single onnxruntime.lib +# so ort-sys can find it (avoids complex directory matching in its build script) +Write-Host "Combining onnxruntime .lib files into single onnxruntime.lib..." +$onnxLibFiles = Get-ChildItem -Path "$ONNX_BUILD\Release" -Filter "onnxruntime_*.lib" | Select-Object -ExpandProperty FullName +& "lib.exe" /out:"$ONNX_BUILD\Release\onnxruntime.lib" -nologo $onnxLibFiles +# Copy to base directory where ort-sys's static_link() checks for it +Copy-Item -Path "$ONNX_BUILD\Release\onnxruntime.lib" -Destination "$ONNX_BUILD\onnxruntime.lib" -Force + +} # end: build ORT from source + +# ========================================================== +# EXPORT ENVIRONMENT +# ========================================================== + + +# ========================================================== +# EXPORT ENVIRONMENT +# ========================================================== +if ($ORT_PREBUILT) { + # The prebuilt package is a shared build: onnxruntime.dll and its provider + # DLLs ship next to the exe rather than being linked in. + $env:ONNXRUNTIME_INCLUDE_DIR = Join-Path $ORT_PREBUILT "include" + $env:ORT_STRATEGY = "system" + $env:ORT_LIB_LOCATION = Join-Path $ORT_PREBUILT "lib" + $env:ORT_PREFER_DYNAMIC_LINK = "1" + $env:ONNXRUNTIME_LIB_DIR = Join-Path $ORT_PREBUILT "lib" +} else { + $env:ONNXRUNTIME_INCLUDE_DIR = Join-Path $ONNX_SRC "include" + $env:ORT_STRATEGY = "system" + $env:ORT_LIB_LOCATION = $ONNX_BUILD + $env:ORT_PREFER_DYNAMIC_LINK = "0" + $env:ONNXRUNTIME_LIB_DIR = Join-Path $ONNX_BUILD "Release" +} +# ----------------------------------------------------------- +$env:GGML_BLAS = $ONNX_USE_BLAS +$env:BLAS_STATIC = $ONNX_USE_BLAS +$env:GGML_BLAS_STATIC = $ONNX_USE_BLAS +$env:GGML_VULKAN = $ONNX_VULKAN_FLAG +$env:BLAS_VENDOR = "OpenBLAS" +$env:BLA_VENDOR = "OpenBLAS" +$env:GGML_BLAS_VENDOR = "OpenBLAS" +$env:BLAS_INCLUDE_DIRS = $INCLUDE_DIR +$env:BLAS_LIBRARIES = $OPENBLAS_LIB +$env:OPENBLAS_PATH = $PREBUILT_OPENBLAS_DIR +$env:OPENBLAS_DIR = $PREBUILT_OPENBLAS_DIR +$env:CMAKE_PREFIX_PATH = "${PREBUILT_OPENBLAS_DIR};${ONNX_BUILD}" +$env:CMAKE_ARGS = "-DGGML_BLAS=$ONNX_USE_BLAS -DGGML_BLAS_STATIC=$ONNX_USE_BLAS -DGGML_VULKAN=$ONNX_VULKAN_FLAG -DGGML_BLAS_VENDOR=OpenBLAS -DBLAS_VENDOR=OpenBLAS -DOPENBLAS_PATH=$PREBUILT_OPENBLAS_DIR -DBLAS_INCLUDE_DIRS=$INCLUDE_DIR -DBLAS_LIBRARIES=$OPENBLAS_LIB -DBLA_VENDOR=OpenBLAS -DBLAS_ROOT=$PREBUILT_OPENBLAS_DIR -DBLAS_DIR=$PREBUILT_OPENBLAS_DIR -DBLAS_LIBDIR=$LIB_DIR -DBLA_STATIC=ON" +$env:WHISPER_RS_STATIC_CRT = "1" +$env:ORT_SYS_STATIC_CRT = "1" +$env:ESPEAK_RS_STATIC_CRT = "1" +# Forces /MT inside espeak-rs-sys / whisper-rs-sys, which build their own +# C deps and otherwise default to /MD (see cmake/static-msvc.toolchain.cmake). +$env:CMAKE_TOOLCHAIN_FILE = Join-Path $PROJECT_ROOT "cmake\static-msvc.toolchain.cmake" +$env:CFLAGS = "/MT /D_CRT_SECURE_NO_WARNINGS /D_CRT_NONSTDC_NO_DEPRECATE" +$env:ESPEAK_NG_DIR = $ESPEAK_INSTALL + + +# $ONNX_BUILD only exists on the from-source path; the prebuilt CUDA variant +# never creates it. +$ortLibDir = if ($ORT_PREBUILT) { Join-Path $ORT_PREBUILT "lib" } else { $ONNX_BUILD } +Write-Host "`n=== FINAL .lib files in $ortLibDir ===" +Get-ChildItem -Path $ortLibDir -Filter *.lib -Recurse -File -ErrorAction SilentlyContinue | + ForEach-Object { Write-Host $_.FullName } + +Write-Host "`n=== VCPKG .lib files in $env:VCPKG_ROOT ===" +Get-ChildItem -Path "$env:VCPKG_ROOT" -Recurse -File -Filter *.lib | + ForEach-Object { Write-Host $_.FullName } + +# Set ORT crate feature flags +if ($WITH_CUDA) { $env:ORT_USE_CUDA = "1" } else { Remove-Item Env:ORT_USE_CUDA -ErrorAction SilentlyContinue } +if ($WITH_OPENBLAS){ $env:ORT_USE_OPENMP = "1" } else { Remove-Item Env:ORT_USE_OPENMP -ErrorAction SilentlyContinue } + +Write-Host "ORT_USE_CUDA = $env:ORT_USE_CUDA" +Write-Host "ORT_USE_OPENMP = $env:ORT_USE_OPENMP" + +# ========================================================== +# BUILD RUST BINARY WITH FEATURES +# ========================================================== +# whisper-rs-sys' build script recursively emits a cargo:rustc-link-search +# for every subdirectory under its nested whisper.cpp/ggml CMake build tree. +# The Visual Studio generator creates a deeply nested per-target/per-config +# tree there (and, with Vulkan, an extra nested vulkan-shaders-gen +# ExternalProject on top of it) - enough subdirectories that the combined +# length of all those -L flags can exceed Windows' ~32k PATH limit, which +# rustc hits when it internally builds a DLL search path to load a +# proc-macro (see https://github.com/rust-lang/rust/issues/110889 - an +# open, unfixed rustc bug, not something this build controls). Ninja +# produces a flat build directory instead, cutting that subdirectory count +# drastically. Only whisper-rs-sys/ort-sys's own internal cmake-rs builds +# read CMAKE_GENERATOR from the environment, so switching it here - after +# every cmake invocation this script drives directly (espeak-ng, abseil, +# onnxruntime) has already run with the Visual Studio generator above - +# only affects that nested build, not the rest of the script. +if (-not (Get-Command ninja -ErrorAction SilentlyContinue)) { + choco install ninja -y + if (-not (Get-Command ninja -ErrorAction SilentlyContinue)) { + Write-Error "ninja not found and could not be installed." + exit 1 + } +} +$env:CMAKE_GENERATOR = "Ninja" + +$TARGET = "x86_64-pc-windows-msvc" + +$CARGO_FEATURES = @() +if ($WITH_OPENBLAS) { $CARGO_FEATURES += "whisper-openblas" } +if ($WITH_VULKAN) { $CARGO_FEATURES += "whisper-vulkan" } +if ($WITH_CUDA) { $CARGO_FEATURES += "whisper-cuda" } +if ($WITH_CUDA) { $CARGO_FEATURES += "ort-cuda" } + + +# Move vcpkg re2.lib dep to target folder so ort-sys can find it +# NOTE: (for some reason onnx runtime doesnt build re2.lib) +# Copy-Item -Path "C:\vcpkg\installed\x64-windows-static\lib\re2.lib" -Destination "$ONNX_BUILD\_deps\onnx-build\Release\re2.lib" -Force +# Remove-Item -Path "C:\vcpkg\installed\*" -Recurse -Force +# Remove-Item -Path "C:\vcpkg\buildtrees\*" -Recurse -Force +# Remove-Item -Path "C:\vcpkg\packages\*" -Recurse -Force + +# Before cargo build +if ($ORT_PREBUILT) { + # The prebuilt ORT is one import library backed by DLLs, so none of the + # per-component static libs the from-source path enumerates below exist. + # crt-static still applies: only ORT is dynamic here, as with cuDNN/cuBLAS. + $env:RUSTFLAGS = "-C target-feature=+crt-static ` + -C codegen-units=1 ` + -C opt-level=3 ` + -L native=$ORT_PREBUILT/lib ` + -C link-arg=$ORT_PREBUILT/lib/onnxruntime.lib ` + -C link-arg=/DEFAULTLIB:legacy_stdio_definitions.lib ` + -C link-arg=/DEFAULTLIB:OLDNAMES.lib ` + -C link-arg=/NODEFAULTLIB:msvcrt.lib ` + -C link-arg=/NODEFAULTLIB:msvcrtd.lib ` + -C link-arg=/NODEFAULTLIB:ucrt.lib ` + -C link-arg=/NODEFAULTLIB:ucrtd.lib ` + -C link-arg=/NODEFAULTLIB:vcruntime.lib ` + -C link-arg=/NODEFAULTLIB:vcruntimed.lib ` + -C link-arg=/DEFAULTLIB:libcmt.lib ` + -C link-arg=/DEFAULTLIB:libucrt.lib ` + -C link-arg=/DEFAULTLIB:libvcruntime.lib " +} else { +$env:RUSTFLAGS = "-C target-feature=+crt-static ` + -C codegen-units=1 ` + -C opt-level=3 ` + -C link-arg=$ONNX_BUILD/_deps/re2-build/Release/re2.lib ` + -C link-arg=$ONNX_BUILD/_deps/abseil_cpp-build/absl/base/Release/absl_base.lib ` + -C link-arg=$ONNX_BUILD/_deps/abseil_cpp-build/absl/base/Release/absl_log_severity.lib ` + -C link-arg=$ONNX_BUILD/_deps/abseil_cpp-build/absl/base/Release/absl_malloc_internal.lib ` + -C link-arg=$ONNX_BUILD/_deps/abseil_cpp-build/absl/base/Release/absl_raw_logging_internal.lib ` + -C link-arg=$ONNX_BUILD/_deps/abseil_cpp-build/absl/base/Release/absl_spinlock_wait.lib ` + -C link-arg=$ONNX_BUILD/_deps/abseil_cpp-build/absl/base/Release/absl_strerror.lib ` + -C link-arg=$ONNX_BUILD/_deps/abseil_cpp-build/absl/base/Release/absl_throw_delegate.lib ` + -C link-arg=$ONNX_BUILD/_deps/abseil_cpp-build/absl/base/Release/absl_tracing_internal.lib ` + -C link-arg=$ONNX_BUILD/_deps/abseil_cpp-build/absl/container/Release/absl_hashtablez_sampler.lib ` + -C link-arg=$ONNX_BUILD/_deps/abseil_cpp-build/absl/container/Release/absl_raw_hash_set.lib ` + -C link-arg=$ONNX_BUILD/_deps/abseil_cpp-build/absl/crc/Release/absl_crc_cord_state.lib ` + -C link-arg=$ONNX_BUILD/_deps/abseil_cpp-build/absl/crc/Release/absl_crc_cpu_detect.lib ` + -C link-arg=$ONNX_BUILD/_deps/abseil_cpp-build/absl/crc/Release/absl_crc_internal.lib ` + -C link-arg=$ONNX_BUILD/_deps/abseil_cpp-build/absl/crc/Release/absl_crc32c.lib ` + -C link-arg=$ONNX_BUILD/_deps/abseil_cpp-build/absl/debugging/Release/absl_debugging_internal.lib ` + -C link-arg=$ONNX_BUILD/_deps/abseil_cpp-build/absl/debugging/Release/absl_decode_rust_punycode.lib ` + -C link-arg=$ONNX_BUILD/_deps/abseil_cpp-build/absl/debugging/Release/absl_demangle_internal.lib ` + -C link-arg=$ONNX_BUILD/_deps/abseil_cpp-build/absl/debugging/Release/absl_demangle_rust.lib ` + -C link-arg=$ONNX_BUILD/_deps/abseil_cpp-build/absl/debugging/Release/absl_examine_stack.lib ` + -C link-arg=$ONNX_BUILD/_deps/abseil_cpp-build/absl/debugging/Release/absl_leak_check.lib ` + -C link-arg=$ONNX_BUILD/_deps/abseil_cpp-build/absl/debugging/Release/absl_stacktrace.lib ` + -C link-arg=$ONNX_BUILD/_deps/abseil_cpp-build/absl/debugging/Release/absl_symbolize.lib ` + -C link-arg=$ONNX_BUILD/_deps/abseil_cpp-build/absl/debugging/Release/absl_utf8_for_code_point.lib ` + -C link-arg=$ONNX_BUILD/_deps/abseil_cpp-build/absl/flags/Release/absl_flags_commandlineflag_internal.lib ` + -C link-arg=$ONNX_BUILD/_deps/abseil_cpp-build/absl/flags/Release/absl_flags_commandlineflag.lib ` + -C link-arg=$ONNX_BUILD/_deps/abseil_cpp-build/absl/flags/Release/absl_flags_config.lib ` + -C link-arg=$ONNX_BUILD/_deps/abseil_cpp-build/absl/flags/Release/absl_flags_internal.lib ` + -C link-arg=$ONNX_BUILD/_deps/abseil_cpp-build/absl/flags/Release/absl_flags_marshalling.lib ` + -C link-arg=$ONNX_BUILD/_deps/abseil_cpp-build/absl/flags/Release/absl_flags_private_handle_accessor.lib ` + -C link-arg=$ONNX_BUILD/_deps/abseil_cpp-build/absl/flags/Release/absl_flags_program_name.lib ` + -C link-arg=$ONNX_BUILD/_deps/abseil_cpp-build/absl/flags/Release/absl_flags_reflection.lib ` + -C link-arg=$ONNX_BUILD/_deps/abseil_cpp-build/absl/hash/Release/absl_city.lib ` + -C link-arg=$ONNX_BUILD/_deps/abseil_cpp-build/absl/hash/Release/absl_hash.lib ` + -C link-arg=$ONNX_BUILD/_deps/abseil_cpp-build/absl/hash/Release/absl_low_level_hash.lib ` + -C link-arg=$ONNX_BUILD/_deps/abseil_cpp-build/absl/log/Release/absl_log_globals.lib ` + -C link-arg=$ONNX_BUILD/_deps/abseil_cpp-build/absl/log/Release/absl_log_internal_check_op.lib ` + -C link-arg=$ONNX_BUILD/_deps/abseil_cpp-build/absl/log/Release/absl_log_internal_conditions.lib ` + -C link-arg=$ONNX_BUILD/_deps/abseil_cpp-build/absl/log/Release/absl_log_internal_fnmatch.lib ` + -C link-arg=$ONNX_BUILD/_deps/abseil_cpp-build/absl/log/Release/absl_log_internal_format.lib ` + -C link-arg=$ONNX_BUILD/_deps/abseil_cpp-build/absl/log/Release/absl_log_internal_globals.lib ` + -C link-arg=$ONNX_BUILD/_deps/abseil_cpp-build/absl/log/Release/absl_log_internal_log_sink_set.lib ` + -C link-arg=$ONNX_BUILD/_deps/abseil_cpp-build/absl/log/Release/absl_log_internal_message.lib ` + -C link-arg=$ONNX_BUILD/_deps/abseil_cpp-build/absl/log/Release/absl_log_internal_nullguard.lib ` + -C link-arg=$ONNX_BUILD/_deps/abseil_cpp-build/absl/log/Release/absl_log_internal_proto.lib ` + -C link-arg=$ONNX_BUILD/_deps/abseil_cpp-build/absl/log/Release/absl_log_internal_structured_proto.lib ` + -C link-arg=$ONNX_BUILD/_deps/abseil_cpp-build/absl/log/Release/absl_log_sink.lib ` + -C link-arg=$ONNX_BUILD/_deps/abseil_cpp-build/absl/log/Release/absl_vlog_config_internal.lib ` + -C link-arg=$ONNX_BUILD/_deps/abseil_cpp-build/absl/numeric/Release/absl_int128.lib ` + -C link-arg=$ONNX_BUILD/_deps/abseil_cpp-build/absl/profiling/Release/absl_exponential_biased.lib ` + -C link-arg=$ONNX_BUILD/_deps/abseil_cpp-build/absl/strings/Release/absl_cord_internal.lib ` + -C link-arg=$ONNX_BUILD/_deps/abseil_cpp-build/absl/strings/Release/absl_cord.lib ` + -C link-arg=$ONNX_BUILD/_deps/abseil_cpp-build/absl/strings/Release/absl_cordz_functions.lib ` + -C link-arg=$ONNX_BUILD/_deps/abseil_cpp-build/absl/strings/Release/absl_cordz_handle.lib ` + -C link-arg=$ONNX_BUILD/_deps/abseil_cpp-build/absl/strings/Release/absl_cordz_info.lib ` + -C link-arg=$ONNX_BUILD/_deps/abseil_cpp-build/absl/strings/Release/absl_str_format_internal.lib ` + -C link-arg=$ONNX_BUILD/_deps/abseil_cpp-build/absl/strings/Release/absl_string_view.lib ` + -C link-arg=$ONNX_BUILD/_deps/abseil_cpp-build/absl/strings/Release/absl_strings_internal.lib ` + -C link-arg=$ONNX_BUILD/_deps/abseil_cpp-build/absl/strings/Release/absl_strings.lib ` + -C link-arg=$ONNX_BUILD/_deps/abseil_cpp-build/absl/synchronization/Release/absl_graphcycles_internal.lib ` + -C link-arg=$ONNX_BUILD/_deps/abseil_cpp-build/absl/synchronization/Release/absl_kernel_timeout_internal.lib ` + -C link-arg=$ONNX_BUILD/_deps/abseil_cpp-build/absl/synchronization/Release/absl_synchronization.lib ` + -C link-arg=$ONNX_BUILD/_deps/abseil_cpp-build/absl/time/Release/absl_civil_time.lib ` + -C link-arg=$ONNX_BUILD/_deps/abseil_cpp-build/absl/time/Release/absl_time_zone.lib ` + -C link-arg=$ONNX_BUILD/_deps/abseil_cpp-build/absl/time/Release/absl_time.lib ` + -C link-arg=$ONNX_BUILD/_deps/protobuf-build/Release/libprotobuf-lite.lib ` + -C link-arg=$ONNX_BUILD/_deps/protobuf-build/Release/libprotobuf.lib ` + -C link-arg=$ONNX_BUILD/_deps/protobuf-build/Release/libprotoc.lib ` + -C link-arg=$ONNX_BUILD/_deps/pytorch_cpuinfo-build/Release/cpuinfo.lib ` + -C link-arg=$ONNX_BUILD/_deps/flatbuffers-build/Release/flatbuffers.lib ` + -C link-arg=$ONNX_BUILD/Release/onnxruntime_common.lib ` + -C link-arg=$ONNX_BUILD/Release/onnxruntime_flatbuffers.lib ` + -C link-arg=$ONNX_BUILD/Release/onnxruntime_framework.lib ` + -C link-arg=$ONNX_BUILD/Release/onnxruntime_graph.lib ` + -C link-arg=$ONNX_BUILD/Release/onnxruntime_lora.lib ` + -C link-arg=$ONNX_BUILD/Release/onnxruntime_mlas.lib ` + -C link-arg=$ONNX_BUILD/Release/onnxruntime_optimizer.lib ` + -C link-arg=$ONNX_BUILD/Release/onnxruntime_providers_shared.lib ` + -C link-arg=$ONNX_BUILD/Release/onnxruntime_providers.lib ` + -C link-arg=$ONNX_BUILD/Release/onnxruntime_session.lib ` + -C link-arg=$ONNX_BUILD/Release/onnxruntime_util.lib ` + -C link-arg=$ONNX_BUILD/_deps/onnx-build/Release/onnx_proto.lib ` + -C link-arg=$ONNX_BUILD/_deps/onnx-build/Release/onnx.lib ` + -C link-arg=/DEFAULTLIB:legacy_stdio_definitions.lib ` + -C link-arg=/DEFAULTLIB:OLDNAMES.lib ` + -C link-arg=/NODEFAULTLIB:msvcrt.lib ` + -C link-arg=/NODEFAULTLIB:msvcrtd.lib ` + -C link-arg=/NODEFAULTLIB:ucrt.lib ` + -C link-arg=/NODEFAULTLIB:ucrtd.lib ` + -C link-arg=/NODEFAULTLIB:vcruntime.lib ` + -C link-arg=/NODEFAULTLIB:vcruntimed.lib ` + -C link-arg=/DEFAULTLIB:libcmt.lib ` + -C link-arg=/DEFAULTLIB:libucrt.lib ` + -C link-arg=/DEFAULTLIB:libvcruntime.lib " +} + +# ---------------------------------------------------------- +# CUDA variant: link the CUDA EP plus the CUDA/cuDNN runtimes. +# The explicit -C link-arg list above enumerates the ORT libs by name and +# predates the CUDA provider, so onnxruntime_providers_cuda.lib has to be +# added here. cudnn/cublas stay dynamic - they are driver-side and exempt +# from the fully-static rule. +# ---------------------------------------------------------- +if ($WITH_CUDA) { + $CUDA_LIB_DIR = Join-Path $env:CUDA_PATH "lib\x64" + + # RUSTFLAGS is split on spaces, and the toolkit lives under + # "C:\Program Files\NVIDIA GPU Computing Toolkit\...", so passing it + # directly makes rustc see "Files\NVIDIA" as a second input filename + # ("error: multiple input filenames provided"). Expose it under a + # space-free junction and pass that instead. + $CUDA_LINK_DIR = "C:\cuda-lib" + if (-not (Test-Path $CUDA_LINK_DIR)) { + New-Item -ItemType Junction -Path $CUDA_LINK_DIR -Target $CUDA_LIB_DIR | Out-Null + } + if (-not (Test-Path (Join-Path $CUDA_LINK_DIR "cudart_static.lib"))) { + Write-Error "CUDA lib junction $CUDA_LINK_DIR does not expose $CUDA_LIB_DIR" + exit 1 + } + if ($CUDNN_LIB_DIR -match ' ') { + Write-Error "CUDNN_LIB_DIR contains a space and cannot go in RUSTFLAGS: $CUDNN_LIB_DIR" + exit 1 + } + + $env:RUSTFLAGS += " -L native=$CUDA_LINK_DIR" + + " -L native=$CUDNN_LIB_DIR" + + if ($ORT_PREBUILT) { + # The CUDA EP lives in onnxruntime_providers_cuda.dll, loaded at run + # time by onnxruntime.dll; there is no static lib to link, and CUDA's + # own runtime is already inside those DLLs. + Write-Host "Prebuilt ORT: CUDA EP is provided by onnxruntime_providers_cuda.dll" + } else { + $env:RUSTFLAGS += " -C link-arg=$ONNX_BUILD/Release/onnxruntime_providers_cuda.lib" + + " -C link-arg=cudart_static.lib" + + " -C link-arg=cublas.lib" + + " -C link-arg=cublasLt.lib" + + " -C link-arg=cudnn.lib" + } + + Write-Host "CUDA link dirs: $CUDA_LINK_DIR (-> $CUDA_LIB_DIR) ; $CUDNN_LIB_DIR" +} + +$env:CXXFLAGS="/std:c++17 /MT /D_CRT_SECURE_NO_WARNINGS /D_CRT_NONSTDC_NO_DEPRECATE" + +if ($WITH_CUDA) { + # ggml's CUDA backend is compiled by whisper-rs-sys through its own cmake + # run, which passes -DCMAKE_CUDA_FLAGS=-Xcompiler=-fPIC and so overrides + # anything we would set there. cl.exe reads the CL environment variable on + # every invocation, including the ones nvcc spawns, so inject there. + # + # /Zc:preprocessor, not CCCL_IGNORE_MSVC_TRADITIONAL_PREPROCESSOR_WARNING: + # CUDA 13 bundles CCCL, which really does require the conforming + # preprocessor. The IGNORE define only silences the guard, after which + # CCCL's own macro concatenation fails to expand: + # cub/util_macro.cuh(24): error: expected a "{" + # namespace cub { inline namespace _V_300303_CCCL_PP_CAT(_SM_, 750) { + # This is safe now that ONNX Runtime is prebuilt - ggml is the only thing + # still compiled by cl.exe here. + $env:CL = "/Zc:preprocessor" + + # ggml compiles the CUDA host code with CMake's default MSVC Release + # flags, which include /MD, while the rest of the binary is /MT via + # +crt-static. Mixing CRTs leaves the dynamic-CRT imports unresolved: + # *.cu.obj : error LNK2001: unresolved external symbol __imp_modff + # vtmate.exe : fatal error LNK1120: 19 unresolved externals + # CL options are prepended to the command line, so a /MT there would lose + # to ggml's explicit /MD. _CL_ is appended instead, and for MSVC the last + # runtime-library flag wins. + $env:_CL_ = "/MT" + + # ggml drives cl.exe through ccache, which does not support MSVC's CL + # environment variable - the options are invisible to it and the object + # files silently never appear, so linking dies with + # LNK1181: cannot open input file '...\ggml-base.dir\ggml.c.obj' + # Turn ccache into a pass-through for this variant. It costs nothing here: + # CI runners start with an empty cache, so nothing was being reused. + $env:CCACHE_DISABLE = "1" + Write-Host "CL = $env:CL ; _CL_ = $env:_CL_ ; CCACHE_DISABLE = $env:CCACHE_DISABLE" +} + +Set-Location $PROJECT_ROOT + +Write-Host "Ensuring Rust target $TARGET is installed..." +rustup target add $TARGET + +Write-Host "Building Rust binary..." +# NOTE: -vv dumps every rustc command line and blows past GitHub's per-step log +# size limit, which truncates the log *before* the actual error. Keep it off. +cargo build --release --target $TARGET --features ($CARGO_FEATURES -join ",") +if ($LASTEXITCODE -ne 0) { Write-Error "cargo build failed"; exit 1 } + +$SRC_BIN = Join-Path $env:CARGO_TARGET_DIR "$TARGET\release\$BIN_BASE.exe" +# Fallback: try plain release folder if cross-target folder does not exist +if (-not (Test-Path $SRC_BIN)) { + $SRC_BIN = Join-Path $env:CARGO_TARGET_DIR "release\$BIN_BASE.exe" +} + +$DST_BIN = Join-Path $TARGET_DIR "$VARIANT\$BIN_BASE-$VARIANT.exe" + +if (-not (Test-Path $SRC_BIN)) { + Write-Error "ERROR: Built binary not found." + exit 1 +} + +# $TARGET_DIR is wiped by the clean step and only recreated one level deep, +# so the per-variant subdirectory has to be made here. Copy-Item does not +# create missing intermediate directories. +New-Item -ItemType Directory -Force -Path (Split-Path -Parent $DST_BIN) | Out-Null + +Copy-Item -Force $SRC_BIN $DST_BIN +Write-Host "Built $DST_BIN" + +# cuDNN and cuBLAS are not part of the NVIDIA driver, so they must ship +# alongside the exe - the driver alone will not satisfy them at runtime. +if ($WITH_CUDA) { + $binDir = Split-Path -Parent $DST_BIN + foreach ($pattern in "cudnn*64*.dll", "cublas*64*.dll") { + Get-ChildItem -Path (Join-Path $env:CUDNN_HOME "bin") -Filter $pattern -ErrorAction SilentlyContinue | + ForEach-Object { Copy-Item -Force $_.FullName $binDir } + Get-ChildItem -Path (Join-Path $env:CUDA_PATH "bin") -Filter $pattern -ErrorAction SilentlyContinue | + ForEach-Object { Copy-Item -Force $_.FullName $binDir } + } + Write-Host "Bundled CUDA/cuDNN runtime DLLs into $binDir" +} + +# The prebuilt ORT is a shared build, so its DLLs must ship with the exe. +if ($ORT_PREBUILT) { + $binDir = Split-Path -Parent $DST_BIN + Get-ChildItem -Path (Join-Path $ORT_PREBUILT "lib") -Filter "*.dll" | + ForEach-Object { Copy-Item -Force $_.FullName $binDir } + if (-not (Test-Path (Join-Path $binDir "onnxruntime.dll"))) { + Write-Error "onnxruntime.dll was not bundled into $binDir" + exit 1 + } + Write-Host "Bundled prebuilt ONNX Runtime DLLs into $binDir" +} + +# ========================================================== +# VERIFY FULLY STATIC LINK +# The goal is a binary with no non-OS DLL imports. Anything from the +# MSVC CRT, the MSVC OpenMP runtime, or one of our own vendored libs +# means something got linked dynamically. +# ========================================================== +Write-Host "`n=== Import table for $DST_BIN ===" +$dumpbin = Get-Command dumpbin.exe -ErrorAction SilentlyContinue +if (-not $dumpbin) { + Write-Warning "dumpbin.exe not found; skipping static-link verification." +} else { + $deps = & dumpbin.exe /NOLOGO /DEPENDENTS $DST_BIN | + Select-String -Pattern '^\s{4}(\S+\.dll)$' | + ForEach-Object { $_.Matches[0].Groups[1].Value } + + $deps | ForEach-Object { Write-Host " $_" } + + # Only the CUDA/Vulkan loaders and plain Win32 system DLLs may be + # dynamic. Everything below indicates a CRT or vendored library that + # failed to link statically. + # + # ucrtbase / api-ms-win-crt-* are listed deliberately: they ship with + # Windows, but their presence means the UCRT was linked dynamically, + # which is exactly what +crt-static and /MT are supposed to prevent. + # (api-ms-win-core-* and other non-crt contracts stay allowed.) + $forbidden = @( + 'vcruntime\d*\.dll', 'msvcp\d*\.dll', 'msvcr\d*\.dll', + 'ucrtbase(d)?\.dll', 'api-ms-win-crt-.*\.dll', + 'vcomp\d*\.dll', + 'libopenblas\.dll', 'openblas\.dll', + 'onnxruntime.*\.dll', 'espeak-ng\.dll', 'whisper\.dll', 'ggml.*\.dll' + ) + + # The CUDA variant deliberately links the prebuilt shared ORT (building it + # from source does not fit GitHub's 6h job limit), so its DLLs are expected + # here and are bundled next to the exe. Everything else stays forbidden. + if ($ORT_PREBUILT) { + $forbidden = $forbidden | Where-Object { $_ -ne 'onnxruntime.*\.dll' } + } + + $bad = $deps | Where-Object { + $d = $_ + $forbidden | Where-Object { $d -match "^$_$" } + } + + if ($bad) { + Write-Host "" + Write-Error ("NOT STATIC: binary imports " + ($bad -join ", ")) + exit 1 + } + Write-Host "OK: no dynamic CRT / OpenMP / vendored-library imports." +} + +if ($UPLOAD_ENABLED) { + Write-Host "Uploading artifact for $VARIANT..." + gh run upload-artifact "$BIN_BASE-$VARIANT" $DST_BIN +} + +Write-Host "`nSUCCESS: $DST_BIN" +exit 0 \ No newline at end of file diff --git a/cmake/static-msvc.toolchain.cmake b/cmake/static-msvc.toolchain.cmake new file mode 100644 index 0000000..7a4e05b --- /dev/null +++ b/cmake/static-msvc.toolchain.cmake @@ -0,0 +1,21 @@ +# Forces the static MSVC runtime (/MT) on every CMake sub-build. +# +# Consumed via the CMAKE_TOOLCHAIN_FILE env var, which the `cmake` crate +# forwards to CMake. This is how espeak-rs-sys and whisper-rs-sys — which +# build their own C/C++ deps and ignore our flags - get /MT instead of /MD. +# Without it espeak-ng links MSVCRT and drags vcruntime140.dll into the exe. + +set(CMAKE_POLICY_DEFAULT_CMP0091 NEW) +set(CMAKE_MSVC_RUNTIME_LIBRARY + "MultiThreaded$<$:Debug>" + CACHE STRING "MSVC runtime library" FORCE) + +# Belt and braces: CMAKE_MSVC_RUNTIME_LIBRARY only applies when the project +# honours CMP0091. Some vendored CMakeLists set policies to OLD, so append +# the flag directly too. +foreach(lang C CXX) + foreach(cfg "" _RELEASE _RELWITHDEBINFO _MINSIZEREL) + string(APPEND CMAKE_${lang}_FLAGS${cfg} " /MT /wd4875") + endforeach() + string(APPEND CMAKE_${lang}_FLAGS_DEBUG " /MTd") +endforeach() diff --git a/rust-toolchain.toml b/rust-toolchain.toml new file mode 100644 index 0000000..e88baf1 --- /dev/null +++ b/rust-toolchain.toml @@ -0,0 +1,2 @@ +[toolchain] +channel = "1.88.0" diff --git a/scripts/check-static.sh b/scripts/check-static.sh new file mode 100755 index 0000000..bd1b92c --- /dev/null +++ b/scripts/check-static.sh @@ -0,0 +1,33 @@ +#!/usr/bin/env bash +# Check a Windows .exe for dynamic-library imports, from Linux. +# Mirrors the dumpbin gate in build_windows.ps1 so an artifact can be +# verified without a Windows box. +# +# ./scripts/check-static.sh path/to/vtmate-cpu.exe +set -uo pipefail + +exe=${1:?usage: check-static.sh } +[[ -f $exe ]] || { echo "no such file: $exe" >&2; exit 2; } + +command -v objdump >/dev/null || { echo "objdump not found (install binutils)" >&2; exit 2; } +objdump -i 2>/dev/null | grep -q pei-x86-64 || \ + echo "warning: this objdump may lack PE support; results could be empty" >&2 + +deps=$(objdump -p "$exe" 2>/dev/null | sed -n 's/^\tDLL Name: //p' | sort -fu) +[[ -n $deps ]] || { echo "no import table found - not a PE file?" >&2; exit 2; } + +echo "=== Imports for $(basename "$exe") ===" +printf ' %s\n' $deps + +# Same denylist as the CI gate: dynamic CRT, MSVC OpenMP, vendored libs. +# CUDA/Vulkan loaders and plain Win32 DLLs are allowed. +bad=$(printf '%s\n' $deps | grep -iE \ + '^(vcruntime[0-9]*\.dll|msvcp[0-9]*\.dll|msvcr[0-9]*\.dll|ucrtbased?\.dll|api-ms-win-crt-.*\.dll|vcomp[0-9]*\.dll|libopenblas\.dll|openblas\.dll|onnxruntime.*\.dll|espeak-ng\.dll|whisper\.dll|ggml.*\.dll)$' || true) + +echo +if [[ -n $bad ]]; then + echo "NOT STATIC - forbidden imports:" + printf ' %s\n' $bad + exit 1 +fi +echo "OK: no dynamic CRT / OpenMP / vendored-library imports."