Skip to content

The Windows CI jobs install a slim conda environment and cache none of it, and link with Visual Studio's tools - #527

Merged
ofloveandhate merged 14 commits into
bertiniteam:developfrom
ofloveandhate:windows-ci-caches-conda-archives-keyed-by-what-was-installed
Nov 7, 2026
Merged

ofloveandhate merged 14 commits into
bertiniteam:developfrom
ofloveandhate:windows-ci-caches-conda-archives-keyed-by-what-was-installed

Conversation

@ofloveandhate

@ofloveandhate ofloveandhate commented Nov 5, 2026 •

Copy link
Copy Markdown
Contributor

The repository's Actions caches were full: 8.9 GB of GitHub's 10 GB, all on develop, and not one Linux cache. The Windows jobs each cached their whole conda package folder, about 2.6 GB apiece, one for the C++ tests and one per Python version. Six of those don't fit, and while they evicted each other they evicted every other platform's caches and every pull request's, so PRs compiled cold.

This PR set out to cache the Windows environment better, measured whether that pays, and found it doesn't: the Windows jobs now install a slim environment and cache none of it. ADR-0078 records the numbers.

Moving the toolchain also exposed a crash on Windows that the old one had hidden, which this PR fixes. ADR-0079 records it.

What changes

  • The Windows environment is a third the size. environment-win.yml asks for clang >=20, which provides clang-cl, instead of the compilers metapackage: its Fortran compiler (flang) brought clang along with llvm-tools and lld, 830 MB of a 1153 MB download. It drops conda's git (115 MB; the runners have git) and takes OpenBLAS instead of MKL for numpy and scipy (109 MB; b2 links no BLAS). An install downloads ~325 MB.
  • No conda cache on Windows. Over three runs of this PR, with the archives cached and every job logging what conda reused: the two jobs that restored a 326.7 MB cache (in 5 s) installed in 178 s and 211 s, the 16 without one in 116-252 s, and conda rewrote all 81 restored archives, reusing none. Runner speed decides the install time, and the most a working cache could save is a download about as fast as its own restore. The ~1.9 GB it would hold stays free for ccache and the other platforms.
  • The STL version-gate override is gone. Visual Studio 2026's STL refuses Clang older than 20 (STL1000), and the conda clang was 19, so the builds defined _ALLOW_COMPILER_AND_STL_VERSION_MISMATCH. The pin is clang >=20 (22.1.8 today), which compiled everything without it.
  • The builds link with Visual Studio's tools. Without conda's lld, CMake took lld-link, llvm-rc and llvm-mt from the runner's own LLVM, which nothing pins. tools/msvc_link_tools.ps1 enters the developer environment of the newest Visual Studio (vswhere; the one clang-cl uses) for LIB, LIBPATH and INCLUDE, puts PATH back so conda's cmake, ninja, ccache and clang-cl stay in use, and returns link.exe, lib.exe, rc.exe and mt.exe, which both builds pass to CMake by full path.
  • bertini2 defines BOOST_JSON_NO_LIB. b2 uses Boost.JSON header-only, but on Windows Boost's auto-linking asked for boost_json.lib by name, and only the old linker happened to find one in conda's library folder.
  • The Windows build steps log what they used and fail where they fail: main conda package versions, which cmake/ninja/clang-cl/ccache run, clang-cl --version, and the compiler and link tools in CMakeCache.txt. A native command's exit code does not stop a pwsh step, so a failed link had surfaced only as ten tests "Not Run".
  • A thread frees its own MPFR caches (ADR-0079). With the new toolchain test_nag_algorithms died with heap corruption (0xc0000374). Boost.Multiprecision frees MPFR's per-thread caches from a thread_local destructor; conda-forge's Windows MPFR keeps that state in libgcc's emulated TLS, and in an executable the destructor ran after the state was freed, so a worker's exit read freed memory. The read does not depend on the compiler; why it became fatal here is not established. On Windows, mpfr_extensions.hpp now skips Boost's thread_local, and WorkerThreadPool calls ReleaseThreadMpfrCaches() last on every platform. thread_pool_workers_that_filled_mpfr_caches_exit_cleanly is the regression test.
  • A test no longer reads freed memory. earlier["accuracy_estimate"] = earlier.at(...) in zero_dim_records.cpp read through a reference the insert had invalidated. The debugger's heap made it fail, and AddressSanitizer reports it on Linux too.
  • The Windows C++ job can show a crash and keeps its compile work when a test fails. When a suite fails, a step reruns it under cdb and then under full page heap, naming each test case as it starts. ccache is saved right after the build, because actions/cache's post step is skipped when a job fails.

The wheel jobs' pip cache, which held only build and delvewheel, is dropped. The branch name records where this started.

After merging

The old windows-conda-* caches on develop (~2.6 GB each) can be deleted by hand or left to age out after 7 days unused.

🤖 Generated with Claude Code

ofloveandhate and others added 11 commits November 5, 2026 11:15
…ead of compilers, whose Fortran compiler brought clang along with ~600 MB of LLVM tools and flang; clang is now 22 instead of 19

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…runners have git, and nothing asked for conda's copy

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…ller; b2 links no BLAS, and the Linux jobs already test against OpenBLAS (numpy's pip wheels)

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…the environment file and the month, not the workflow: ~0.5 GB in place of six unpacked folders of ~2.6 GB each, which evicted every other platform's caches; the wheel tests restore it too

Of a ~320 MB solve, ~40 MB depends on the Python version, so whichever job saves the cache first leaves the others a few packages to download.  Unpacked copies are left out of the cache: conda unpacks an archive it finds.  Leaving the workflow file out of the key means editing the workflow reuses the cache; the month in it brings conda-forge's updates in.  The wheel builds' pip cache held only build and delvewheel and is dropped.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…he environment asks for clang >=20, which Visual Studio 2026's STL accepts, the proper fix the override's comment named

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…kage archives, keyed by the environment file and the month

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…month: each job restores the newest cache for its Python, installs, records conda list --explicit, prunes to those archives, and saves under the record's hash when it is new

A cache that hits saves nothing, so a key that stays the same would make every run download each conda-forge release since the save; the month only bounded that.  The record changes exactly when the contents do.  One cache per Python version (~320 MB each) stores the shared packages six times, but needs no job ahead of the Windows jobs to solve all six.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…ut Boost's auto-linking on Windows asked for boost_json.lib by name, and the link failed once the linker no longer searched conda's library folder

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
… the SDK's rc.exe and mt.exe, by full path, instead of whatever LLVM the runner image ships, which CMake took once conda's lld was gone

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…s/msvc_link_tools.ps1), and log what they were configured with, how the conda cache was used, and fail where the build fails

The conda activation does not run Visual Studio's setup script in PowerShell, so VCToolsInstallDir was unset and the previous run stopped before configuring.  tools/msvc_link_tools.ps1 takes link.exe and lib.exe from the default toolset of the newest Visual Studio, the one clang-cl finds, and rc.exe and mt.exe from the newest Windows SDK.  tools/conda_archive_cache.ps1 takes over the cache bookkeeping and reports, in every Windows job including the wheel tests, the archives reused from the cache, downloaded, and restored but unused, with the main package versions.  The build steps print clang-cl's version and the compiler and link tools in CMakeCache.txt, and now fail when the build does: a native command's exit code does not stop a pwsh step, so the first run's link failure surfaced only as ten tests Not Run.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…cache of the package archives saved no time, so it is gone, and the builds enter Visual Studio's developer environment so link.exe finds the Windows libraries

Over three runs, conda installs took 178 s and 211 s in the two jobs that restored a 326.7 MB cache and 116-252 s in the 16 that had none; conda rewrote all 81 restored archives, reusing none, and the most a working cache could save is a download about as fast as its own 5 s restore.  ADR-0078 records the numbers and the decision.  The previous run's link failed with LNK1104 kernel32.lib: link.exe needs LIB, which only Visual Studio's developer environment sets.  tools/msvc_link_tools.ps1 enters it, puts PATH back so the build keeps conda's cmake, ninja, ccache and clang-cl, and takes the four tools from the variables it sets.  The build steps print which cmake, ninja, clang-cl and ccache they run, and every Windows job prints the main conda package versions.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@ofloveandhate ofloveandhate changed the title The Windows CI jobs cache conda package archives, keyed by what was installed, and install a third as much The Windows CI jobs install a slim conda environment and cache none of it, and link with Visual Studio's tools Nov 6, 2026
ofloveandhate and others added 3 commits November 6, 2026 11:10
…e stack: test_nag_algorithms died with heap corruption (0xc0000374) under clang 22 and link.exe where it passed under clang 19 and lld-link

The first run of bertiniteam#527 that linked built everything and passed nine suites, and test_nag_algorithms exited with 0xc0000374 two seconds in, during the zero_dim tests, with no stack and its buffered stdout lost. The suites run in the C++ test job without the mimalloc hook (only the CLI and the Python module install it), so the corruption is in the C runtime heap itself.

The C++ test build now carries debug information (/Z7, which ccache can cache, and /DEBUG) and, when a suite fails, a step reruns each failed suite under cdb with Boost.Test naming every test case on stderr as it starts, under both thread settings, and lists the DLLs the binary imports. The package listing adds gmp, mpfr, mpc, libboost-headers and the two runtime packages.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
…s a scoped variable, it adds a run under full page heap, and the C++ job saves ccache before the tests so a failed test does not cost a cold build

The previous run's crash step stopped at a parse error, $failedLog: in a double-quoted string, so cdb never ran. test_nag_algorithms crashed the same way again, 0xc0000374 about two seconds in, so the crash is deterministic. The failed job skipped actions/cache's post step and saved nothing, so restore and save are now separate steps and the save comes right after the build.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…cleanup ran after the thread's MPFR state was freed, which corrupted the heap when a worker exited (ADR-0079)

Run under cdb, test_nag_algorithms faulted in mpfr_bernoulli_freecache reading 0xfeeefeee, called from mpfr_free_cache2 in the executable's __dyn_tls_dtor during thread shutdown, and under full page heap in libgcc's _emutls_get_address. conda-forge's Windows MPFR is built by gcc and keeps its thread-local state in emulated TLS, which was already freed. On Windows, mpfr_extensions.hpp now specializes mpfr_cleanup<true>::force_instantiate to skip Boost's thread_local object, and WorkerThreadPool, the only place b2 starts threads, calls ReleaseThreadMpfrCaches last on every platform. thread_pool_workers_that_filled_mpfr_caches_exit_cleanly is the regression test.

The serial rerun found a separate bug, in the test the_accuracy_estimates_are_recorded_under_names_that_say_their_coordinates: earlier["accuracy_estimate"] = earlier.at(...) read a value through a reference the insert had invalidated. AddressSanitizer reports it as heap-use-after-free on Linux too. The test now copies the value first.

The C++ test build drops the /Z7 and /DEBUG flags that found this, and keeps the step that reruns a failed suite under cdb.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@ofloveandhate
ofloveandhate merged commit a41bc00 into bertiniteam:develop Nov 7, 2026
51 checks passed
ofloveandhate added a commit that referenced this pull request Nov 8, 2026
…FR thread-exit fix (#527), Windows ccache (#524), the random state (#520) and solution metadata JSON (#522)

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

# Conflicts:
#	CHANGELOG.md
#	docs/adr/README.md
ofloveandhate added a commit that referenced this pull request Nov 8, 2026
…MPFR thread-exit fix, Windows ccache, the random state and solution metadata JSON (#531)

Brings `develop` into `integration/intervals`, so the interval work
builds on what has landed there since the branch was cut:

- #527: the slim Windows conda environment, clang 22 with Visual
Studio's `link.exe`, and the fix for heap corruption when a worker
thread exits on Windows (ADR-0078, ADR-0079).
- #524: ccache caches the Windows builds (ADR-0076).
- #520: the random state can be got and set (ADR-0077).
- #522: a solution's metadata round-trips through exact JSON.

It is a merge, not a rebase, as an integration branch takes its trunk.
Two files conflicted, both lists where each side added entries: the
4.1.0 Added block of the changelog (FLINT here; the random state and
solution metadata on develop) and the ADR index (0075 here; 0076 to 0079
on develop). Both sides are kept, in order. The workflow,
`core/CMakeLists.txt` and `environment-win.yml` merged without conflict,
and `libflint` stays in the slim Windows environment.

One commit beyond the merge: the Windows jobs print FLINT's, GMP's,
MPFR's and MPC's versions with the other main conda packages.

What this run shows for the first time: FLINT linked by `link.exe` on
Windows (#526 linked it with `lld-link`), and `test_flint` under clang
22.

🤖 Generated with [Claude Code](https://claude.com/claude-code)
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