Skip to content

Windows installation overhaul - #156

Merged
mmelnich merged 20 commits into
mainfrom
windows-vcpkg-manifest
Aug 12, 2026
Merged

Windows installation overhaul#156
mmelnich merged 20 commits into
mainfrom
windows-vcpkg-manifest

Conversation

@mmelnich

@mmelnich mmelnich commented Aug 10, 2026

Copy link
Copy Markdown
Contributor

Problem

A Windows install failed on a plain Visual Studio machine. Fixing it uncovered three independent causes, all invisible to CI for the same reason: CI never runs the configuration a user actually has.

1. vcpkg. The installer ran a classic-mode vcpkg install, but the vcpkg bundled with Visual Studio is manifest-only. CI runners ship a separate, classic-capable vcpkg.

2. A 32-bit toolchain. BLAS++ reported BLAS library not found while oneMKL was correctly installed and discovered. The real cause is one line earlier: .../bin/Hostx86/x86/cl.exe — a 32-bit compiler, whose linker cannot use an x64 import library.

Our documentation caused it: it said to open "Developer PowerShell for VS 2022", which defaults to x86. "Developer Command Prompt" is a 64-bit process that also defaults to x86, so shell bitness is not a usable signal — only "x64 Native Tools Command Prompt" gives an x64 toolchain. CI sets arch: x64 explicitly, so it never ran the prescribed shell.

3. Spaces in the library path. With a correct x64 toolchain and our own link check passing, BLAS++ still failed. Its try_compile log gives the reason:

ninja: error: 'C:/Program', needed by 'cmTC_x.exe', missing and no known rule to make it

BLAS++ flattens BLAS_LIBRARIES from a CMake list into a space-separated string, then splits it back on spaces before probing. That round-trip is lossy: once joined, a space inside a path is indistinguishable from a separator. Intel installs oneMKL to C:\Program Files (x86)\... by default, so every discovered oneMKL hits this. It stayed hidden because the downloaded oneMKL lands in a space-free directory — the layout CI uses.

Library discovery and provisioning

Windows has no system prefix for third-party libraries, no loader cache, no RPATH. So build-time discovery cannot be a filesystem search (CMake's own FindBLAS finds MKL only via MKLROOT), and at run time Windows searches the executable's own directory first and PATH last. PATH is the wrong tool in both phases.

-Backend mkl (default) is the one backend with real discovery, since oneMKL has a canonical location. Probed in order, first match wins:

# Source
1 -MklRoot <path> — explicit; invalid is a hard error, never a silent fallback
2 $env:MKLROOT — set by setvars.bat; the variable CMake's FindBLAS uses
3 $env:ONEAPI_ROOT\mkl\latest
4 C:\Program Files (x86)\Intel\oneAPI\mkl\latest — the installer default

A candidate counts only if it holds mkl_intel_ilp64_dll.lib under lib\ or lib\intel64\ alongside a DLL directory (bin\, or redist\intel64\ pre-2024), so a partial install is rejected rather than half-used. If nothing is found the installer states what it searched and asks before downloading a pinned, checksum-verified copy into the project directory; declining lists the alternatives and exits non-zero. -NoDownload turns "not found" into an error outright.

-Backend openblas gets no discovery deliberately — no canonical location exists (GitHub zips, vcpkg, conda and MSYS2 all differ, and the release zips ship configs with wrong hardcoded paths). It asks whether you already have OpenBLAS and, if so, prints the exact -Backend custom invocation.

-Backend custom takes your .lib paths and DLL directory — the route for AMD AOCL, whose downloads are licence-gated.

Whatever the source, libraries are proved to work before any dependency is built, by compiling, linking and running a dgemm_/dgesv_ program with a numeric check. That check previously skipped the default mkl backend, which is why cause 2 surfaced three layers down as a misleading BLAS error. Where paths contain spaces, import libraries are staged into a space-free directory. That is a workaround: the underlying bug is fixed upstream in icl-utk-edu/blaspp#137, and removing the staging once that lands is tracked in #158.

Prompts appear only when someone can answer them: $script:Interactive is false whenever stdin is redirected, mirroring install.sh's INTERACTIVE flag. Every question has a defensible unattended default, so CI cannot hang.

What this PR does

  1. vcpkg removed. oneMKL comes from Intel's official NuGet packages, pinned by version and SHA256. No package manager prerequisite.
  2. Backend choice-Backend mkl|openblas|custom — with the discovery, provisioning and validation above, plus -NoDownload / -Yes.
  3. x64 toolchain guard in both install.ps1 preflight and setup.ps1, with different messages for x86 (wrong shell, one-command fix) and arm64/arm (unsupported — no oneMKL build exists). Shared via .github/scripts/windows/toolchain-arch.ps1; reads VSCMD_ARG_TGT_ARCH, then the bin\Host<host>\<target>\ convention, then cl.exe's banner — the banner alone would miss on a localized Visual Studio, and a missed detection fails open.
  4. No PATH edits, ever. Runtime DLLs are staged beside each executable (app-local deployment). RANDLAPACK_RUNTIME_DLL_DIRS covers the backend DLLs TARGET_RUNTIME_DLLS cannot see; the installed package exports randlapack_stage_runtime_dlls() for downstream projects.
  5. MSVC OpenMP fixed, and now reaching users. RandLAPACK's find_package(OpenMP) ran before RandBLAS's /openmp:llvm guard, so classic -openmp was cached — under which MSVC silently ignores the collapse clause rl_rpchol needs (C4849). The installer also unconditionally disabled OpenMP, so the fix shipped unusable; it is now on by default, with -NoOpenMP to opt out.
  6. Dependencies from upstream, pinned to immutable refs. BLAS++/LAPACK++ came from forks carrying two one-line MSVC fixes; both merged upstream 2026-08-06 (Remove stray debug print from symv.hh icl-utk-edu/blaspp#132, Add missing direct includes for MSVC builds (stdint.h, complex) icl-utk-edu/lapackpp#87), so both now come from icl-utk-edu. They were pinned to branch names inside a cache keyed on the setup script, so a cache hit could restore a different revision than a miss builds. Everything the installer can fetch is now the newest stable release, pinned to an exact version: oneMKL 2026.1.0.226, OpenBLAS 0.3.34, GoogleTest v1.18.0, Random123 v1.14.0. BLAS++ 3057185 and LAPACK++ 40b9d0d are the two exceptions, pinned to commits because the latest release of each (v2025.05.28) predates the MSVC fixes; they move to a tag once one carries them. The oneMKL bump renames the runtime DLLs mkl_*.2.dll -> mkl_*.3.dll; nothing hardcodes those names (staging globs *.dll), and both provisioning paths were re-verified after the bump.
  7. Reuse gated on provenance, not presence. A clone is reused only if at the pinned remote and ref; a built dependency only if built from the source we would build from now. Without this, changing a pin is a no-op for anyone who already has an install.
  8. One CI job, windows-toolchain-guards, covering the documented user path the build matrix structurally cannot. It asserts refusals and runs pure logic, so nothing builds and it finishes in seconds. A decision table covers arm64/arm — the only possible coverage, since we cannot build for them — and integration steps run the installer under a real x86 toolchain and an amd64_arm64 cross-compiling one, giving an arm64-targeting cl.exe on an x64 runner. It launches exactly as the docs prescribe, so the documented invocation stays under test.
  9. CI ran every job twice. All four workflows fired on pull_request and on push for every branch, so each commit ran the full matrix twice — same SHA, same result, 22 check runs where 11 would do. push is now restricted to main. Nothing cancelled superseded runs either, so pushing a fix left the previous run going to completion; a concurrency group now supersedes in-flight runs, for pull requests only (on main every commit should still be validated). This removes automatic CI for a branch with no pull request open, which workflow_dispatch covers on demand.
  10. Smaller fixes: Ninja unified across dependency builds; --retry-all-errors on downloads (plain --retry misses connection-level failures like curl 52); cache keys off hashFiles(), which silently resolves EMPTY under the install-script checkout path and split caches that claimed to be shared; and a dead ctest exclusion for TestABRIK.ABRIK_catch_instability — a test gone since January 2025 — removed from the Windows paths, where it had leaked into the user-facing installer even though install.sh has no such exclusion.
  11. Docs. New INSTALL_WINDOWS.md: quick start, Windows-vs-Unix contrast, backend table, install.ps1 reference, runtime-DLL explainer, troubleshooting. It states the toolchain contract the way Linux and macOS do -- you bring a compiler, CMake, Ninja and Git, in whatever terminal you like -- and then offers ways to satisfy it rather than mandating one shell, since naming a single Start-menu entry without naming the requirement is what let the wrong shell go unnoticed. It verifies with where cl (bare cl is silent in both failing cases), gives a vswhere -latest -products * one-liner that works for any edition (without -products * it finds nothing on Build Tools), and documents -ExecutionPolicy Bypass, since stock Windows refuses to run .ps1 at all.

Verification

All Windows CI green. Every backend and both interactive branches were also exercised on a machine that began with no Visual Studio, CMake, Git, Ninja or MKL, under Windows PowerShell 5.1 rather than CI's PowerShell 7, and later against a real oneAPI install at the default (spaced) location:

Path Result
oneMKL discovery (the failing real-world case) 749/749
oneMKL downloaded, from scratch 745/745 serial, 749/749 OpenMP
Build from the upstream pins 749/749, origins confirmed icl-utk-edu
-Backend openblas 745/745
-MklRoot valid / invalid reused / hard error
-Backend custom, insufficient library rejected (LNK2019: unresolved dgemm_)
openblas interactive yes / no custom recipe / downloads
-NoDownload (mkl, openblas, declined) all error, nothing fetched
Real x86 toolchain refused at preflight
Guard decision table 5/5, plus a mutation test catching a sabotaged guard
Stale provenance rebuilds; reuses on second run; fork clone re-cloned
After bumping oneMKL to 2026.1.0.226 and GoogleTest to v1.18.0 discovery 749/749; forced NuGet download verified both SHA256s and staged the renamed mkl_*.3.dll

The rows above the last one were measured before the version bump; the bump was then re-verified on both provisioning paths, which is the last row.

Rebased onto main after #157, which quarantined the macOS Accelerate gesdd canary, so core-macos is green here rather than carrying a known failure.

Notes for reviewers

  • The amd64_arm64 leg is the only thing exercising a genuine ARM64 compiler; it could not be verified locally (adding the toolset needs interactive elevation) and passed on its first run.
  • The space-in-path staging works around a BLAS++ quoting bug; fixing that upstream would remove the need for it.
  • Pre-existing and left for separate changes: install.sh clones BLAS++/LAPACK++ at floating HEAD, and the same dead ctest exclusion remains in the three Linux/macOS workflows.

rileyjmurray pushed a commit to BallisticLA/RandBLAS that referenced this pull request Aug 10, 2026
…bundled vcpkg) (#185)

## Problem

On a plain Visual Studio 2022/18 (or Build Tools) machine, the Windows
dependency setup fails immediately at the oneMKL step:

```
error: Could not locate a manifest (vcpkg.json) above the current working directory.
This vcpkg distribution does not have a classic mode instance.
```

`setup.ps1` located the vcpkg copy that Visual Studio bundles with the
C++ workload and ran a classic-mode `vcpkg install
intel-mkl:x64-windows`. The bundled copy is manifest-only: it lives
read-only under Program Files and has no classic-mode instance, so every
classic invocation fails. CI never caught this because GitHub's
windows-2022 runners ship a standalone, classic-capable vcpkg at
`C:\vcpkg` (exported as `VCPKG_INSTALLATION_ROOT`), which the discovery
logic finds first. The manual recipe in INSTALL.md Appendix A taught the
same classic-mode command. First reported against RandLAPACK's
installer, which reuses this pattern.

## Change

- `setup.ps1` now provisions oneMKL in **manifest mode**, the one mode
every vcpkg distribution supports. It generates a minimal `vcpkg.json`
under the dependency root and redirects vcpkg's
downloads/buildtrees/packages scratch trees there as well, since the
bundled copy's default scratch locations are not writable. The manifest
pins `builtin-baseline` to vcpkg release 2026.07.29 (intel-mkl 2025.2.0,
matching the current CI cache key); the bundled vcpkg requires that
field, and the pin makes the installed oneMKL version independent of how
old the user's vcpkg copy is. The installed-tree layout
(`vcpkg-installed\x64-windows`) is unchanged, so caches and downstream
paths are untouched.
- vcpkg discovery gained a fallback to the Visual Studio bundled copy
via `VSINSTALLDIR` (a developer prompt does not always put it on
`PATH`).
- **New CI lane `windows-vs-bundled-vcpkg`** (core workflow): hides the
runner's standalone vcpkg and runs dependency setup plus the serial core
build through the VS-bundled copy, reproducing the user environment that
was broken. The `vcpkg-installed` tree is deliberately not cached so the
manifest-mode fetch is exercised on every run; only the oneMKL installer
download is cached.
- INSTALL.md Appendix A now gives manifest-mode commands that work with
both the bundled and standalone distributions.

## Verification

`setup.ps1` parses clean under Windows PowerShell and the workflow is
valid YAML. The end-to-end proof is the new CI lane in this PR, which
fails on `main`'s classic-mode invocation by construction and must pass
here.

A matching RandLAPACK PR (BallisticLA/RandLAPACK#156) applies the same
fix to its installer; its RandBLAS submodule pin can pick this change up
once merged.

🤖 Generated with [Claude Code](https://claude.com/claude-code)
@mmelnich mmelnich changed the title Windows: fetch oneMKL through vcpkg manifest mode (fixes install on VS-bundled vcpkg) Windows: fetch oneMKL directly from Intel's NuGet packages (drops the vcpkg dependency) Aug 10, 2026
@mmelnich

Copy link
Copy Markdown
Contributor Author

Reworked per the complexity feedback on BallisticLA/RandBLAS#185: vcpkg is now removed entirely rather than accommodated. Its only role was downloading oneMKL, and Intel publishes the same bits as plain zips on nuget.org, so the installer now downloads two pinned, SHA256-verified packages (15 MB + 140 MB) and arranges them into the standard oneAPI layout. The vcpkg prerequisite, the distribution-discovery logic, the manifest machinery, and the extra CI lane are all gone; net diff vs main is +58/-37 and the Windows install has one fewer tool requirement than before this bug was found.

@mmelnich mmelnich changed the title Windows: fetch oneMKL directly from Intel's NuGet packages (drops the vcpkg dependency) Windows installation overhaul: no vcpkg, backend choice (oneMKL/OpenBLAS/custom), DLL staging, INSTALL_WINDOWS.md Aug 11, 2026
@mmelnich mmelnich changed the title Windows installation overhaul: no vcpkg, backend choice (oneMKL/OpenBLAS/custom), DLL staging, INSTALL_WINDOWS.md Windows installation overhaul: x64 toolchain guard, explicit BLAS discovery/provisioning, no vcpkg, DLL staging Aug 12, 2026
@mmelnich

Copy link
Copy Markdown
Contributor Author

The space-in-path staging added here is a workaround for a BLAS++ bug, now fixed upstream in icl-utk-edu/blaspp#137. Removal is tracked in #158, gated on that PR merging and this repo's pinned blaspp commit advancing to include it.

…ages

Per review feedback on the RandBLAS counterpart (BallisticLA/RandBLAS#185): the manifest-mode machinery fixed the VS-bundled-vcpkg failure but added complexity. vcpkg's only job here was downloading oneMKL, which Intel also publishes as plain zip packages on nuget.org. Downloading those directly (pinned by version and SHA256, arranged into the oneAPI layout) removes vcpkg from the picture entirely: no distribution variance, no discovery, no manifest, no extra CI lane.
…ry, app-local DLL staging

setup.ps1 gains -Backend: mkl (default) auto-discovers an installed oneAPI via MKLROOT/ONEAPI_ROOT/the default install path before falling back to the pinned NuGet download; openblas provisions the official 0.3.34 release binaries (SHA256-pinned, link-checked with a dgemm_/dgesv_ conftest, import lib regenerated from the .def if needed -- the DLL is self-contained, importing only kernel32/msvcrt); custom passes user libraries through to BLAS++/LAPACK++ after the same conftest. Defect fixes: all dependency builds move NMake->Ninja, Random123 pinned to v1.14.0, blaspp/lapackpp cache-reuse now checks for the package config file instead of bare directory existence, and action.yml cache keys drop hashFiles() (it silently resolves empty under the install-script workflow's RandLAPACK/ checkout path, splitting caches that claimed to be shared) for manual -rN revisions. rl_runtime_dlls.cmake gains RANDLAPACK_RUNTIME_DLL_DIRS so the BLAS backend's DLLs (invisible to TARGET_RUNTIME_DLLS as raw-path imports) are staged beside executables; the module + configured dirs are exported with the package and the benchmark project stages via the same function; install.ps1 replaces its keep-MKL-on-PATH instruction with staging, gains preflight checks with fix-it commands. core-windows matrix: mkl-ilp64 serial/openmp + openblas-lp64 serial, with a stripped-PATH staging proof step and a temporary OpenMP-flavor diagnostic.
….md/CI.md refresh

INSTALL_WINDOWS.md is a self-contained non-expert guide: quick start (winget + Developer PowerShell), an explicit how-Windows-differs-from-Linux/macOS section (package sourcing, per-session compiler, app-local DLL staging vs RPATH, backend defaults), backend choice table, full install.ps1 reference with worked examples incl. AOCL bring-your-own, runtime-DLL explanation with the downstream staging helper, and troubleshooting. INSTALL.md section 6 shrinks to a pointer and section 0 gains the MSVC/Ninja requirement; INSTALL_SCRIPT.md gets a Windows call-out and the correct CMake floor (3.21); docs/CI.md documents the new Windows matrix, the staging proof step, and the manual cache-revision policy with the hashFiles trap.
… the stripped-PATH proof step

The temporary CI diagnostic confirmed classic -openmp was reaching cl.exe: the root find_package(OpenMP) runs before RandBLAS's /openmp:llvm guard, so the classic flavor got cached and MSVC silently ignored the collapse clause (warning C4849) in rl_rpchol. Mirror RandBLAS's guard in rl_build_options.cmake and the benchmark project. The stripped-PATH staging proof failed on its own harness: piping the native process into Select-Object -First terminates the pipeline early, killing the process before it exits and leaving LASTEXITCODE unset. Capture the full output, then truncate for display.
…in docs/CI.md

The diagnostic confirmed classic -openmp was cached before the fix and /openmp:llvm after it; the resolution now lives in rl_build_options.cmake with the rationale documented in docs/CI.md.
…onda-as-MklRoot

Conda environments carry MKL/OpenBLAS DLLs under identical filenames and prepend Library/bin to PATH on activation; staged executables are immune by construction, but downstream programs that skip staging can be shadowed. Also documents the deliberate opt-in route: -MklRoot <env>/Library against conda's mkl-devel.
A collaborator's install failed with BLAS++ reporting "BLAS library not
found" while oneMKL was correctly installed and correctly discovered. The
libraries were never the problem: his log shows bin/Hostx86/x86/cl.exe, a
32-bit compiler, and a 32-bit linker cannot use an x64 import library.

Our own documentation caused it. INSTALL_WINDOWS.md said to open "Developer
PowerShell for VS 2022"; that shortcut targets SysWOW64 (32-bit PowerShell)
and Enter-VsDevShell defaults to an x86 toolchain. "Developer Command Prompt
for VS 2022" is a 64-bit process that also defaults to x86, so shell bitness
is not a usable signal. CI never caught this because every Windows leg
initializes MSVC with an explicit arch: x64 under pwsh, so it has never run
the shell the docs prescribe.

Architecture guard:
- install.ps1 preflight and setup.ps1 both reject a non-x64 toolchain, with
  different messages for x86 (wrong shell, one-command fix) and arm64/arm
  (unsupported: no oneMKL build exists for them).
- Detection reads VSCMD_ARG_TGT_ARCH, then the bin\Host<host>\<target>\
  layout, then cl.exe's banner. The banner alone would miss on a localized
  Visual Studio, and a missed detection fails open, defeating the check.
- Test-BlasLinkage now also runs for the mkl backend. It previously covered
  only openblas and custom, which is why the default backend had no early
  diagnostic and failed three layers down instead.

Backend provisioning is now explicit rather than implicit:
- oneMKL is discovered first (-MklRoot, MKLROOT, ONEAPI_ROOT, default oneAPI
  path). When none is found the installer explains what it looked for and
  what it would download, then asks before fetching anything.
- -NoDownload turns "not found" into an error; -Yes skips questions.
- Prompts are gated on stdin being a terminal, mirroring install.sh's
  INTERACTIVE flag, so a question can never block a CI job. CI also passes
  -Yes explicitly.
- Non-interactive default stays yes, preserving one-command installs.

CI coverage for the documented user path:
- windows-guard-logic drives the architecture decision over a table
  (x64/amd64/x86/arm64/arm). This is the only way to cover arm64 and arm,
  since we cannot build for them and no build leg could ever test them. It
  also asserts install.ps1's and setup.ps1's duplicated copies agree.
- windows-toolchain-guards runs the real installer under a real x86
  toolchain and an amd64_arm64 cross-compiling one, which yields an
  arm64-targeting cl.exe on ordinary x64 runners. Both assert a refusal, so
  neither builds a dependency and both finish in seconds.

Docs: INSTALL_WINDOWS.md names the x64 Native Tools prompt, explains the
execution-policy flag (stock Windows is Restricted), and documents the
discovery order; docs/CI.md records the rationale for the guard jobs and the
provisioning policy.

Verified on a Windows 11 machine that began with no Visual Studio, no CMake,
no Git and no MKL: full install 745/745 tests passing via both the oneMKL and
OpenBLAS backends, the x86 and declined-download paths refused correctly, and
a mutation test confirming the guard table catches a deliberately broken copy.
…sient errors

Two failures on the first run of the new jobs.

The guard steps assert that install.ps1 FAILS, and both correctly reported
"OK: refused x86 / amd64_arm64 at preflight" -- then exited 1 anyway.
`shell: powershell` exits the step with $LASTEXITCODE, which the intentional
failure had left non-zero, so a passing assertion was reported as a failure.
Clear it explicitly on the success path.

Separately, the openblas leg hit `curl: (52) Empty reply from server` from
GitHub's release CDN. `--retry` alone does not cover error 52; it retries
only timeouts and 5xx-class responses. Add --retry-all-errors with a delay,
and raise the count, on both the OpenBLAS and oneMKL downloads.

Note the amd64_arm64 leg did its job on this first run: it exercised a real
ARM64-targeting cl.exe on an x64 runner and the guard rejected it with the
unsupported-platform message, which could not be verified locally.
…share the arch check

Self-audit of this PR turned up two bugs, three stale docs, one functional
gap, and two structural cleanups.

Bugs:
- -NoDownload was silently ignored by -Backend openblas: only the prompt was
  gated on it, and the download ran regardless. A flag that says "never
  download" must never download, so this is now a clear error pointing at
  -Backend custom, which is the actual way to supply your own OpenBLAS.
- install.ps1's -Backend help still described the oneMKL fallback as an
  automatic download; it asks first, and the -NoDownload entry three lines
  below contradicted it.

Functional gap: install.ps1 passed -DCMAKE_DISABLE_FIND_PACKAGE_OpenMP=TRUE
unconditionally, so users got a serial build even though this PR fixed MSVC
OpenMP and core-windows exercises /openmp:llvm on every run. OpenMP is now on
by default with -NoOpenMP to opt out. Verified locally: 749/749 tests pass
with /openmp:llvm selected (four more tests than the serial build's 745).

Docs:
- INSTALL_WINDOWS.md verified the shell with bare `cl`, which is silent in
  both failing cases (no compiler at all, or a 32-bit one) -- neither reads as
  "wrong shell". Now uses `where cl`, which names the toolchain either way.
- INSTALL_WINDOWS.md hardcoded a VS 2022 Community path for the PowerShell
  route, which is wrong for Build Tools (different directory, and under
  Program Files (x86)), for other editions, and for other versions. Replaced
  with a vswhere one-liner that works for any install. Note -products *:
  without it vswhere reports only full editions and finds nothing on Build
  Tools, which is exactly the configuration that hit this.
- The interactive oneMKL prompt was undocumented; users now see it in §1.

Structure:
- Get-ClTargetArchitecture and Get-ToolchainArchitectureProblem were
  copy-pasted into install.ps1 and setup.ps1, with a test asserting the copies
  agreed. They now live in .github/scripts/windows/toolchain-arch.ps1,
  dot-sourced by both, which removes the duplication and the need for that
  test.
- The three guard jobs became one. Every check in them asserts a refusal or
  runs pure logic, so each takes seconds while runner start-up dominates;
  core-windows drops from 6 Windows runners per event back to 4.
…upstream, pinned

Two findings from running every backend path against a real oneMKL install.

1. The architecture fix was necessary but NOT sufficient. With a correct x64
   toolchain and our own dgemm_/dgesv_ link check passing, BLAS++ still
   reported "BLAS library not found". Its try_compile log gives the real
   reason:

       ninja: error: 'C:/Program', needed by 'cmTC_x.exe', missing

   BLAS++ feeds library paths into that probe unquoted, so a path containing
   a space splits at the space. Intel installs oneMKL to
   C:\Program Files (x86)\Intel\oneAPI\ by default, so every *discovered*
   oneMKL hits this, as does any custom backend under Program Files. It was
   invisible to CI and to earlier local runs because the downloaded oneMKL
   lands in a space-free directory under the dependency root -- the one
   layout that avoids it. Proven by copying the identical libraries to a
   space-free path, after which BLAS++ finds them immediately.

   Worked around by staging backend import libraries into a space-free
   directory when, and only when, a path contains a space. Import libraries
   only name their DLL, which is still resolved at run time from the
   backend's bin directory, so this is safe. The underlying quoting bug is
   BLAS++'s and is worth reporting upstream.

2. BLAS++ and LAPACK++ were cloned from BallisticLA forks carrying two
   one-line MSVC fixes. Both merged upstream on 2026-08-06 (blaspp #132,
   lapackpp #87), so the forks are obsolete; both now come from
   icl-utk-edu. Verified the pinned commits carry the fixes.

   They were also pinned to *branch names*, i.e. a moving tip, inside a cache
   keyed on this script -- so a cache hit could restore a different revision
   than a cache miss builds. That is the same defect already fixed here for
   Random123. Clone-Head is now Clone-Pinned and takes a tag or a commit SHA,
   so every dependency is pinned to an immutable ref: blaspp 3057185,
   lapackpp 40b9d0d, GoogleTest v1.17.0, Random123 v1.14.0. Commits rather
   than tags for the first two only because the latest release of each,
   v2025.05.28, predates the merges. Cache keys renamed off the fork branches
   and bumped, so no fork-built artifact can be restored.

Verified on Windows against a real oneAPI install at the default (spaced)
location: discovery path 749/749 tests, and a full build from the upstream
pins 749/749, with both clones confirmed to originate from icl-utk-edu.

Also corrects two unverified doc claims: the quick start recommended a VS
edition and winget invocation that had never been run (missing --wait, which
returns while the installer is still going), and INSTALL_SCRIPT.md suggested
cmake@3.27 two paragraphs after stating the floor is 3.21.
Every lane excludes ^TestABRIK\.ABRIK_catch_instability. No such test
exists: it is absent from the source, and the built tree registers six ABRIK
tests, none by that name. The exclusion arrived in c028dc7 (Jan 2025) and the
test it names was removed or renamed afterwards without dropping it.

Two reasons to remove it from the Windows paths specifically. It is dead, so
a --exclude-regex matching nothing stops protecting anything the moment a
test with that name reappears, while reading as though a known-bad test is
being skipped. And install.ps1 carried it while install.sh has no exclusion
at all, so the Windows *user* installer was silently skipping something the
Linux one runs -- a CI-only convention that should never have reached
user-facing code.

Verified: the full suite still passes, 749/749, with no exclusion.

The same dead regex remains in core-linux.yaml, core-macos.yaml and
install-script.yaml. Those predate this PR and are equally safe to clean up,
but they belong in their own change rather than here.
Changing a dependency's source had no effect on anyone who already had it
built. Two layers both keyed on presence rather than identity:

- Clone-Pinned returned early whenever the destination existed, without
  checking it was at the pinned remote and ref. A clone left by an earlier
  revision of this script -- for instance from the BallisticLA forks this
  branch just moved off -- was silently rebuilt as-is.
- The blaspp/lapackpp reuse test only asked whether a config file existed, so
  a completed install from the old source was reused forever and the clone
  step never even ran.

Together that meant the repoint to upstream would have been a no-op for every
existing checkout, which is the same failure mode as reusing a dependency tree
configured by the wrong compiler: the fix looks applied and nothing changes.

Clone-Pinned now verifies remote and HEAD and re-clones on mismatch, and each
dependency install carries a stamp naming the source it was built from, which
the reuse test must match. The URL and ref are declared once and used for both
the clone and the stamp so they cannot drift.

Verified: an install predating the stamp rebuilds, a second run reuses it, and
a clone pointed at the old fork is detected and re-cloned from upstream.
…ller fetches

The installer never supplies a compiler, CMake or Ninja -- those are the user's to provide,
same as on Linux and macOS. It does fetch the BLAS/LAPACK backend and the build-time
dependencies, and until now the versions were only discoverable by reading setup.ps1.

Adds a table naming each component, its exact pinned version, its upstream source and how it
is verified, plus two points that were implicit:

- oneMKL is downloaded ONLY when no existing oneAPI is discovered. When one is found the
  installer uses the user's version, so the documented version applies to the no-oneMKL case
  only.
- BLAS++ and LAPACK++ are pinned to commits rather than to their latest release,
  v2025.05.28, because that release predates the two one-line MSVC fixes this build needs
  (blaspp #132, lapackpp #87, merged upstream 2026-08-06). They move to a release tag once one
  includes them. Everything else is a stable, released version.

Sections renumbered to keep the new one in place; internal cross-references updated.
…l pinned

Everything the installer can fetch is now the newest stable release of that
component, pinned to an exact version:

  oneMKL      2025.2.0.627 -> 2026.1.0.226   (was four releases behind)
  GoogleTest  v1.17.0      -> v1.18.0
  OpenBLAS    0.3.34                          already newest
  Random123   v1.14.0                         already newest

BLAS++ and LAPACK++ stay pinned to commits rather than to v2025.05.28: that
release predates the two MSVC fixes this build needs (blaspp #132, lapackpp
#87). They move to a tag once one carries them; tracked in #158.

The oneMKL bump renames the runtime DLLs from mkl_*.2.dll to mkl_*.3.dll.
Nothing hardcodes those names -- staging globs *.dll -- so the change is
transparent, but it would have silently staged nothing had anything hardcoded
them. The package layout is otherwise unchanged (build/native/win-x64,
build/native/include, runtimes/win-x64/native).

Cache keys bumped for oneMKL and both GoogleTest variants, so no artifact
built from the old versions can be restored.

Verified on Windows, both provisioning paths:
- discovery against an installed 2026.1.0 oneAPI: link check OK
- forced NuGet download of 2026.1.0.226: both SHA256s verified, unpacked,
  "Found BLAS library", mkl_core.3.dll / mkl_sequential.3.dll staged
- GoogleTest v1.18.0 is a major bump touching every test: 749/749 pass
@mmelnich
mmelnich force-pushed the windows-vcpkg-manifest branch from 569b1d3 to c5aaf05 Compare August 12, 2026 20:08
…ng one shell

The guide read as a fixed recipe -- install exactly these packages, open
exactly this Start-menu entry -- which is not the contract the other platforms
have. On Linux and macOS the user brings a compiler and CMake and works
wherever they like; install.sh only suggests how to get them. Windows should
be no different, and the previous framing was what led a user into the wrong
shell in the first place: it named one entry without explaining that the
requirement is an x64 toolchain, not that particular entry.

Now the requirement comes first -- a C++ compiler, CMake, Ninja and Git, in
whatever terminal you choose -- followed by ways to satisfy it, explicitly as
options rather than mandates:
  - the x64 Native Tools prompt,
  - vswhere + vcvars64 from any Command Prompt (any edition or version),
  - whatever environment an editor or build tool already provides,
with a single check that decides whether any of them worked: `where cl` must
show Hostx64\x64.

The x86 trap is still documented, because it remains easy to hit, and now also
notes that shell bitness is not a usable signal (the "Developer Command Prompt"
is a 64-bit process that still selects x86 tools).

No behaviour change; the installer already supplied nothing but the BLAS
backend and build dependencies. This aligns the documentation with that.
…ed path

Audit of this PR's own prose. Net -65 lines, no behaviour change.

Corrections, not just trimming:

- A comment pointed at "randnla/reference/windows-software-distribution.md",
  a file in a private notes repository that does not exist here. Removed.
- docs/CI.md still described BLAS++/LAPACK++ as coming from BallisticLA fork
  branches with upstream PRs open. Both merged on 2026-08-06 and this branch
  now pulls from icl-utk-edu pinned by commit, so the entry was actively
  misleading. Rewritten around what the pins actually are and why they are
  commits rather than tags.
- docs/CI.md said "the two Windows guard jobs" after they were consolidated
  into one.

Trimming:

- The space-in-path comment was 17 lines re-deriving an analysis that now
  lives in icl-utk-edu/blaspp#137 and issue #158. Six lines and two
  references instead.
- The no-backend-found comment was 14 lines, a third of which restated
  discovery rationale already in the parameter docs. Four lines.
- install.ps1's header was 50 lines, a quarter of the file, and duplicated
  INSTALL_WINDOWS.md's guidance on shells and execution policy. 29 lines
  that point at the guide instead.
- The no-oneMKL error printed 20 lines before its options. It now leads with
  what it searched and lists the four fixes as commands, not prose.

Readability:

- INSTALL_WINDOWS.md's "Quick start" was 625 words and contained the BLAS
  acquisition philosophy, a verbatim prompt transcript and flag references,
  all of which belong in sections 4 and 5. Now 480 words: prerequisites,
  make the toolchain visible, clone, run.
- ILP64 and LP64 appeared nine times without ever being explained. One short
  gloss added where the backends are compared, in terms of what it means for
  the reader rather than what it means to a linker.
@mmelnich mmelnich changed the title Windows installation overhaul: x64 toolchain guard, explicit BLAS discovery/provisioning, no vcpkg, DLL staging Windows installation overhaul Aug 12, 2026
Trimming the quick start removed the prompt transcript with a pointer to
section 4, but section 4 never carried it, so the interactive prompt ended up
documented nowhere -- a regression introduced by that trim, not present before
it. Section 4 now shows what the installer asks and what declining does, which
is where it belongs: the quick start says a question may appear, the backend
section explains it.
…t runs

Every workflow fired on `pull_request` AND on `push` for every branch, so each
commit on a pull request ran every job twice: same SHA, same result. With 11
jobs across the four workflows that is 22 check runs per commit, half of them
redundant -- visible on this very branch, where every check count all session
read double. `push` is now restricted to `main`; a branch with an open pull
request is already covered by the `pull_request` event, and `main` still gets
a run of its own.

Nothing cancelled superseded runs either, so pushing a fix while CI was in
flight left the previous run going to completion, a full Windows matrix
included. A `concurrency` group now supersedes in-flight runs, but only for
pull requests: on `main` every commit should be validated, not just the
newest, so `cancel-in-progress` is false there.

The one behaviour this removes is automatic CI for a branch with no pull
request open. `workflow_dispatch` covers that on demand and is already
declared in all four workflows.

Rationale recorded in docs/CI.md so neither is "tidied" back.
References like "blaspp #132" and "lapackpp #87" were written bare. GitHub
resolves a bare #NNN against the repository it is rendered in, so these linked
to RandLAPACK #132 and #87 -- unrelated pull requests (funNystrom++ and a
benchmarking DNM) -- rather than to the upstream fixes they name. A reader
following them lands somewhere plausible and wrong, which is worse than a
dead link.

Now written as icl-utk-edu/blaspp#132 and icl-utk-edu/lapackpp#87, which
GitHub renders as cross-repository links. Same for the other upstream
references in these files.
@mmelnich
mmelnich merged commit e4e8565 into main Aug 12, 2026
12 checks passed
@mmelnich
mmelnich deleted the windows-vcpkg-manifest branch August 12, 2026 21:44
mmelnich added a commit that referenced this pull request Aug 13, 2026
…161)

## Problem

Two unrelated CI defects, both of which fail quietly rather than loudly.

**The Windows oneMKL cache has never hit.** `action.yml` pointed `path`
at `windows-deps\onemkl-2025.2.0.627`, while `setup.ps1` creates
`onemkl-$mklVersion` with `$mklVersion = "2026.1.0.226"`
(`setup.ps1:384,390`). The path was left behind when the version was
bumped, so it names a directory that does not exist.

Nothing reports an error. `actions/cache` looks up a key, misses, the
job re-downloads **155 MB** of oneMKL, and then the post-job step saves
a cache entry for a path that isn't there. So the cache can never hit,
on every MKL leg, indefinitely. The key already said `2026.1.0.226` —
only the path was stale.

**A `ctest` exclusion for a test that does not exist.** `--exclude-regex
"^TestABRIK\.ABRIK_catch_instability"` appears at six sites. That test
is nowhere in `test/` or `RandLAPACK/`, and `git log -S` finds no
history of it there under that name, so it has always been excluding
nothing.

The cost isn't the wasted filter. It's the misdirection: a `ctest` line
carrying an exclusion reads as though a known failure is being
suppressed, which invites exactly the wrong conclusion when someone
audits what the suite actually covers.

## What this PR does

1. **Corrects the cache path** to `onemkl-2026.1.0.226`, and adds a
comment recording that the version there must track `$mklVersion` in
`setup.ps1`. Every other path/key pair in that file was checked and is
consistent — OpenBLAS, GoogleTest, GoogleTest-ASan, Random123, BLAS++
and LAPACK++ all agree.
2. **Removes the dead exclusion** from all six sites: `core-linux.yaml`
(×2), `install-script.yaml` (×2), `core-macos.yaml` (×2).

## Notes for reviewers

- **The macOS change is deliberately partial.** There the regex was an
alternation with `TestQB.Polynomial_Decay_general1`, which is the *real*
Apple Accelerate `gesdd` quarantine from #157. Only the dead ABRIK half
is removed; the quarantine, its "suppression is ACTIVE" warning, and the
check that shouts if the test starts passing are all untouched.
- Expect the first Windows MKL leg after this merges to still be slow —
it has to populate the cache once. Subsequent runs are where the 155 MB
comes back.
- This is the first of a short series bringing RandLAPACK's `install.sh`
up to the standard `install.ps1` reached in #156, mirroring the RandBLAS
installer work. It is independent of the rest and useful on its own.
mmelnich added a commit that referenced this pull request Aug 13, 2026
…kend selection

Brings the Unix installer up to the standard install.ps1 reached in #156, and
in line with the RandBLAS installer. The Windows script already pinned every
dependency, recorded provenance and validated the BLAS before building; this one
did none of that. Eleven defects, all verified against main before changing
anything:

* Three `git clone` calls with no ref (:336,339,342) -- blaspp, lapackpp and
  random123 all tracked upstream default branches, so two runs of the same
  script could build different source. Now pinned to the refs this repository's
  own Windows provisioner already validated, fetched one commit deep, with a
  provenance stamp on each install and each source tree.

* No BLAS validation at all. The installer now compiles, links and *runs* a
  program against the finished install, through BLAS++ and LAPACK++ rather than
  raw dgemm_ (that is how RandLAPACK reaches them), checking a gemm result and
  a gesdd factorization numerically.

  The gesdd call is deliberate. It is the routine Apple's legacy Accelerate
  computes incorrectly, so a broken SVD surfaces at install time rather than in
  someone's RSVD output. And the run-it-not-just-link-it part matters for a
  reason peculiar to integer width: BLAS++/LAPACK++ guard the int64_t downcast
  and throw rather than truncate, but that guard keys off sizeof(blas_int) as
  declared by the *header*. If the headers say 64-bit while the loaded library
  is LP64, the guard compiles out and 64-bit values reach routines reading 32
  bits -- which shows up not as wrong numbers but as an absurd workspace size
  and a run that dies in allocation or never finishes. Nothing catches that by
  inspection.

* /opt/homebrew hardcoded in eight places, which hard-fails on Intel macOS and
  any custom HOMEBREW_PREFIX. Now `brew --prefix`.

* The script moved the user's own clone into <project>/lib/RandLAPACK, breaking
  git worktrees. It now works in place and puts a symlink there instead, so the
  layout still reads as complete and the path CI invokes still resolves.

* The log was truncated on every run (:211), destroying the previous run's
  output at exactly the moment you want it. Now appended with a run header.

* The libdir search omitted lib/aarch64-linux-gnu.

* No --prefix; extras and benchmarks had no way to be skipped. Both added,
  with extras and benchmarks still built by DEFAULT (--no-extras /
  --no-benchmarks to opt out): unlike RandBLAS's examples they need nothing this
  script has not already built, so default-on costs only time.

Backend selection is new: --blas=auto|openblas|mkl|accelerate|custom,
--blas-int, --blas-libraries. Integer width is requested per backend and then
READ BACK from BLAS++'s generated blas/defines.h rather than assumed, because
BLAS++ probes int32 before int64 while blas_int only filters library *names* --
so for MKL the request genuinely selects mkl_intel_ilp64, but for OpenBLAS
there is only -lopenblas and an LP64 build passes the int32 probe and is
accepted. Trusting the request would stamp LP64 installs as ILP64.

macOS deliberately keeps Homebrew OpenBLAS as the default rather than moving to
Accelerate. Apple's legacy Accelerate has a broken divide-and-conquer gesdd and
RandLAPACK calls gesdd in rl_rsvd, rl_abrik, rl_revd2, rl_preconditioners and
rl_util, so Accelerate would quietly return wrong singular values across most
of the SVD-based drivers -- which is what #157's quarantine of
TestQB.Polynomial_Decay_general1 is about. --blas=accelerate is allowed and
warns, citing #159.

The provenance stamp includes the GPU setting, and the dependency install
directories now carry the backend and GPU configuration in their names
(blaspp-mkl-cpu-install). Both matter, and the second one for a reason outside
this project: RandBLAS's installer uses the same RandNLA-project layout and
named its BLAS++ install identically. Sharing that name meant this script would
rebuild over RandBLAS's BLAS++ (their stamp is a different file, so it never
matched ours), and RandBLAS's next run would reuse an artifact we had replaced
underneath it while its own stamp still described the original. Distinct names
make that impossible; cross-project sharing is instead explicit, via
BLASPP_INSTALL_DIR, with the conftest confirming the result works.

Also adds the three progress tiers from the RandBLAS installer -- determinate,
parsed from Ninja's "[12/34]" and Make's "[ 42%]" -- with tier 0 (redirected
output) byte-identical to the plain step list, and a CI assertion that
redirected output contains no escape sequence or carriage return.

CI: the install-script lanes globbed rather than hardcoded for the new
dependency directory names, so they are not coupled to that naming, and the
discovery assertion updated to the text the new script prints.

Verified on Linux (gcc 15.2, oneAPI MKL): fresh MKL build; idempotent re-run;
full default run with extras and benchmarks (13/13 steps); switching --no-gpu to
--gpu correctly invalidating the BLAS++ provenance and rebuilding; dependency
discovery reducing to 3 steps; running from a git worktree with the clone left
in place; redirected output escape-free; and RandBLAS-then-RandLAPACK into one
shared RANDNLA_PROJECT_DIR leaving both projects' dependencies intact, plus
RandLAPACK reusing RandBLAS's BLAS++ when pointed at it explicitly.
mmelnich added a commit that referenced this pull request Aug 13, 2026
…nvironment

Brings the Windows installer's location handling in line with install.sh, which
is what makes the shared RandNLA-project convention actually work on both
platforms.

RANDNLA_PROJECT_DIR was read nowhere in this script. Its precedence is now
identical to install.sh's -- the flag, then the environment variable, then a
sibling of the clone -- so a machine that has already installed one RandNLA
project does not scatter a second tree somewhere else.

-ModifyEnvironment persists that variable for the user with
[Environment]::SetEnvironmentVariable(..., "User"), the Windows equivalent of
install.sh's --modify-rc and the only mechanism that survives opening a new
shell; setting $env: alone would last for the current process. Opt-in, matching
install.sh: the default touches nothing and prints the setx command instead.

-Prefix installs RandLAPACK itself somewhere other than
<ProjectDir>\install\RandLAPACK-install, for a module tree or any prefix a site
wants to own. Dependencies stay in the project directory.

Also moved the project-directory length warning to after path resolution. It
was guarded on `$ProjectDir -ne ""`, so it only ever fired for an explicitly
passed -ProjectDir -- never for the default, which is the common case and is
derived from wherever the clone happens to sit, so the more likely one to be
long.

Verified on Windows 11 under Windows PowerShell 5.1 with VS 2022 Build Tools:
all three precedence cases resolve as intended (sibling default, environment
variable honoured, flag beating the variable), and the User-scope environment
write round-trips. The oneMKL discovery, BLAS link check and space-free import
library staging from #156 were all observed still working along the way.
mmelnich added a commit that referenced this pull request Aug 13, 2026
… backend selection, and docs (#162)

## Problem

RandLAPACK's two installers had drifted apart. `install/install.ps1` was
rebuilt in #156 and pins every dependency, checksums downloads, records
provenance and link-and-run validates the BLAS before building anything.
`install/install.sh` did none of that, and the documentation described a
script that no longer existed in several respects.

Eleven defects, all verified against `main` before touching anything:

| Where | Defect |
|---|---|
| `install.sh:336,339,342` | three `git clone` with **no ref** — blaspp,
lapackpp, random123 all tracked upstream default branches |
| — | **no BLAS validation at all** |
| — | `/opt/homebrew` hardcoded, **8 places** — hard-fails on Intel
macOS and custom `HOMEBREW_PREFIX` |
| `install.sh:331` | `mv "$REPO_DIR"` relocated the user's own clone,
breaking git worktrees |
| `install.sh:211` | `: > "$LOG"` truncated the log, destroying the
previous run's output |
| `install.sh:247` | libdir search omitted `lib/aarch64-linux-gnu` |
| — | no `--prefix`; extras and benchmarks unconditional with no way to
skip |
| `install.ps1` | **zero** references to `RANDNLA_PROJECT_DIR`, no
`--modify-rc` equivalent |
| `INSTALL_SCRIPT.md` | documented the pre-backend flag list, and still
told people the script would move their clone |

## The two things most worth reviewing

### Integer width is read back, not assumed

BLAS++ probes `int32` before `int64`, and `blas_int` only filters which
*library names* to try. For MKL that is a real choice — `mkl_intel_lp64`
and `mkl_intel_ilp64` are different libraries. For OpenBLAS there is
only `-lopenblas`, so **a successful `blas_int=int64` configure proves
nothing**: an LP64 build passes the `int32` probe and is accepted. The
resolved width therefore comes from BLAS++'s generated `blas/defines.h`
after the build. Trusting the request would stamp LP64 installs as ILP64
and have every later run reuse them believing otherwise.

### Verification runs the program, and that is the point

The conftest compiles, links and *runs* against the finished install —
through BLAS++ and LAPACK++ rather than raw `dgemm_`, since that is how
RandLAPACK reaches them — checking a `gemm` result and a `gesdd`
factorization numerically. `gesdd` is chosen because it is the routine
Apple's legacy Accelerate computes incorrectly.

Running it rather than only linking matters for a reason specific to
integer width. BLAS++ and LAPACK++ *do* guard the `int64_t` downcast —
`to_blas_int` (`blaspp/src/blas_internal.hh:16-22`) throws rather than
truncates — but that guard keys off `sizeof(blas_int)` **as declared by
the header**. If the headers say 64-bit while the library actually
loaded is LP64, the guard compiles out and 64-bit values reach routines
reading 32 bits. That surfaces not as wrong numbers but as nonsense
control values: a misread workspace query becomes an absurd `lwork`, and
the run dies in allocation or never finishes. Nothing catches it by
inspection.

## A cross-project collision, found while testing

Dependency install directories now carry backend and GPU in their names
(`blaspp-mkl-cpu-install`). That is not cosmetic.

RandBLAS's installer uses the same `RandNLA-project` layout and named
its BLAS++ install **identically** (`blaspp-<backend>-install`), with a
different stamp filename. With a shared `RANDNLA_PROJECT_DIR`, this
script would rebuild over RandBLAS's BLAS++ — their stamp never matches
ours — and **RandBLAS's next run would reuse an artifact we had replaced
underneath it, while its own stamp still described the original.**
Distinct names make it impossible; sharing is instead explicit via
`BLASPP_INSTALL_DIR`, verified by the conftest.

## Everything else

- **macOS keeps Homebrew OpenBLAS as the default.** Apple's legacy
Accelerate has a broken divide-and-conquer `gesdd`, and RandLAPACK calls
`gesdd` in `rl_rsvd.hh:146`, `rl_abrik.hh:692`, `rl_revd2.hh:208`,
`rl_preconditioners.hh:355` and `rl_util.hh:413`. That is what #157's
quarantine is about. `--blas=accelerate` warns, citing #159. The
hardcoded `/opt/homebrew` is fixed regardless.
- **Extras and benchmarks stay built by default** (`--no-extras` /
`--no-benchmarks`), unlike RandBLAS's examples — they need nothing this
script has not already built.
- **The clone is never moved**; a symlink gives the same layout and the
path CI invokes still resolves.
- **RandBLAS is untouched** — still a pinned submodule, still
authoritative.
- **`install.ps1`** now honours `RANDNLA_PROJECT_DIR` with the same
precedence as `install.sh`, gains `-ModifyEnvironment` (User-scope, the
only thing that survives a new shell) and `-Prefix`, and its path-length
warning now checks the resolved path rather than only an explicitly
passed one.
- **Progress rendering** in three tiers, determinate from the build
tool's own output, with tier 0 byte-identical to the plain step list —
plus a CI assertion that redirected output carries no escape sequence.
- **`INSTALL_SCRIPT.md`** rewritten: current flags, corrected layout
diagram, and a new section 6 with a tested-configuration table drawn
from the CI lanes, the per-backend integer width, whether LP64 actually
limits RandLAPACK, and why macOS is on OpenBLAS.

## Verification

Local Linux (gcc 15.2, oneAPI MKL, NVIDIA GPU present):

| Scenario | Result |
|---|---|
| Fresh MKL build, `--no-gpu` | 9/9 steps, ILP64 confirmed from
`blas/defines.h` |
| Re-run in place | all six dependency steps reused, 1s rebuild |
| Full default run (extras + benchmarks) | 13/13 steps |
| `--no-gpu` → `--gpu` | BLAS++ provenance invalidated, rebuilt rather
than reused |
| Dependency discovery | all three reused, 3/3 steps |
| Run from a git worktree | clone left in place, worktree still
functional |
| Output redirected | no ANSI escapes, no carriage returns |
| **Full test suite** | **313/313 pass** |
| RandBLAS then RandLAPACK, shared `RANDNLA_PROJECT_DIR` | both
dependency sets intact |
| RandLAPACK reusing RandBLAS's BLAS++ | reused, only LAPACK++ built,
5/5 steps |

Windows 11, Windows PowerShell 5.1, VS 2022 Build Tools: all three
`-ProjectDir` precedence cases resolve as intended, and the User-scope
environment write round-trips.

## RandBLAS submodule bump

Also bumps the vendored RandBLAS from `04f2018` to `952251c` (RandBLAS
main), and `RandLAPACK_RandBLAS_PIN` with it.

The three commits in that range are **#185** (vcpkg manifest fix),
**#186** (CMake hygiene) and **#187** (installer scripts). None of them
touch `RandBLAS/` headers — the library code is byte-identical, and
everything in the range is build machinery. Two pieces of it matter
here:

- RandBLAS's CMake floor moved 3.12 → 3.21. RandLAPACK already declares
3.21, so nothing to change; the submodule now simply enforces what
RandLAPACK already required.
- RandBLAS exports `randblas_stage_runtime_dlls()` from its installed
package and prints a configuration summary. As a subproject under
RandLAPACK, which configures it with `BUILD_TESTS=OFF`, that summary
reports tests as deliberately skipped rather than warning.

`CMakeLists.txt` enforces that the pin variable and the submodule move
together, and it means it: the cross-check compares the variable against
`git ls-tree HEAD RandBLAS`, so a staged-but-uncommitted bump fails
configure until both land in one commit. Worth knowing if you ever bump
this iteratively — it is not satisfiable before committing.

Verified: clean build against the bumped submodule, and the full suite
re-run.

## Notes for reviewers

- **Consolidated from #162 + #163 + #164** at Max's request, matching
how the RandBLAS installer set was combined. Those two show as *merged*
rather than closed because this branch was fast-forwarded to contain
them — they merged into their stacked base, not into `main`. The tree is
byte-identical to the state tested and green as three separate PRs.
- Stacked on #161 (CI fixes), which stays separate — it is independent
and useful on its own.
- **`-ModifyEnvironment` is not verified end to end.** It runs at the
end of a successful install, and I could not reach it: the clone sits on
a `\\wsl.localhost` path and Windows `git` refuses it with `dubious
ownership`. Precedence and the User-scope write are each verified
directly, but not together in one completed run.
- Two upstream gaps are filed rather than worked around silently: BLAS++
never looks for an ILP64 OpenBLAS (#166), and implements only Apple's
legacy Accelerate interface (#165).
mmelnich added a commit that referenced this pull request Aug 29, 2026
Brings in the six commits main gained since the 2026-08-05 refresh (5da0674):

  1cbe9e0  Native Windows (MSVC) support (#154)
  4cc72a8  TEMPORARY: quarantine the macOS Accelerate gesdd test (#157)
  e4e8565  Windows installation overhaul (#156)
  26eaed6  CI: fix the broken oneMKL cache path and drop a dead ctest exclusion (#161)
  ab05872  Installer overhaul: pins, provenance, run-not-just-link BLAS check (#162)
  d4ee721  Include the GPU layer from RandLAPACK.hh, guarded on __CUDACC__ (#169)

Two conflicts, both in test/, both from main's MSVC work colliding with branch
test infrastructure. Neither is library code.

test/CMakeLists.txt: kept BOTH sides. Main adds /bigobj and
randlapack_stage_runtime_dlls(RandLAPACK_tests); the branch adds
PROPERTIES TIMEOUT 300 to gtest_discover_tests. The ordering is forced rather
than stylistic: the DLL staging must precede gtest_discover_tests, because that
command runs the test binary at build time to enumerate cases and on Windows
that run needs the DLLs already staged.

test/drivers/test_cqrrt.cc: both sides made the same code change, atol from
eps^0.7 to eps^0.65, and differed only in the justification comment. Merged the
comment to credit both backends. Two unrelated BLAS implementations independently
put norm_0/sqrt(n) over the tighter bound on this case, vcpkg oneMKL sequential
on Windows at 4.4e-11 and Apple Accelerate on macOS at ~1.7e-11. That it was
found twice independently is what says the bound was wrong, not the backends.

Three things worth recording.

1. 26eaed6 removed the dead ctest exclusion
   --exclude-regex "^TestABRIK\.ABRIK_catch_instability" that this branch was
   still carrying in six places across three workflow files, so the branch no
   longer needs to. One correction to that PR's description, which states there
   is no history of the test under that name: edab935^ contains four such tests
   (_prelim, _good, _bad, _worse) and the regex was unanchored on the right, so
   it matched all four until edab935 deleted them on 2026-02-02. The exclusion
   was live once, not always inert.

2. Main's 4cc72a8 quarantines TestQB.Polynomial_Decay_general1 on macOS, while
   this branch's 5a6b6d3 fixed that same test on Apple Silicon by switching its
   reference SVD from gesdd to gesvd. Both survive the merge, so the
   quarantine's own "if this passes, delete the suppression" warning will now
   fire on every macOS run. Left in place deliberately: 4cc72a8 is a temporary
   main-side commit pinned to two open upstream PRs, and reverting it here would
   fight main. Flagged for the PR thread instead.

3. install.sh shrinks by 484 lines because ab05872 turned it into a wrapper
   delegating to install/install.sh. Nothing was lost, and the branch's own
   cluster-install optimisation from 2ca557e survives there as -Dbuild_tests=OFF
   for both blaspp and lapackpp, using those projects' actual lowercase option
   name rather than the branch's BUILD_TESTING.

The RandBLAS submodule advances from 8417f4b (1.1.0-32) to 952251c (1.1.0-42).
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant