From f8878c01562425f7c760f8e494b849fd37040f1f Mon Sep 17 00:00:00 2001
From: Justin Nolan <justin@nolan.de>
Date: Fri, 17 Jul 2026 17:20:17 +0200
Subject: [PATCH] Add cmake support

Revert Logger thread_local workaround for macOS duplicate-symbol investigation

TEMP: macOS failure diagnostics (nm TLV linkage + lldb backtraces)

TEMP: macos debugging

TEMP: macos debugging

TEMP: macos debugging

TEMP: macos debugging

Revert "TEMP: macOS failure diagnostics (nm TLV linkage + lldb backtraces)"

This reverts commit eb8d07008687f1be813107203ecf561d3ad5e3e0.
---
 .github/actions/openvic-sim-build/action.yml | 110 +++----
 .github/changed-files.yml                    |  17 +-
 .github/workflows/build-matrix.yml           |  36 +--
 .github/workflows/nightly-releases.yml       |  34 +--
 .github/workflows/releases.yml               |  28 +-
 .gitignore                                   |  17 +-
 .gitmodules                                  |  38 ---
 .pre-commit-config.yaml                      |   4 +-
 CMakeLists.txt                               | 132 +++++++++
 CMakePresets.json                            | 183 ++++++++++++
 README.md                                    |  22 +-
 SConstruct                                   | 197 -------------
 deps/CMakeLists.txt                          | 185 ++++++++++++
 deps/SCsub                                   | 291 -------------------
 deps/function2                               |   1 -
 deps/int128                                  |   1 -
 deps/lexy-vdf                                |   2 +-
 deps/memory                                  |   1 -
 deps/openvic-dataloader                      |   2 +-
 deps/ordered-map                             |   1 -
 deps/plf_colony                              |   1 -
 deps/spdlog                                  |   1 -
 deps/std_function/func                       |   1 -
 deps/type_safe                               |   1 -
 deps/xoshiro                                 |   1 -
 docs/ecs/determinism.md                      |   2 +-
 pyproject.toml                               |   7 -
 scripts                                      |   1 -
 tests/CMakeLists.txt                         |  39 +++
 tests/SCsub                                  |  64 ----
 tests/benchmarks/CMakeLists.txt              |  30 ++
 tests/benchmarks/SCsub                       |  67 -----
 tests/benchmarks/deps/SCsub                  |  40 ---
 tests/benchmarks/deps/nanobench              |   1 -
 tests/deps/SCsub                             | 217 --------------
 tests/deps/snitch                            |   1 -
 36 files changed, 699 insertions(+), 1077 deletions(-)
 create mode 100644 CMakeLists.txt
 create mode 100644 CMakePresets.json
 delete mode 100644 SConstruct
 create mode 100644 deps/CMakeLists.txt
 delete mode 100644 deps/SCsub
 delete mode 160000 deps/function2
 delete mode 160000 deps/int128
 delete mode 160000 deps/memory
 delete mode 160000 deps/ordered-map
 delete mode 160000 deps/plf_colony
 delete mode 160000 deps/spdlog
 delete mode 160000 deps/std_function/func
 delete mode 160000 deps/type_safe
 delete mode 160000 deps/xoshiro
 delete mode 160000 scripts
 create mode 100644 tests/CMakeLists.txt
 delete mode 100644 tests/SCsub
 create mode 100644 tests/benchmarks/CMakeLists.txt
 delete mode 100644 tests/benchmarks/SCsub
 delete mode 100644 tests/benchmarks/deps/SCsub
 delete mode 160000 tests/benchmarks/deps/nanobench
 delete mode 100644 tests/deps/SCsub
 delete mode 160000 tests/deps/snitch

diff --git a/.github/actions/openvic-sim-build/action.yml b/.github/actions/openvic-sim-build/action.yml
index ab5262cb6..326efdd74 100644
--- a/.github/actions/openvic-sim-build/action.yml
+++ b/.github/actions/openvic-sim-build/action.yml
@@ -1,63 +1,63 @@
 name: Build OpenVic-Simulation
-description: Setup and Build OpenVic-Simulation with the provided options
+description: Configure, build, and test OpenVic-Simulation with CMake
 
 inputs:
   identifier:
-    description: Identifier of this build
+    description: Identifier of this build (artifact name)
     required: true
-  target:
-    description: Target type to build for (template_release, template_debug, editor)
-    required: true
-  platform:
-    description: Platform to build for (windows, linux, macos)
+  preset:
+    description: CMake configure preset
     required: true
-  arch:
-    description: Architecture to build for (x86_32, x86_64, universal)
+  configuration:
+    description: Build configuration (Debug, Release, RelWithDebInfo)
     required: true
-  scons-flags:
-    description: Additional flags to send to SCons
-    default: 'build_ovsim_benchmarks=yes'
-  build-library:
-    description: Whether to build the library file
+  target:
+    description: Godot target flavor (template_debug, template_release, editor)
+    default: 'template_debug'
+  cmake-args:
+    description: Additional -D flags passed to the CMake configure step
+    default: ''
+  run-tests:
+    description: Whether to run the unit tests after building
     default: 'true'
-  cache-base-branch:
-    description: Branch to base the cache upon
-    default: 'master'
-  disable-cache:
-    description: Whether to disable the build cache
-    default: 'false'
 
 runs:
   using: composite
   steps:
-    - name: Setup build cache
-      uses: OpenVicProject/openvic-cache@master
-      if: ${{ inputs.disable-cache != 'true' }}
-      with:
-        cache-name: ${{ inputs.identifier }}
-        base-branch: ${{ inputs.cache-base-branch }}
-      continue-on-error: true
-
-    - name: Set up Python
-      uses: actions/setup-python@a309ff8b426b58ec0e2a45f0f869d46889d02405 # v6.2.0
-      with:
-        python-version: "3.x"
+    - name: Setup MSVC environment
+      if: ${{ runner.os == 'Windows' }}
+      shell: pwsh
+      run: |
+        $vswhere = "${env:ProgramFiles(x86)}\Microsoft Visual Studio\Installer\vswhere.exe"
+        $vsPath = & $vswhere -latest -products * -requires Microsoft.VisualStudio.Component.VC.Tools.x86.x64 -property installationPath
+        Import-Module (Join-Path $vsPath "Common7\Tools\Microsoft.VisualStudio.DevShell.dll")
+        Enter-VsDevShell -VsInstallPath $vsPath -SkipAutomaticLocation -DevCmdArguments '-arch=x64 -host_arch=x64'
+        Get-ChildItem env: | ForEach-Object { Add-Content -Path $env:GITHUB_ENV -Value "$($_.Name)=$($_.Value)" }
 
-    - name: Set up SCons
+    - name: Setup Ninja
       shell: bash
       run: |
-        python -c "import sys; print(sys.version)"
-        python -m pip install scons
-        scons --version
+        # Ninja is preinstalled on GitHub-hosted images; fall back to the
+        # image's package manager if a future image drops it.
+        if ! command -v ninja >/dev/null 2>&1; then
+          if [ "$RUNNER_OS" = "macOS" ]; then
+            brew install ninja
+          elif [ "$RUNNER_OS" = "Windows" ]; then
+            choco install ninja -y
+          else
+            sudo apt-get update -y && sudo apt-get install -y ninja-build
+          fi
+        fi
+        ninja --version
 
     - name: Install APT dependencies
-      if: ${{ inputs.platform == 'linux' }}
-      uses: awalsh128/cache-apt-pkgs-action@681749ae568c81c2037cb9185e38b709b261bd2f # v1.6.1
+      if: ${{ runner.os == 'Linux' }}
+      uses: awalsh128/cache-apt-pkgs-action@553a35bb8ebd9fcabcb1c9451aa4c98e1b4ca8a9 # v1.6.3
       with:
         packages: build-essential pkg-config libtbb-dev
 
     - name: Install and Set g++ to version 13
-      if: ${{ inputs.platform == 'linux' }}
+      if: ${{ runner.os == 'Linux' }}
       shell: sh
       run: |
         g++ --version
@@ -68,29 +68,37 @@ runs:
         sudo update-alternatives --set g++ /usr/bin/g++-13
         g++ --version
 
-    - name: Compile with SCons
-      uses: OpenVicProject/openvic-build@master
-      with:
-        platform: ${{ inputs.platform }}
-        target: ${{ inputs.target }}
-        sconsflags: arch=${{ inputs.arch }} ${{ inputs.build-library && 'build_ovsim_library=yes' }} ${{ inputs.scons-flags }}
+    - name: Configure
+      shell: bash
+      run: |
+        cmake --preset ${{ inputs.preset }} \
+          -DGODOTCPP_TARGET=${{ inputs.target }} \
+          ${{ inputs.cmake-args }}
 
-    - name: Delete compilation files
-      if: ${{ inputs.platform == 'windows' }}
-      shell: pwsh
+    - name: Build
+      shell: bash
+      run: |
+        CONFIG_LOWER=$(echo "${{ inputs.configuration }}" | tr '[:upper:]' '[:lower:]')
+        cmake --build --preset ${{ inputs.preset }}-$CONFIG_LOWER
+
+    - name: Run tests
+      if: ${{ inputs.run-tests == 'true' }}
+      shell: bash
       run: |
-        Remove-Item bin/* -Include *.exp,*.pdb -Force
+        CONFIG_LOWER=$(echo "${{ inputs.configuration }}" | tr '[:upper:]' '[:lower:]')
+        ctest --preset ${{ inputs.preset }}-$CONFIG_LOWER
 
     - name: Upload library artifact
       uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1
       with:
         name: ${{ github.event.repository.name }}-${{ inputs.identifier }}-library
         path: |
-          ${{ github.workspace }}/bin/libopenvic-simulation.*
+          ${{ github.workspace }}/out/build/${{ inputs.preset }}/bin/${{ inputs.configuration }}/libopenvic-simulation.a
+          ${{ github.workspace }}/out/build/${{ inputs.preset }}/bin/${{ inputs.configuration }}/openvic-simulation.lib
 
     - name: Upload executable artifact
       uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1
       with:
         name: ${{ github.event.repository.name }}-${{ inputs.identifier }}-executable
         path: |
-          ${{ github.workspace }}/bin/openvic-simulation.headless.*
+          ${{ github.workspace }}/out/build/${{ inputs.preset }}/bin/${{ inputs.configuration }}/openvic-simulation.headless*
diff --git a/.github/changed-files.yml b/.github/changed-files.yml
index f8fb54369..a06ff714c 100644
--- a/.github/changed-files.yml
+++ b/.github/changed-files.yml
@@ -4,23 +4,24 @@ everything: &everything
   - "**"
 
 # Determines if build actions should occur after static checks are ran. Broadly speaking, these
-#  files changing would result in SCons rebuilding the engine, or are otherwise pertinent to the
+#  files changing would result in CMake rebuilding, or are otherwise pertinent to the
 #  buildsystem itself.
 sources:
   - .github/{actions/*,workflows}/*.yml
-  - "**/{SConstruct,SCsub,*.py}"
+  - "**/CMakeLists.txt"
+  - "**/*.cmake"
+  - "**/CMakePresets.json"
+  - "**/*.py"
   - "**/*.{hpp,cpp,inc}"
-  - scripts
   - deps/**
   - tests/**
 
 pre-commit:
   - *everything
-  - '!scripts/**'
-  - '!deps/*/**'
-  - '!tests/deps/*/**'
-  - '!tests/benchmarks/deps/*/**'
+  # First-party submodules (openvic-dataloader, lexy-vdf) run their own checks.
+  - '!deps/openvic-dataloader/**'
+  - '!deps/lexy-vdf/**'
 
 # Determines which files are appropriate for running clangd-tidy checks on.
 clangd:
-  - "**/*.{hpp,cpp,inc}"
\ No newline at end of file
+  - "**/*.{hpp,cpp,inc}"
diff --git a/.github/workflows/build-matrix.yml b/.github/workflows/build-matrix.yml
index 0cb240549..40998fed2 100644
--- a/.github/workflows/build-matrix.yml
+++ b/.github/workflows/build-matrix.yml
@@ -17,46 +17,44 @@ jobs:
           - identifier: windows-debug
             os: windows-latest
             name: 🏁 Windows Debug
+            preset: windows-x64-md
+            configuration: Debug
             target: template_debug
-            platform: windows
-            arch: x86_64
-            test-bin-suffix: .md.exe
 
           - identifier: windows-release
             os: windows-latest
             name: 🏁 Windows Release
+            preset: windows-x64-md
+            configuration: Release
             target: template_release
-            platform: windows
-            arch: x86_64
-            test-bin-suffix: .md.exe
 
           - identifier: macos-debug
             os: macos-latest
             name: 🍎 macOS (universal) Debug
+            preset: macos-universal
+            configuration: Debug
             target: template_debug
-            platform: macos
-            arch: universal
 
           - identifier: macos-release
             os: macos-latest
             name: 🍎 macOS (universal) Release
+            preset: macos-universal
+            configuration: Release
             target: template_release
-            platform: macos
-            arch: universal
 
           - identifier: linux-debug
             os: ubuntu-22.04
             name: 🐧 Linux Debug
+            preset: linux-x64
+            configuration: Debug
             target: template_debug
-            platform: linux
-            arch: x86_64
 
           - identifier: linux-release
             os: ubuntu-22.04
             name: 🐧 Linux Release
+            preset: linux-x64
+            configuration: Release
             target: template_release
-            platform: linux
-            arch: x86_64
 
     steps:
       - name: Checkout project
@@ -69,10 +67,8 @@ jobs:
         uses: ./.github/actions/openvic-sim-build
         with:
           identifier: ${{ matrix.identifier }}
+          preset: ${{ matrix.preset }}
+          configuration: ${{ matrix.configuration }}
           target: ${{ matrix.target }}
-          platform: ${{ matrix.platform }}
-          arch: ${{ matrix.arch }}
-
-      - name: Run tests
-        run: |
-          ./tests/bin/openvic-simulation.tests.${{ matrix.platform }}.${{ matrix.target }}.${{ matrix.arch }}${{ matrix.test-bin-suffix || '' }}
\ No newline at end of file
+          # Benchmarks are built (not run) for compile coverage.
+          cmake-args: -DOPENVIC_SIM_BUILD_BENCHMARKS=ON
diff --git a/.github/workflows/nightly-releases.yml b/.github/workflows/nightly-releases.yml
index 9d1aab142..ba6530b1e 100644
--- a/.github/workflows/nightly-releases.yml
+++ b/.github/workflows/nightly-releases.yml
@@ -89,13 +89,13 @@ jobs:
           relevant_files=(
             "deps/*"
             "misc/*"
-            "scripts"
             "src/*.hpp"
             "src/*.cpp"
             ':!src/openvic-simulation/pch.hpp'
             ':!src/openvic-simulation/pch.cpp'
             "pyproject.toml"
-            "SConstruct"
+            "CMakeLists.txt"
+            "CMakePresets.json"
             ".github/workflows/builds.yml"
             ".github/actions/openvic-sim-release/action.yml"
             ".github/workflows/nightly-releases.yml"
@@ -131,27 +131,21 @@ jobs:
           - identifier: windows-release
             os: windows-latest
             name: 🏁 Windows Release
-            target: template_release
-            platform: windows
-            arch: x86_64
-            scons-flags: ''
+            preset: windows-x64-md
+            cmake-args: ''
 
           - identifier: macos-release
             os: macos-latest
             name: 🍎 macOS (universal) Release
-            target: template_release
-            platform: macos
-            arch: universal
-            scons-flags: 'lto=full'
+            preset: macos-universal
+            cmake-args: -DCMAKE_INTERPROCEDURAL_OPTIMIZATION=ON
 
           - identifier: linux-release
             os: ubuntu-latest
             name: 🐧 Linux Release
             runner: ubuntu-22.04
-            target: template_release
-            platform: linux
-            arch: x86_64
-            scons-flags: 'lto=full use_static_cpp=yes'
+            preset: linux-x64
+            cmake-args: -DCMAKE_INTERPROCEDURAL_OPTIMIZATION=ON
 
     steps:
       - name: Checkout project
@@ -164,11 +158,13 @@ jobs:
         uses: ./.github/actions/openvic-sim-build
         with:
           identifier: ${{ matrix.identifier }}
-          target: ${{ matrix.target }}
-          platform: ${{ matrix.platform }}
-          arch: ${{ matrix.arch }}
-          disable-cache: true
-          scons-flags: use_hot_reload=no build_ovsim_tests=no debug_symbols=yes optimize=speed_trace ${{ matrix.scons-flags }}
+          preset: ${{ matrix.preset }}
+          # Optimized build with debug symbols (the old optimize=speed_trace
+          # debug_symbols=yes flavor).
+          configuration: RelWithDebInfo
+          target: template_release
+          run-tests: false
+          cmake-args: -DOPENVIC_SIM_BUILD_TESTS=OFF ${{ matrix.cmake-args }}
 
   publish-nightly-release:
     name: Publish Release
diff --git a/.github/workflows/releases.yml b/.github/workflows/releases.yml
index b92ed2bc9..c5e2838b1 100644
--- a/.github/workflows/releases.yml
+++ b/.github/workflows/releases.yml
@@ -60,27 +60,21 @@ jobs:
           - identifier: windows-release
             os: windows-latest
             name: 🏁 Build Windows
-            target: template_release
-            platform: windows
-            arch: x86_64
-            scons-flags: ''
+            preset: windows-x64-md
+            cmake-args: ''
 
           - identifier: macos-release
             os: macos-latest
             name: 🍎 Build macOS
-            target: template_release
-            platform: macos
-            arch: universal
-            scons-flags: lto=full
+            preset: macos-universal
+            cmake-args: -DCMAKE_INTERPROCEDURAL_OPTIMIZATION=ON
 
           - identifier: linux-release
             os: ubuntu-latest
             name: 🐧 Build Linux
             runner: ubuntu-22.04
-            target: template_release
-            platform: linux
-            arch: x86_64
-            scons-flags: lto=full use_static_cpp=yes
+            preset: linux-x64
+            cmake-args: -DCMAKE_INTERPROCEDURAL_OPTIMIZATION=ON
 
     steps:
       - name: Checkout project
@@ -93,11 +87,11 @@ jobs:
         uses: ./.github/actions/openvic-sim-build
         with:
           identifier: ${{ matrix.identifier }}
-          target: ${{ matrix.target }}
-          platform: ${{ matrix.platform }}
-          arch: ${{ matrix.arch }}
-          disable-cache: true
-          scons-flags: use_hot_reload=no build_ovsim_tests=no ${{ matrix.scons-flags }}
+          preset: ${{ matrix.preset }}
+          configuration: Release
+          target: template_release
+          run-tests: false
+          cmake-args: -DOPENVIC_SIM_BUILD_TESTS=OFF ${{ matrix.cmake-args }}
 
   publish-release:
     name: Publish Release
diff --git a/.gitignore b/.gitignore
index deb314a91..d4417447e 100644
--- a/.gitignore
+++ b/.gitignore
@@ -44,7 +44,6 @@
 # Godot 4+ specific ignores
 .godot/
 game/bin/openvic/*
-.sconsign*.dblite
 
 # Binaries
 *.o
@@ -53,7 +52,6 @@ game/bin/openvic/*
 *.obj
 *.bc
 *.pyc
-*.dblite
 *.pdb
 *.lib
 bin/*
@@ -65,12 +63,6 @@ bin/*
 *.idb
 *.exp
 
-tests/bin/*
-tests/benchmarks/bin/*
-
-# Build configuarion.
-/custom.py
-
 # MacOS stuff
 .DS_Store
 
@@ -80,16 +72,17 @@ tests/benchmarks/bin/*
 # JetBrains
 .idea
 
-# scons stuff
+# clangd
 .cache
 compile_commands.json
 src/openvic-simulation/testing/test_results/results.txt
 
-# Out-of-source build directory
-/build/
-
 # ccls
 .ccls-cache
 
 # Generated source files
 src/openvic-simulation/gen/*
+
+# CMake out-of-source build directory
+out/
+CMakeUserPresets.json
diff --git a/.gitmodules b/.gitmodules
index f90bc2c3e..a7e16c92c 100644
--- a/.gitmodules
+++ b/.gitmodules
@@ -1,44 +1,6 @@
 [submodule "deps/openvic-dataloader"]
 	path = deps/openvic-dataloader
 	url = https://github.com/OpenVicProject/OpenVic-Dataloader
-[submodule "scripts"]
-	path = scripts
-	url = https://github.com/OpenVicProject/scripts
 [submodule "deps/lexy-vdf"]
 	path = deps/lexy-vdf
 	url = https://github.com/OpenVicProject/lexy-vdf
-[submodule "deps/ordered-map"]
-	path = deps/ordered-map
-	url = https://github.com/Tessil/ordered-map
-[submodule "deps/plf_colony"]
-	path = deps/plf_colony
-	url = https://github.com/mattreecebentley/plf_colony
-[submodule "tests/deps/snitch"]
-	path = tests/deps/snitch
-	url = https://github.com/snitch-org/snitch
-	ignore = dirty
-[submodule "deps/function2"]
-	path = deps/function2
-	url = https://github.com/Naios/function2
-[submodule "deps/std_function/func"]
-	path = deps/std_function/func
-	url = https://github.com/skarupke/std_function
-[submodule "tests/benchmarks/deps/nanobench"]
-	path = tests/benchmarks/deps/nanobench
-	url = https://github.com/Spartan322/nanobench
-[submodule "deps/memory"]
-	path = deps/memory
-	url = https://github.com/foonathan/memory
-	ignore = dirty
-[submodule "deps/spdlog"]
-	path = deps/spdlog
-	url = https://github.com/gabime/spdlog
-[submodule "deps/xoshiro"]
-	path = deps/xoshiro
-	url = https://github.com/reputeless/xoshiro-cpp
-[submodule "deps/type_safe"]
-	path = deps/type_safe
-	url = https://github.com/foonathan/type_safe
-[submodule "deps/int128"]
-	path = deps/int128
-	url = https://github.com/cppalliance/int128
diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml
index 17ea48587..c3df7c22b 100644
--- a/.pre-commit-config.yaml
+++ b/.pre-commit-config.yaml
@@ -37,10 +37,10 @@ repos:
     hooks:
       - id: ruff-check
         args: [--fix]
-        files: (\.py|SConstruct|SCsub)$
+        files: \.py$
         types_or: [text]
       - id: ruff-format
-        files: (\.py|SConstruct|SCsub)$
+        files: \.py$
         types_or: [text]
 
   - repo: https://github.com/pre-commit/mirrors-mypy
diff --git a/CMakeLists.txt b/CMakeLists.txt
new file mode 100644
index 000000000..7dc5b1347
--- /dev/null
+++ b/CMakeLists.txt
@@ -0,0 +1,132 @@
+cmake_minimum_required(VERSION 3.28)
+project(openvic-simulation LANGUAGES CXX)
+
+if(NOT COMMAND openvic_setup_base_flags)
+    # scripts is the shared build-tooling repo, fetched once as a pinned
+    # tarball (like the other third-party deps). In a composed build the
+    # outermost repo fetches + loads it first and this guard skips for the
+    # nested repos; a standalone repo fetches it itself. FetchContent also
+    # dedups by name, so either path yields exactly one fetch.
+    #
+    # Raw FetchContent (not openvic_declare_dep) on purpose: openvic_declare_dep
+    # lives INSIDE scripts (OpenVicDeps.cmake), so it isn't defined yet here.
+    # Local-dev escape hatch: -DFETCHCONTENT_SOURCE_DIR_OPENVIC_SCRIPTS=<path>
+    # points this at a working-tree scripts checkout.
+    include(FetchContent)
+    FetchContent_Declare(
+        openvic_scripts
+        URL "https://github.com/OpenVicProject/scripts/archive/ef469e126854a7c2e1293ac008b2712d0eafad8e.tar.gz"
+        URL_HASH SHA256=d9997aab0d095dd2d9211515d0701a36c205b19e8f240b88ce113c38b8eb36cf
+    )
+    FetchContent_MakeAvailable(openvic_scripts)
+    include("${openvic_scripts_SOURCE_DIR}/cmake/OpenVicScripts.cmake")
+endif()
+openvic_setup_base_flags()
+openvic_disable_rtti()
+
+option(OPENVIC_USE_PCH "Precompile openvic-simulation/pch.hpp" ON)
+
+add_subdirectory(deps)
+
+file(GLOB_RECURSE sim_sources CONFIGURE_DEPENDS src/openvic-simulation/*.cpp)
+list(REMOVE_ITEM sim_sources ${CMAKE_CURRENT_SOURCE_DIR}/src/openvic-simulation/pch.cpp)
+
+add_library(openvic-simulation STATIC ${sim_sources})
+add_library(openvic::simulation ALIAS openvic-simulation)
+
+set(sim_gen_dir ${CMAKE_CURRENT_BINARY_DIR}/gen-include)
+target_include_directories(
+    openvic-simulation
+    PUBLIC src $<BUILD_INTERFACE:${sim_gen_dir}>
+    PRIVATE src/openvic-simulation
+)
+
+openvic_generate_commit_info(
+    TARGET openvic-simulation
+    PREFIX sim
+    REPO_DIR ${CMAKE_CURRENT_SOURCE_DIR}
+    OUTPUT ${sim_gen_dir}/openvic-simulation/gen/commit_info.gen.hpp
+)
+openvic_generate_license_info(
+    TARGET openvic-simulation
+    PREFIX sim
+    COPYRIGHT ${CMAKE_CURRENT_SOURCE_DIR}/COPYRIGHT
+    LICENSE ${CMAKE_CURRENT_SOURCE_DIR}/LICENSE.md
+    OUTPUT ${sim_gen_dir}/openvic-simulation/gen/license_info.gen.hpp
+)
+openvic_generate_author_info(
+    TARGET openvic-simulation
+    PREFIX sim
+    AUTHORS ${CMAKE_CURRENT_SOURCE_DIR}/AUTHORS.md
+    OUTPUT ${sim_gen_dir}/openvic-simulation/gen/author_info.gen.hpp
+    SECTIONS
+        "Senior Developers=AUTHORS_SENIOR_DEVELOPERS"
+        "Developers=AUTHORS_DEVELOPERS"
+        "Contributors=AUTHORS_CONTRIBUTORS"
+        "Consultants=AUTHORS_CONSULTANTS"
+)
+
+if(OPENVIC_USE_PCH)
+    target_precompile_headers(openvic-simulation PRIVATE src/openvic-simulation/pch.hpp)
+endif()
+
+# PUBLIC deps are exposed to consumers (everything except lexy, which is an
+# implementation detail of the parsers and stays private).
+target_link_libraries(
+    openvic-simulation
+    PUBLIC
+        openvic::dataloader
+        openvic::lexy-vdf
+        tsl::ordered_map
+        function2::function2
+        type_safe
+        Boost::int128
+        spdlog::spdlog
+        ov_plf_colony
+        ov_std_function
+        ov_xoshiro
+        ov_foonathan_memory
+)
+
+if(CMAKE_SYSTEM_NAME STREQUAL "Linux")
+    find_package(PkgConfig REQUIRED)
+    pkg_check_modules(TBB REQUIRED IMPORTED_TARGET tbb)
+    target_link_libraries(openvic-simulation PUBLIC PkgConfig::TBB)
+endif()
+
+if(APPLE)
+    # clang emits the TLS init aliases (_ZTH*) for inline thread_local members
+    # as non-weak private-external symbols, one per TU that odr-uses them, and
+    # the Xcode 26 linker rejects the duplicates unless it runs with
+    # -dead_strip (the old scons build always linked with it). INTERFACE so
+    # every consumer linking this archive gets the flag.
+    target_link_options(openvic-simulation INTERFACE LINKER:-dead_strip)
+endif()
+
+set_target_properties(openvic-simulation PROPERTIES ARCHIVE_OUTPUT_DIRECTORY "${CMAKE_BINARY_DIR}/bin/$<CONFIG>")
+
+# Headless executable, unit tests, and benchmarks: built by default only when
+# openvic-simulation is the top-level project (composed builds just want the
+# library). Benchmarks are opt-in even standalone, matching the old default.
+option(OPENVIC_SIM_BUILD_HEADLESS "Build the openvic-simulation headless executable" ${PROJECT_IS_TOP_LEVEL})
+option(OPENVIC_SIM_BUILD_TESTS "Build the openvic-simulation unit tests" ${PROJECT_IS_TOP_LEVEL})
+option(OPENVIC_SIM_BUILD_BENCHMARKS "Build the openvic-simulation benchmarks" OFF)
+
+if(OPENVIC_SIM_BUILD_HEADLESS)
+    file(GLOB_RECURSE ovsim_headless_sources CONFIGURE_DEPENDS src/headless/*.cpp)
+    add_executable(openvic-simulation-headless ${ovsim_headless_sources})
+    target_compile_definitions(openvic-simulation-headless PRIVATE OPENVIC_SIM_HEADLESS)
+    target_include_directories(openvic-simulation-headless PRIVATE src/headless)
+    target_link_libraries(openvic-simulation-headless PRIVATE openvic::simulation)
+    set_target_properties(
+        openvic-simulation-headless
+        PROPERTIES
+            OUTPUT_NAME "openvic-simulation.headless"
+            RUNTIME_OUTPUT_DIRECTORY "${CMAKE_BINARY_DIR}/bin/$<CONFIG>"
+    )
+endif()
+
+if(OPENVIC_SIM_BUILD_TESTS OR OPENVIC_SIM_BUILD_BENCHMARKS)
+    enable_testing()
+    add_subdirectory(tests)
+endif()
diff --git a/CMakePresets.json b/CMakePresets.json
new file mode 100644
index 000000000..54856a603
--- /dev/null
+++ b/CMakePresets.json
@@ -0,0 +1,183 @@
+{
+    "version": 6,
+    "cmakeMinimumRequired": {
+        "major": 3,
+        "minor": 27
+    },
+    "configurePresets": [
+        {
+            "name": "base",
+            "hidden": true,
+            "generator": "Ninja Multi-Config",
+            "binaryDir": "${sourceDir}/out/build/${presetName}",
+            "cacheVariables": {
+                "CMAKE_EXPORT_COMPILE_COMMANDS": "ON",
+                "CMAKE_CONFIGURATION_TYPES": "Release;RelWithDebInfo;Debug"
+            }
+        },
+        {
+            "name": "p-windows-x64",
+            "hidden": true,
+            "inherits": "base",
+            "condition": {
+                "type": "equals",
+                "lhs": "${hostSystemName}",
+                "rhs": "Windows"
+            },
+            "architecture": {
+                "value": "x64",
+                "strategy": "external"
+            }
+        },
+        {
+            "name": "windows-x64-md",
+            "inherits": "p-windows-x64",
+            "cacheVariables": {
+                "CMAKE_MSVC_RUNTIME_LIBRARY": "MultiThreadedDLL"
+            }
+        },
+        {
+            "name": "windows-x64-mt",
+            "inherits": "p-windows-x64",
+            "cacheVariables": {
+                "CMAKE_MSVC_RUNTIME_LIBRARY": "MultiThreaded"
+            }
+        },
+        {
+            "name": "linux-x64",
+            "inherits": "base",
+            "condition": {
+                "type": "equals",
+                "lhs": "${hostSystemName}",
+                "rhs": "Linux"
+            },
+            "cacheVariables": {
+                "CMAKE_CXX_COMPILER": "g++"
+            }
+        },
+        {
+            "name": "macos-universal",
+            "inherits": "base",
+            "condition": {
+                "type": "equals",
+                "lhs": "${hostSystemName}",
+                "rhs": "Darwin"
+            },
+            "cacheVariables": {
+                "CMAKE_OSX_ARCHITECTURES": "x86_64;arm64"
+            }
+        }
+    ],
+    "buildPresets": [
+        {
+            "name": "windows-x64-md-release",
+            "configurePreset": "windows-x64-md",
+            "configuration": "Release"
+        },
+        {
+            "name": "windows-x64-md-debug",
+            "configurePreset": "windows-x64-md",
+            "configuration": "Debug"
+        },
+        {
+            "name": "windows-x64-md-relwithdebinfo",
+            "configurePreset": "windows-x64-md",
+            "configuration": "RelWithDebInfo"
+        },
+        {
+            "name": "windows-x64-mt-release",
+            "configurePreset": "windows-x64-mt",
+            "configuration": "Release"
+        },
+        {
+            "name": "windows-x64-mt-debug",
+            "configurePreset": "windows-x64-mt",
+            "configuration": "Debug"
+        },
+        {
+            "name": "windows-x64-mt-relwithdebinfo",
+            "configurePreset": "windows-x64-mt",
+            "configuration": "RelWithDebInfo"
+        },
+        {
+            "name": "linux-x64-release",
+            "configurePreset": "linux-x64",
+            "configuration": "Release"
+        },
+        {
+            "name": "linux-x64-debug",
+            "configurePreset": "linux-x64",
+            "configuration": "Debug"
+        },
+        {
+            "name": "linux-x64-relwithdebinfo",
+            "configurePreset": "linux-x64",
+            "configuration": "RelWithDebInfo"
+        },
+        {
+            "name": "macos-universal-release",
+            "configurePreset": "macos-universal",
+            "configuration": "Release"
+        },
+        {
+            "name": "macos-universal-debug",
+            "configurePreset": "macos-universal",
+            "configuration": "Debug"
+        },
+        {
+            "name": "macos-universal-relwithdebinfo",
+            "configurePreset": "macos-universal",
+            "configuration": "RelWithDebInfo"
+        }
+    ],
+    "testPresets": [
+        {
+            "name": "windows-x64-md-debug",
+            "configurePreset": "windows-x64-md",
+            "configuration": "Debug",
+            "output": { "outputOnFailure": true }
+        },
+        {
+            "name": "windows-x64-md-release",
+            "configurePreset": "windows-x64-md",
+            "configuration": "Release",
+            "output": { "outputOnFailure": true }
+        },
+        {
+            "name": "windows-x64-mt-debug",
+            "configurePreset": "windows-x64-mt",
+            "configuration": "Debug",
+            "output": { "outputOnFailure": true }
+        },
+        {
+            "name": "windows-x64-mt-release",
+            "configurePreset": "windows-x64-mt",
+            "configuration": "Release",
+            "output": { "outputOnFailure": true }
+        },
+        {
+            "name": "linux-x64-debug",
+            "configurePreset": "linux-x64",
+            "configuration": "Debug",
+            "output": { "outputOnFailure": true }
+        },
+        {
+            "name": "linux-x64-release",
+            "configurePreset": "linux-x64",
+            "configuration": "Release",
+            "output": { "outputOnFailure": true }
+        },
+        {
+            "name": "macos-universal-debug",
+            "configurePreset": "macos-universal",
+            "configuration": "Debug",
+            "output": { "outputOnFailure": true }
+        },
+        {
+            "name": "macos-universal-release",
+            "configurePreset": "macos-universal",
+            "configuration": "Release",
+            "output": { "outputOnFailure": true }
+        }
+    ]
+}
diff --git a/README.md b/README.md
index d87bf9d27..ad2fbca6b 100644
--- a/README.md
+++ b/README.md
@@ -7,19 +7,17 @@ Repo of the OpenVic-Simulation Library for [OpenVic](https://github.com/OpenVicP
 For detailed instructions, view the OpenVic Contributor Quickstart Guide [here](https://github.com/OpenVicProject/OpenVic/blob/master/docs/contribution-quickstart-guide.md)
 
 ## Required
-* [scons](https://scons.org/)
+* [CMake](https://cmake.org/) 3.28+
+* [Ninja](https://ninja-build.org/)
+* Python 3 (used by the build for code generation)
 
 ## Build Instructions
-1. Install [scons](https://scons.org/) for your system.
-2. Run the command `git submodule update --init --recursive` to retrieve all related submodules.
-3. Run `scons` in the project root, you should see a openvic-simulation.headless file in `bin`.
+1. Run the command `git submodule update --init` to retrieve the first-party submodules (openvic-dataloader, lexy-vdf). Third-party dependencies are fetched automatically by CMake.
+2. Pick a configure preset from `CMakePresets.json` (`windows-x64-md`, `windows-x64-mt`, `linux-x64`, `macos-universal`) and run `cmake --preset <preset>` in the project root.
+3. Run `cmake --build --preset <preset>-debug` (or `<preset>-release`). The static library, headless executable, and unit tests land in `out/build/<preset>/bin/<Config>/`.
+4. Run the tests with `ctest --preset <preset>-debug`.
 
-## Link Instructions
-1. Call `ovsim_env = SConscript("openvic-simulation/SConstruct")`
-2. Use the values stored in the `ovsim_env.openvic_simulation` to link and compile against:
+The headless executable and tests are built by default in standalone builds; disable with `-DOPENVIC_SIM_BUILD_HEADLESS=OFF` / `-DOPENVIC_SIM_BUILD_TESTS=OFF`. Benchmarks are opt-in: `-DOPENVIC_SIM_BUILD_BENCHMARKS=ON`.
 
-| Variable Name | Description                               | Correlated ENV variable   |
-| ---           | ---                                       | ---                       |
-| `LIBPATH`     | Library path list                         | `env["LIBPATH"]`          |
-| `LIBS`        | Library files names in the library paths  | `env["LIBS"]`             |
-| `INCPATH`     | Library include files                     | `env["CPPPATH"]`          |
+## Link Instructions
+Use CMake: `add_subdirectory(openvic-simulation)` and link against `openvic::simulation`.
diff --git a/SConstruct b/SConstruct
deleted file mode 100644
index 2fdc6203f..000000000
--- a/SConstruct
+++ /dev/null
@@ -1,197 +0,0 @@
-#!/usr/bin/env python
-
-import os
-
-BINDIR = "bin"
-
-env = SConscript("scripts/SConstruct")
-
-env.PrependENVPath("PATH", os.getenv("PATH"))
-
-opts = env.SetupOptions()
-
-opts.Add(BoolVariable("build_ovsim_tests", "Build the openvic simulation unit tests", env.is_standalone))
-opts.Add(BoolVariable("run_ovsim_tests", "Run the openvic simulation unit tests", False))
-opts.Add(BoolVariable("build_ovsim_benchmarks", "Build the openvic simulation benchmarks", False))
-opts.Add(BoolVariable("run_ovsim_benchmarks", "Run the openvic simulation benchmarks", False))
-opts.Add(
-    BoolVariable(
-        key="build_ovsim_library",
-        help="Build the openvic simulation library.",
-        default=env.get("build_ovsim_library", not env.is_standalone),
-    )
-)
-opts.Add(BoolVariable("build_ovsim_headless", "Build the openvic simulation headless executable", env.is_standalone))
-
-env.FinalizeOptions()
-
-suffix = ".{}.{}".format(env["platform"], env["target"])
-if env.dev_build:
-    suffix += ".dev"
-if env["precision"] == "double":
-    suffix += ".double"
-suffix += "." + env["arch"]
-if env["platform"] == "windows":
-    if env.get("debug_crt", False):
-        suffix += ".mdd"
-    elif env.get("use_static_cpp", False):
-        suffix += ".mt"
-    else:
-        suffix += ".md"
-if env.get("use_asan", False):
-    suffix += ".san"
-env["suffix"] = suffix
-
-build_dir = env.Dir("build/" + suffix.lstrip(".")).abspath.replace("\\", "/")
-env["build_dir"] = build_dir
-
-env.exposed_includes = []
-
-SConscript("deps/SCsub", "env")
-
-env.openvic_simulation = {}
-
-# Tweak this if you want to use different folders, or more folders, to store your source code in.
-source_path = "src/openvic-simulation"
-include_path = "src"
-# Out-of-source build: variant tree holds object files and generated headers,
-# not copies of the source. Compile diagnostics therefore reference original
-# source paths.
-sim_variant = build_dir + "/" + source_path  # forward slashes so VariantDir matches
-sim_variant_parent = build_dir + "/" + include_path  # variant of "src/"
-env.VariantDir(sim_variant, source_path, duplicate=False)
-
-env.Append(CPPPATH=[[env.Dir(p) for p in [sim_variant, sim_variant_parent, source_path, include_path]]])
-
-gen_commit_info = env.CommandNoCache(
-    sim_variant + "/gen/commit_info.gen.hpp",
-    env.Value(env.get_git_info("sim")),
-    env.Run(env.git_builder),
-    name_prefix="sim",
-)
-gen_license_info = env.CommandNoCache(
-    sim_variant + "/gen/license_info.gen.hpp",
-    ["COPYRIGHT", "LICENSE.md"],
-    env.Run(env.license_builder),
-    name_prefix="sim",
-)
-gen_author_info = env.CommandNoCache(
-    sim_variant + "/gen/author_info.gen.hpp",
-    "AUTHORS.md",
-    env.Run(env.author_builder),
-    name_prefix="sim",
-    sections={
-        "Senior Developers": "AUTHORS_SENIOR_DEVELOPERS",
-        "Developers": "AUTHORS_DEVELOPERS",
-        "Contributors": "AUTHORS_CONTRIBUTORS",
-        "Consultants": "AUTHORS_CONSULTANTS",
-    },
-)
-gen_files = gen_commit_info + gen_license_info + gen_author_info
-Default(gen_commit_info, gen_license_info, gen_author_info)
-
-# For future reference:
-# - CCFLAGS are compilation flags shared between C and C++
-# - CFLAGS are for C-specific compilation flags
-# - CXXFLAGS are for C++-specific compilation flags
-# - CPPFLAGS are for pre-processor flags
-# - CPPDEFINES are for pre-processor defines
-# - LINKFLAGS are for linking flags
-
-# Exclude pch.cpp from the regular source list so it isn't compiled twice
-# (env.PCH below builds it once with /Yc).
-pch_cpp_source = source_path + "/pch.cpp"
-pch_cpp_variant = sim_variant + "/pch.cpp"
-sources = env.GlobRecursiveVariant("*.cpp", source_path, sim_variant, pch_cpp_source)
-env.simulation_sources = sources
-
-library = None
-env["OBJSUFFIX"] = suffix + env["OBJSUFFIX"]
-library_name = "libopenvic-simulation{}{}".format(suffix, env["LIBSUFFIX"])
-
-# Precompiled header
-if env.get("is_msvc", False):
-    pch_header_rel = "openvic-simulation/pch.hpp"
-    env["PCHSTOP"] = pch_header_rel
-    pch_pch, pch_obj = env.PCH(pch_cpp_variant)
-    env["PCH"] = pch_pch
-    env.Append(CCFLAGS=["/FI" + pch_header_rel])
-    sources.append(pch_obj)
-
-default_args = []
-
-if env["run_ovsim_tests"]:
-    env["build_ovsim_tests"] = True
-
-if env["run_ovsim_benchmarks"]:
-    env["build_ovsim_benchmarks"] = True
-
-if env["build_ovsim_tests"] or env["build_ovsim_benchmarks"]:
-    env["build_ovsim_library"] = True
-
-if env["build_ovsim_library"]:
-    library = env.StaticLibrary(target=env.File(os.path.join(BINDIR, library_name)), source=sources)
-    default_args += [library]
-
-    # Ensure `scons -c` wipes the per-config build dir, including any stragglers
-    # (gen headers, .pch caches) not directly attached to the library target.
-    env.Clean(library, env.Dir(build_dir))
-
-    env.Append(LIBPATH=[env.Dir(BINDIR)])
-    env.Prepend(LIBS=[library_name])
-
-    env.openvic_simulation["LIBPATH"] = env["LIBPATH"]
-    env.openvic_simulation["LIBS"] = env["LIBS"]
-    # Variant parent for generated headers (gen/*.gen.hpp); source parent for
-    # authored headers (MSVC's preprocessor needs both physically in -I).
-    env.openvic_simulation["INCPATH"] = [env.Dir(sim_variant_parent), env.Dir(include_path)] + env.exposed_includes
-    env.openvic_simulation["GEN_FILES"] = gen_files
-
-headless_program = None
-env["PROGSUFFIX"] = suffix + env["PROGSUFFIX"]
-
-if env["build_ovsim_headless"]:
-    headless_name = "openvic-simulation"
-    headless_env = env.Clone()
-    headless_src = "src/headless"
-    headless_variant = build_dir + "/" + headless_src
-    headless_env.VariantDir(headless_variant, headless_src, duplicate=False)
-    headless_env.Append(CPPDEFINES=["OPENVIC_SIM_HEADLESS"])
-    headless_env.Append(CPPPATH=[headless_env.Dir(headless_variant), headless_env.Dir(headless_src)])
-    headless_env.headless_sources = env.GlobRecursiveVariant("*.cpp", headless_src, headless_variant)
-    if not env["build_ovsim_library"]:
-        headless_env.headless_sources += sources
-    headless_program = headless_env.Program(
-        target=os.path.join(BINDIR, headless_name),
-        source=headless_env.headless_sources,
-        PROGSUFFIX=".headless" + env["PROGSUFFIX"],
-    )
-    default_args += [headless_program]
-
-    # Also wipe the per-config build dir when the headless target is cleaned,
-    # in case the library target isn't built in this configuration.
-    headless_env.Clean(headless_program, headless_env.Dir(build_dir))
-
-if env["build_ovsim_tests"]:
-    tests_env = SConscript("tests/SCsub", "env")
-
-    if env["run_ovsim_tests"]:
-        tests_env.RunUnitTest()
-
-if env["build_ovsim_benchmarks"]:
-    benchmarks_env = SConscript("tests/benchmarks/SCsub", {"env": tests_env if env["build_ovsim_tests"] else env})
-
-    if env["run_ovsim_benchmarks"]:
-        benchmarks_env.RunBenchmarks()
-
-# Add compiledb if the option is set
-if env.get("compiledb", False):
-    default_args += ["compiledb"]
-
-Default(*default_args)
-
-if "env" in locals():
-    # FIXME: This method mixes both cosmetic progress stuff and cache handling...
-    env.show_progress(env)
-
-Return("env")
diff --git a/deps/CMakeLists.txt b/deps/CMakeLists.txt
new file mode 100644
index 000000000..db6917145
--- /dev/null
+++ b/deps/CMakeLists.txt
@@ -0,0 +1,185 @@
+# Dependency wiring for the simulation. Third-party deps are fetched as
+# pinned source tarballs (see scripts/cmake/OpenVicDeps.cmake) so they build
+# out of the build tree's _deps/ dir, immune to working-tree churn.
+# They keep EXCLUDE_FROM_ALL (via openvic_declare_dep) so their install rules
+# stay out of `cmake --install` (the only install rule in the whole build is
+# the OpenVic root's copy of the extension library).
+
+# --- OpenVic libraries (first-party: own CMakeLists, composed from submodule
+# so they rebuild on branch switches) ---
+# openvic-dataloader first: its vendored lexy claims the lexy target names,
+# so lexy-vdf reuses it (first FetchContent declaration wins).
+add_subdirectory(openvic-dataloader)
+add_subdirectory(lexy-vdf)
+
+# --- header-only deps with upstream CMake ---
+openvic_declare_dep(ordered-map
+    REPO Tessil/ordered-map
+    SHA bd8d5ef4149cd40783a486011778a2e7eedde441
+    SHA256 39f458bf3808b2a83371580bcbb48b0336fbaa95cb1228502346f934a4f5d49e
+)
+FetchContent_MakeAvailable(ordered-map) # tsl::ordered_map
+
+openvic_declare_dep(function2
+    REPO Naios/function2
+    SHA 43fc0ca473ecb081918709bd7d524d84c2ff8dce
+    SHA256 7bd09f73b1d0de46885e117e9ef094ade2c771917d126e13451e3cbb9b228c20
+)
+FetchContent_MakeAvailable(function2) # function2::function2
+
+openvic_declare_dep(int128
+    REPO cppalliance/int128
+    SHA ea0f85e693767ae667e26d66022d6ed68d3425de
+    SHA256 b0685286c9b5ff29e7f94772dbf9600b865dcdcf8da96009c553a1d1da23594a
+)
+FetchContent_MakeAvailable(int128) # Boost::int128
+
+# --- header-only deps without upstream CMake: fetch source-only, hand-roll
+# INTERFACE targets (${<name>_SOURCE_DIR} is the extracted tarball root) ---
+openvic_declare_dep(plf_colony
+    REPO mattreecebentley/plf_colony
+    SHA fbc8483fb785c431dc4410396815abf886728c40
+    SHA256 d7006bebdc42694491d11e2d4d95417848a5fa2fff2caade2b123bb99479d098
+    DOWNLOAD_ONLY
+)
+FetchContent_MakeAvailable(plf_colony)
+add_library(ov_plf_colony INTERFACE)
+target_include_directories(ov_plf_colony SYSTEM INTERFACE ${plf_colony_SOURCE_DIR})
+
+# Consumers include std_function as <func/function.h>. Extract into a `func/`
+# subdir and point the include dir at its parent to preserve that prefix.
+openvic_declare_dep(std_function
+    REPO skarupke/std_function
+    SHA dbf2df963e6d0a47ab5e21e93c838233cd8b6b03
+    SHA256 b0f2d651ca6e0d4c112434ceab78785b4f5e19422821f78c19c57e77a84f7935
+    DOWNLOAD_ONLY
+    SOURCE_DIR ${CMAKE_BINARY_DIR}/_deps/std_function-src/func
+)
+FetchContent_MakeAvailable(std_function)
+add_library(ov_std_function INTERFACE)
+get_filename_component(ov_std_function_include "${std_function_SOURCE_DIR}" DIRECTORY)
+target_include_directories(ov_std_function SYSTEM INTERFACE ${ov_std_function_include})
+
+openvic_declare_dep(xoshiro
+    REPO reputeless/xoshiro-cpp
+    SHA 19bcbb2ce0ed158233187f524fd0964c105a65b3
+    SHA256 973884cfb77f2f979f9ac09e8846dfd758af4bc08422b7c92a0e676a3fb780ff
+    DOWNLOAD_ONLY
+)
+FetchContent_MakeAvailable(xoshiro)
+add_library(ov_xoshiro INTERFACE)
+target_include_directories(ov_xoshiro SYSTEM INTERFACE ${xoshiro_SOURCE_DIR})
+
+# --- type_safe (assertions/wrapper enabled only on template_release;
+# arithmetic policy defaults to checked for dev builds, ub otherwise) ---
+if(GODOTCPP_TARGET STREQUAL "template_release")
+    set(TYPE_SAFE_ENABLE_ASSERTIONS ON)
+    set(TYPE_SAFE_ENABLE_WRAPPER ON)
+else()
+    set(TYPE_SAFE_ENABLE_ASSERTIONS OFF)
+    set(TYPE_SAFE_ENABLE_WRAPPER OFF)
+endif()
+set(TYPE_SAFE_ENABLE_PRECONDITION_CHECKS ON)
+set(OPENVIC_TYPE_SAFE_ARITHMETIC_POLICY
+    ""
+    CACHE STRING
+    "type_safe arithmetic policy: ub, checked, or default (empty = auto: checked for dev builds, ub otherwise)"
+)
+set_property(CACHE OPENVIC_TYPE_SAFE_ARITHMETIC_POLICY PROPERTY STRINGS ";ub;checked;default")
+if(OPENVIC_TYPE_SAFE_ARITHMETIC_POLICY STREQUAL "")
+    if(GODOTCPP_DEV_BUILD)
+        set(TYPE_SAFE_ARITHMETIC_POLICY "checked")
+    else()
+        set(TYPE_SAFE_ARITHMETIC_POLICY "ub")
+    endif()
+elseif(OPENVIC_TYPE_SAFE_ARITHMETIC_POLICY MATCHES "^(ub|checked|default)$")
+    set(TYPE_SAFE_ARITHMETIC_POLICY "${OPENVIC_TYPE_SAFE_ARITHMETIC_POLICY}")
+else()
+    message(FATAL_ERROR "OPENVIC_TYPE_SAFE_ARITHMETIC_POLICY must be ub, checked, default, or empty")
+endif()
+openvic_declare_dep(type_safe
+    REPO foonathan/type_safe
+    SHA 292e8c127037e33d92f8f52dab0f1184993942d0
+    SHA256 622337e530b5812fd65ef91bc6df580946b2ac8b0ba865253d1dad80394e88df
+)
+FetchContent_MakeAvailable(type_safe) # type_safe (+ vendored external/debug_assert)
+
+# --- foonathan/memory: hand-rolled. Upstream's CMake bakes container node
+# sizes at configure time via try_compile (wrong for macOS universal builds);
+# instead we fetch source-only and generate the config and node-size headers
+# into the fetched tree ourselves (see OpenVicCodegen.cmake). ---
+openvic_declare_dep(memory
+    REPO foonathan/memory
+    SHA 92a5bfc53b97955b3d685af679636e87d0f5d15a
+    SHA256 8601f99ec73a2f84a16447f3e320b76c62227530217c6bf2fff44aece67d110e
+    DOWNLOAD_ONLY
+)
+FetchContent_MakeAvailable(memory)
+
+openvic_generate_memory_headers(
+    MEMORY_DIR ${memory_SOURCE_DIR}
+    DEFINES
+        FOONATHAN_MEMORY_CHECK_ALLOCATION_SIZE=1
+        FOONATHAN_MEMORY_IMPL_DEFAULT_ALLOCATOR=heap_allocator
+        FOONATHAN_MEMORY_DEBUG_ASSERT=0
+        FOONATHAN_MEMORY_DEBUG_FILL=0
+        FOONATHAN_MEMORY_DEBUG_FENCE=0
+        FOONATHAN_MEMORY_DEBUG_LEAK_CHECK=0
+        FOONATHAN_MEMORY_DEBUG_POINTER_CHECK=0
+        FOONATHAN_MEMORY_DEBUG_DOUBLE_DEALLOC_CHECK=0
+        FOONATHAN_MEMORY_EXTERN_TEMPLATE=1
+        FOONATHAN_MEMORY_TEMPORARY_STACK_MODE=2
+)
+
+file(GLOB_RECURSE ov_memory_sources CONFIGURE_DEPENDS ${memory_SOURCE_DIR}/src/*.cpp)
+add_library(ov_foonathan_memory STATIC ${ov_memory_sources})
+target_include_directories(
+    ov_foonathan_memory
+    PRIVATE ${memory_SOURCE_DIR}/src ${memory_SOURCE_DIR}/include/foonathan/memory
+)
+target_include_directories(ov_foonathan_memory SYSTEM PUBLIC ${memory_SOURCE_DIR}/include)
+target_compile_definitions(
+    ov_foonathan_memory
+    PUBLIC
+        FOONATHAN_MEMORY=1
+        FOONATHAN_MEMORY_VERSION_MAJOR=0
+        FOONATHAN_MEMORY_VERSION_MINOR=7
+        FOONATHAN_MEMORY_VERSION_PATCH=4
+)
+
+# --- spdlog (external fmt comes from openvic-dataloader's vendored copy) ---
+set(OPENVIC_SPDLOG_LEVEL
+    ""
+    CACHE STRING
+    "Compile-time spdlog level: trace, debug, info, warn, error, critical, or off (empty = auto: debug for dev builds, warn for template_release, info otherwise)"
+)
+set_property(CACHE OPENVIC_SPDLOG_LEVEL PROPERTY STRINGS ";trace;debug;info;warn;error;critical;off")
+if(NOT OPENVIC_SPDLOG_LEVEL STREQUAL "")
+    string(TOUPPER "${OPENVIC_SPDLOG_LEVEL}" _ov_spdlog_upper)
+    if(NOT _ov_spdlog_upper MATCHES "^(TRACE|DEBUG|INFO|WARN|ERROR|CRITICAL|OFF)$")
+        message(FATAL_ERROR "OPENVIC_SPDLOG_LEVEL must be trace, debug, info, warn, error, critical, off, or empty")
+    endif()
+    set(OV_SPDLOG_LEVEL "SPDLOG_LEVEL_${_ov_spdlog_upper}")
+elseif(GODOTCPP_DEV_BUILD)
+    set(OV_SPDLOG_LEVEL "SPDLOG_LEVEL_DEBUG")
+elseif(GODOTCPP_TARGET STREQUAL "template_release")
+    set(OV_SPDLOG_LEVEL "SPDLOG_LEVEL_WARN")
+else()
+    set(OV_SPDLOG_LEVEL "SPDLOG_LEVEL_INFO")
+endif()
+
+set(SPDLOG_FMT_EXTERNAL ON)
+set(SPDLOG_NO_EXCEPTIONS ON)
+set(SPDLOG_INSTALL OFF)
+set(SPDLOG_BUILD_EXAMPLE OFF)
+if(CMAKE_SYSTEM_NAME STREQUAL "Darwin")
+    set(SPDLOG_FWRITE_UNLOCKED OFF)
+endif()
+openvic_declare_dep(spdlog
+    REPO gabime/spdlog
+    SHA f1d748e5e3edfa4b1778edea003bac94781bc7b7
+    SHA256 0804ca200b3bb4ac5cfee7fbb9ca42069b4dbf21f5f1869465b4a5bf10b90ef4
+)
+FetchContent_MakeAvailable(spdlog) # spdlog::spdlog
+# Compile-time log level filter; not an upstream option, so applied directly.
+target_compile_definitions(spdlog PUBLIC SPDLOG_ACTIVE_LEVEL=${OV_SPDLOG_LEVEL})
diff --git a/deps/SCsub b/deps/SCsub
deleted file mode 100644
index 44677de4f..000000000
--- a/deps/SCsub
+++ /dev/null
@@ -1,291 +0,0 @@
-#!/usr/bin/env python
-
-Import("env")
-
-
-def build_openvic_dataloader(env):
-    ovdl_env = SConscript("openvic-dataloader/SConstruct")
-    env.AppendUnique(CPPDEFINES=ovdl_env["CPPDEFINES"])
-    env.Append(LIBPATH=ovdl_env.openvic_dataloader["LIBPATH"])
-    env.Prepend(LIBS=ovdl_env.openvic_dataloader["LIBS"])
-    env.Append(CPPPATH=ovdl_env.openvic_dataloader["INCPATH"])
-    env.openvic_dataloader = ovdl_env.openvic_dataloader
-    env.exposed_includes += env.openvic_dataloader["INCPATH"]
-
-
-def build_lexy_vdf(env):
-    lvdf_env = SConscript("lexy-vdf/SConstruct")
-    env.AppendUnique(CPPDEFINES=lvdf_env["CPPDEFINES"])
-    env.Append(LIBPATH=lvdf_env.lexy_vdf["LIBPATH"])
-    env.Prepend(LIBS=lvdf_env.lexy_vdf["LIBS"])
-    env.Append(CPPPATH=lvdf_env.lexy_vdf["INCPATH"])
-    env.lexy_vdf = lvdf_env.lexy_vdf
-    env.exposed_includes += env.lexy_vdf["INCPATH"]
-
-
-def build_ordered_map(env):
-    include_path = "ordered-map/include"
-    env.ordered_map = {}
-    env.ordered_map["INCPATH"] = [env.Dir(include_path)]
-    env.Append(CPPPATH=env.ordered_map["INCPATH"])
-    env.exposed_includes += env.ordered_map["INCPATH"]
-
-
-def build_colony(env):
-    include_path = "plf_colony"
-    env.colony = {}
-    env.colony["INCPATH"] = [env.Dir(include_path)]
-    env.Append(CPPPATH=env.colony["INCPATH"])
-    env.exposed_includes += env.colony["INCPATH"]
-
-
-def build_function2(env):
-    include_path = "function2/include"
-    env.function2 = {}
-    env.function2["INCPATH"] = [env.Dir(include_path)]
-    env.Append(CPPPATH=env.function2["INCPATH"])
-    env.exposed_includes += env.function2["INCPATH"]
-
-
-def build_std_function(env):
-    include_path = "std_function"
-    env.std_function = {}
-    env.std_function["INCPATH"] = [env.Dir(include_path)]
-    env.Append(CPPPATH=env.std_function["INCPATH"])
-    env.exposed_includes += env.std_function["INCPATH"]
-
-
-def build_memory(env):
-    from pathlib import Path
-
-    import methods
-
-    include_path = "memory/include"
-    inner_include_path = include_path + "/foonathan/memory"
-    source_path = "memory/src"
-
-    env.Append(
-        CPPDEFINES=[
-            ("FOONATHAN_MEMORY", 1),
-            ("FOONATHAN_MEMORY_VERSION_MAJOR", 0),
-            ("FOONATHAN_MEMORY_VERSION_MINOR", 7),
-            ("FOONATHAN_MEMORY_VERSION_PATCH", 4),
-        ]
-    )
-
-    memory_env = env.Clone()
-    memory_env.Append(
-        BUILDERS={
-            "GenerateMemoryConfig": memory_env.Builder(action=methods.generate_memory_config_header),
-            "GenerateMemoryContainerNodeSizes": memory_env.Builder(
-                action=methods.generate_memory_container_size_header
-            ),
-        }
-    )
-
-    memory_env.config_data = {
-        "FOONATHAN_MEMORY_CHECK_ALLOCATION_SIZE": 1,
-        "FOONATHAN_MEMORY_IMPL_DEFAULT_ALLOCATOR": "heap_allocator",
-        "FOONATHAN_MEMORY_DEBUG_ASSERT": 0,
-        "FOONATHAN_MEMORY_DEBUG_FILL": 0,
-        "FOONATHAN_MEMORY_DEBUG_FENCE": 0,
-        "FOONATHAN_MEMORY_DEBUG_LEAK_CHECK": 0,
-        "FOONATHAN_MEMORY_DEBUG_POINTER_CHECK": 0,
-        "FOONATHAN_MEMORY_DEBUG_DOUBLE_DEALLOC_CHECK": 0,
-        "FOONATHAN_MEMORY_EXTERN_TEMPLATE": 1,
-        "FOONATHAN_MEMORY_TEMPORARY_STACK_MODE": 2,
-    }
-
-    config = memory_env.GenerateMemoryConfig(memory_env.File(Path(inner_include_path) / "config_impl.hpp"), "SCsub")
-    node_size_impl = memory_env.GenerateMemoryContainerNodeSizes(
-        memory_env.File(Path(inner_include_path) / "detail" / "container_node_sizes_impl.hpp"), "SCsub"
-    )
-
-    if memory_env.get("is_msvc", False):
-        if memory_env.get("use_clang_cl"):
-            memory_env.Append(
-                CXXFLAGS=["-pedantic-errors", "-Werror", "-Wall", "-Wextra", "-Wconversion", "-Wsign-conversion"]
-            )
-        else:
-            memory_env.Append(CXXFLAGS=["/WX", "/W3", "/D", "_CRT_SECURE_NO_WARNINGS"])
-    else:
-        memory_env.Append(
-            CXXFLAGS=["-pedantic-errors", "-Werror", "-Wall", "-Wextra", "-Wconversion", "-Wsign-conversion"]
-        )
-    memory_env.Append(CPPPATH=[[memory_env.Dir(p) for p in [source_path, Path(inner_include_path)]]])
-    # Mirror memory's source tree into the per-config build dir
-    build_root = (env.get("build_dir") or env.Dir("#").abspath).replace("\\", "/")
-    out_dir = build_root + "/memory/" + source_path
-    memory_env.VariantDir(out_dir, source_path, duplicate=True)
-    sources = env.GlobRecursive("*.cpp", [out_dir])
-    env.memory_sources = sources
-
-    library_name = "libmemory" + env.get("suffix", "") + env["LIBSUFFIX"]
-    library = memory_env.StaticLibrary(target=out_dir + "/" + library_name, source=sources)
-    Default([config, node_size_impl, library])
-
-    env.memory = {}
-    env.memory["INCPATH"] = [memory_env.Dir(include_path)]
-
-    env.Append(CPPPATH=env.memory["INCPATH"])
-    env.Append(LIBPATH=[memory_env.Dir(out_dir)])
-    env.Prepend(LIBS=[library_name])
-
-    env.exposed_includes += env.memory["INCPATH"]
-
-
-def build_spdlog(env):
-    import os
-
-    spdlog_level = "SPDLOG_LEVEL_INFO"
-    if "SPDLOG_LEVEL" in os.environ:
-        spdlog_level = os.environ["SPDLOG_LEVEL"].upper()
-        if not spdlog_level.startswith("SPDLOG_LEVEL_"):
-            spdlog_level = "SPDLOG_LEVEL_" + spdlog_level
-        if spdlog_level not in [
-            "SPDLOG_LEVEL_TRACE",
-            "SPDLOG_LEVEL_DEBUG",
-            "SPDLOG_LEVEL_INFO",
-            "SPDLOG_LEVEL_WARN",
-            "SPDLOG_LEVEL_ERROR",
-            "SPDLOG_LEVEL_CRITICAL",
-            "SPDLOG_LEVEL_OFF",
-        ]:
-            print("SPDLOG_LEVEL can only be trace, debug, info, warn, error, critical, or off")
-            exit(255)
-    elif env["dev_build"]:
-        spdlog_level = "SPDLOG_LEVEL_DEBUG"
-    elif env["target"] == "template_release" or env["optimize"] in ["speed", "size"]:
-        spdlog_level = "SPDLOG_LEVEL_WARN"
-
-    env.Append(
-        CPPDEFINES=[
-            ("SPDLOG_ACTIVE_LEVEL", spdlog_level),
-            "SPDLOG_FMT_EXTERNAL",
-            "SPDLOG_COMPILED_LIB",
-            "SPDLOG_NO_EXCEPTIONS",
-        ]
-    )
-    if env["platform"] != "macos":
-        env.Append(CPPDEFINES=["SPDLOG_FWRITE_UNLOCKED"])
-    spdlog_env = env.Clone()
-
-    include_path = "spdlog/include"
-    source_path = "spdlog/src"
-    source_dir = spdlog_env.Dir(source_path)
-
-    env.spdlog = {}
-    env.spdlog["INCPATH"] = [spdlog_env.Dir(include_path)]
-
-    spdlog_env.Append(CPPPATH=[source_dir, env.spdlog["INCPATH"]])
-    # Mirror spdlog's source tree into the per-config build dir.
-    build_root = (env.get("build_dir") or env.Dir("#").abspath).replace("\\", "/")
-    spdlog_out = build_root + "/spdlog/" + source_path
-    spdlog_env.VariantDir(spdlog_out, source_path, duplicate=True)
-    sources = env.GlobRecursive("*.cpp", [spdlog_out])
-    env.spdlog_sources = sources
-
-    library_name = "libspdlog" + env.get("suffix", "") + env["LIBSUFFIX"]
-    library = spdlog_env.StaticLibrary(target=spdlog_out + "/" + library_name, source=sources)
-    Default(library)
-
-    env.Append(CPPPATH=[env.spdlog["INCPATH"]])
-    if env.get("is_msvc", False):
-        env.Append(CXXFLAGS=[f"/external:I{env.spdlog['INCPATH'][0]}"])
-    else:
-        env.Append(CXXFLAGS=["-isystem", env.spdlog["INCPATH"][0]])
-    env.Append(LIBPATH=[spdlog_env.Dir(spdlog_out)])
-    env.Prepend(LIBS=[library_name])
-
-    env.exposed_includes += env.spdlog["INCPATH"]
-
-
-def build_xoshiro(env):
-    include_path = "xoshiro"
-    env.xoshiro = {}
-    env.xoshiro["INCPATH"] = [env.Dir(include_path)]
-    env.Append(CPPPATH=env.xoshiro["INCPATH"])
-    env.exposed_includes += env.xoshiro["INCPATH"]
-
-
-def build_type_safe(env):
-    import os
-
-    arithmetic_policy = 1
-    if "TYPE_SAFE_ARITHMETIC_POLICY" in os.environ:
-        arithmetic_policy = os.environ["TYPE_SAFE_ARITHMETIC_POLICY"].upper().replace(" ", "_")
-        if arithmetic_policy not in [
-            "ub",
-            "undefined_behavior",
-            "undefined",
-            "checked",
-            "default",
-            "0",
-            "1",
-            "2",
-        ]:
-            print("TYPE_SAFE_ARITHMETIC_POLICY can only be undefined, checked, or default")
-            exit(255)
-        match arithmetic_policy:
-            case "default":
-                arithmetic_policy = 0
-            case "ub", "undefined_behavior", "undefined":
-                arithmetic_policy = 1
-            case "checked":
-                arithmetic_policy = 2
-            case "0", "1", "2":
-                arithmetic_policy = int(arithmetic_policy)
-    elif env["dev_build"]:
-        arithmetic_policy = 2
-
-    env.Append(
-        CPPDEFINES=[
-            ("TYPE_SAFE_ENABLE_ASSERTIONS", 1 if env["target"] == "template_release" else 0),
-            ("TYPE_SAFE_ENABLE_PRECONDITION_CHECKS", 1),
-            ("TYPE_SAFE_ENABLE_WRAPPER", 1 if env["target"] == "template_release" else 0),
-            ("TYPE_SAFE_ARITHMETIC_POLICY", arithmetic_policy),
-        ]
-    )
-
-    include_path = "type_safe/include"
-    debug_assert_include_path = "type_safe/external/debug_assert"
-    env.type_safe = {}
-    env.type_safe["INCPATH"] = [env.Dir(include_path), env.Dir(debug_assert_include_path)]
-    env.Append(CPPPATH=[env.type_safe["INCPATH"]])
-    if env.get("is_msvc", False):
-        env.Append(CXXFLAGS=[f"/external:I{env.type_safe['INCPATH'][0]}"])
-    else:
-        env.Append(CXXFLAGS=["-isystem", env.type_safe["INCPATH"][0]])
-    env.exposed_includes += env.type_safe["INCPATH"]
-
-
-def build_int128(env):
-    include_path = "int128/include"
-    env.int128 = {}
-    env.int128["INCPATH"] = [env.Dir(include_path)]
-    env.Append(CPPPATH=env.int128["INCPATH"])
-    env.exposed_includes += env.int128["INCPATH"]
-
-
-def link_tbb(env):
-    import sys
-
-    if not env.get("is_msvc", False) and not env.get("use_mingw", False) and sys.platform != "darwin":
-        env.ParseConfig("pkg-config tbb --cflags --libs")
-
-
-if env.get("is_msvc", False):
-    env.AppendUnique(CXXFLAGS=["/external:W0"])
-
-build_openvic_dataloader(env)
-build_lexy_vdf(env)
-build_ordered_map(env)
-build_colony(env)
-build_function2(env)
-build_std_function(env)
-build_memory(env)
-build_spdlog(env)
-build_xoshiro(env)
-build_type_safe(env)
-build_int128(env)
-link_tbb(env)
diff --git a/deps/function2 b/deps/function2
deleted file mode 160000
index 43fc0ca47..000000000
--- a/deps/function2
+++ /dev/null
@@ -1 +0,0 @@
-Subproject commit 43fc0ca473ecb081918709bd7d524d84c2ff8dce
diff --git a/deps/int128 b/deps/int128
deleted file mode 160000
index ea0f85e69..000000000
--- a/deps/int128
+++ /dev/null
@@ -1 +0,0 @@
-Subproject commit ea0f85e693767ae667e26d66022d6ed68d3425de
diff --git a/deps/lexy-vdf b/deps/lexy-vdf
index c6d02d1a8..821aa7ea4 160000
--- a/deps/lexy-vdf
+++ b/deps/lexy-vdf
@@ -1 +1 @@
-Subproject commit c6d02d1a8d982e3f05552492ac075415d00bd565
+Subproject commit 821aa7ea4abd79591abe6bb80e4c820990ab2d74
diff --git a/deps/memory b/deps/memory
deleted file mode 160000
index 92a5bfc53..000000000
--- a/deps/memory
+++ /dev/null
@@ -1 +0,0 @@
-Subproject commit 92a5bfc53b97955b3d685af679636e87d0f5d15a
diff --git a/deps/openvic-dataloader b/deps/openvic-dataloader
index 698b47c8f..4a9cc55c8 160000
--- a/deps/openvic-dataloader
+++ b/deps/openvic-dataloader
@@ -1 +1 @@
-Subproject commit 698b47c8f93611af55d0eff664337802cb91c286
+Subproject commit 4a9cc55c855092bb49f6c74f54e7dbe46f03f214
diff --git a/deps/ordered-map b/deps/ordered-map
deleted file mode 160000
index bd8d5ef41..000000000
--- a/deps/ordered-map
+++ /dev/null
@@ -1 +0,0 @@
-Subproject commit bd8d5ef4149cd40783a486011778a2e7eedde441
diff --git a/deps/plf_colony b/deps/plf_colony
deleted file mode 160000
index fbc8483fb..000000000
--- a/deps/plf_colony
+++ /dev/null
@@ -1 +0,0 @@
-Subproject commit fbc8483fb785c431dc4410396815abf886728c40
diff --git a/deps/spdlog b/deps/spdlog
deleted file mode 160000
index f1d748e5e..000000000
--- a/deps/spdlog
+++ /dev/null
@@ -1 +0,0 @@
-Subproject commit f1d748e5e3edfa4b1778edea003bac94781bc7b7
diff --git a/deps/std_function/func b/deps/std_function/func
deleted file mode 160000
index dbf2df963..000000000
--- a/deps/std_function/func
+++ /dev/null
@@ -1 +0,0 @@
-Subproject commit dbf2df963e6d0a47ab5e21e93c838233cd8b6b03
diff --git a/deps/type_safe b/deps/type_safe
deleted file mode 160000
index 292e8c127..000000000
--- a/deps/type_safe
+++ /dev/null
@@ -1 +0,0 @@
-Subproject commit 292e8c127037e33d92f8f52dab0f1184993942d0
diff --git a/deps/xoshiro b/deps/xoshiro
deleted file mode 160000
index 19bcbb2ce..000000000
--- a/deps/xoshiro
+++ /dev/null
@@ -1 +0,0 @@
-Subproject commit 19bcbb2ce0ed158233187f524fd0964c105a65b3
diff --git a/docs/ecs/determinism.md b/docs/ecs/determinism.md
index bbe12639c..a857060ec 100644
--- a/docs/ecs/determinism.md
+++ b/docs/ecs/determinism.md
@@ -338,7 +338,7 @@ The existing gates, each guarding a different determinism risk:
 | `tests/src/ecs/Checksum.cpp` | Full-state checksum determinism, sensitivity, and the checksum-based worker-count gate above. |
 | `tests/src/ecs/IdentitySnapshotInvariance.cpp` | Save/load: `digest(tick^k(restore(snapshot(s)))) == digest(tick^k(s))` at every worker count, including exact `EntityID` reuse from the restored free list. |
 
-Run them with `scons run_ovsim_tests=yes`. When you add game systems, extend the gate: a worker-count sweep over a realistic scenario, digested with `world_checksum`, is cheap to write and catches the whole class of races and order-dependencies that code review misses. Remember the gate's limits, though — it catches scheduling races only probabilistically. The declarations (rule 3) and the type-level checksum enforcement are what make determinism hold by construction; the gate is the alarm, not the lock.
+Run them with `ctest --preset <preset>-debug` (after building with `cmake --build --preset <preset>-debug`). When you add game systems, extend the gate: a worker-count sweep over a realistic scenario, digested with `world_checksum`, is cheap to write and catches the whole class of races and order-dependencies that code review misses. Remember the gate's limits, though — it catches scheduling races only probabilistically. The declarations (rule 3) and the type-level checksum enforcement are what make determinism hold by construction; the gate is the alarm, not the lock.
 
 ## Cross-references
 
diff --git a/pyproject.toml b/pyproject.toml
index 7e60f8d31..c29baeb92 100644
--- a/pyproject.toml
+++ b/pyproject.toml
@@ -12,7 +12,6 @@ explicit_package_bases = true
 python_version = "3.10"
 
 [tool.ruff]
-extend-include = ["SConstruct", "SCsub"]
 line-length = 120
 target-version = "py310"
 
@@ -21,12 +20,6 @@ extend-select = [
 	"I", # isort
 ]
 
-[tool.ruff.lint.per-file-ignores]
-"{SConstruct,SCsub}" = [
-	"E402", # Module level import not at top of file
-	"F821", # Undefined name
-]
-
 [tool.typos]
 files.extend-exclude = [".mailmap", "*.gitignore", "*.po", "*.pot", "*.rc"]
 default.extend-ignore-re = [
diff --git a/scripts b/scripts
deleted file mode 160000
index 9f8318fb9..000000000
--- a/scripts
+++ /dev/null
@@ -1 +0,0 @@
-Subproject commit 9f8318fb9f34fc49709ad8227577438e488dc88b
diff --git a/tests/CMakeLists.txt b/tests/CMakeLists.txt
new file mode 100644
index 000000000..0df122ed8
--- /dev/null
+++ b/tests/CMakeLists.txt
@@ -0,0 +1,39 @@
+# snitch honors these as plain variables (CMP0077 NEW via
+# openvic_setup_base_flags). Only deltas from upstream defaults are set;
+# everything else (SNITCH_DEFINE_MAIN, timings, reporters, ...) already
+# matches upstream. Exceptions are additionally forced off at compile time by
+# snitch itself when _HAS_EXCEPTIONS=0 / -fno-exceptions is in effect.
+#
+# NOTE: openvic-dataloader pins an older snitch under the same FetchContent
+# name; first declaration wins. The two test suites are never enabled in one
+# build by default (tests default ON only for the top-level project), so in
+# practice each repo's own pin applies.
+set(SNITCH_WITH_EXCEPTIONS OFF)
+set(SNITCH_WITH_MULTITHREADING OFF)
+openvic_declare_dep(snitch
+    REPO snitch-org/snitch
+    SHA 2a594333016b9cfc1f90b116184cb0bdbf98a480 # v1.3.1
+    SHA256 7512cd7ec34d02faea463e7588ccbcfddc229a7e2243dd49e421b7158741c103
+)
+FetchContent_MakeAvailable(snitch)
+
+if(OPENVIC_SIM_BUILD_TESTS)
+    file(GLOB_RECURSE ovsim_tests_sources CONFIGURE_DEPENDS src/*.cpp)
+
+    add_executable(openvic-simulation-tests ${ovsim_tests_sources})
+    target_compile_definitions(openvic-simulation-tests PRIVATE OPENVIC_SIMULATION_TESTS)
+    target_include_directories(openvic-simulation-tests PRIVATE src)
+    target_link_libraries(openvic-simulation-tests PRIVATE openvic::simulation snitch::snitch)
+    set_target_properties(
+        openvic-simulation-tests
+        PROPERTIES
+            OUTPUT_NAME "openvic-simulation.tests"
+            RUNTIME_OUTPUT_DIRECTORY "${CMAKE_BINARY_DIR}/bin/$<CONFIG>"
+    )
+
+    add_test(NAME openvic-simulation-tests COMMAND openvic-simulation-tests)
+endif()
+
+if(OPENVIC_SIM_BUILD_BENCHMARKS)
+    add_subdirectory(benchmarks)
+endif()
diff --git a/tests/SCsub b/tests/SCsub
deleted file mode 100644
index 201d98193..000000000
--- a/tests/SCsub
+++ /dev/null
@@ -1,64 +0,0 @@
-#!/usr/bin/env python
-import os
-import subprocess
-
-from SCons.Script.SConscript import SConsEnvironment
-
-
-def UnitTestPostAction(target=None, source=None, env=None):
-    print()
-    return subprocess.run([source[0].path]).returncode
-
-
-def BuildUnitTest(env, **kwargs):
-    test = env.Program(**kwargs)
-    env.NoCache(test)
-    return test
-
-
-def RunUnitTest(env):
-    # Alias-based runner: AlwaysBuild on the alias makes the run-action fire on
-    # every invocation, but the unit-test Program target underneath is only
-    # re-linked when its sources/lib deps actually changed.
-    run_alias = env.Alias("run_tests", env.unit_test, env.Action(UnitTestPostAction, None))
-    env.AlwaysBuild(run_alias)
-    Default(run_alias)
-
-
-SConsEnvironment.BuildUnitTest = BuildUnitTest
-SConsEnvironment.RunUnitTest = RunUnitTest
-
-Import("env")
-
-BINDIR = "bin"
-
-env.openvic_simulation_tests = {}
-
-# For the reference:
-# - CCFLAGS are compilation flags shared between C and C++
-# - CFLAGS are for C-specific compilation flags
-# - CXXFLAGS are for C++-specific compilation flags
-# - CPPFLAGS are for pre-processor flags
-# - CPPDEFINES are for pre-processor defines
-# - LINKFLAGS are for linking flags
-
-# tweak this if you want to use different folders, or more folders, to store your source code in.
-source_path = "src"  # relative to this SCsub (i.e. tests/src under the sim root)
-
-tests_name = "openvic-simulation"
-tests_env = env.Clone()
-# Out-of-source build: variant tree holds object files only.
-tests_variant = env["build_dir"] + "/tests/" + source_path
-tests_env.VariantDir(tests_variant, source_path, duplicate=False)
-tests_env.Append(CPPDEFINES=["OPENVIC_SIMULATION_TESTS"])
-tests_env.Append(CPPPATH=[tests_env.Dir(tests_variant), tests_env.Dir(source_path)])
-tests_env.tests_sources = env.GlobRecursiveVariant("*.cpp", source_path, tests_variant)
-
-SConscript("deps/SCsub", {"env": tests_env})
-
-tests_env.unit_test = tests_env.BuildUnitTest(
-    source=tests_env.tests_sources, target=os.path.join(BINDIR, tests_name), PROGSUFFIX=".tests" + env["PROGSUFFIX"]
-)
-Default(tests_env.unit_test)
-
-Return("tests_env")
diff --git a/tests/benchmarks/CMakeLists.txt b/tests/benchmarks/CMakeLists.txt
new file mode 100644
index 000000000..9db6e3cc3
--- /dev/null
+++ b/tests/benchmarks/CMakeLists.txt
@@ -0,0 +1,30 @@
+# nanobench (the OpenVic fork with exceptions/RTTI disabled): its own
+# CMakeLists only supports standalone builds, so fetch source-only and
+# hand-roll a static library from the single implementation TU.
+openvic_declare_dep(nanobench
+    REPO Spartan322/nanobench
+    SHA f49b052a68bc05d5497acf1f6c0c2d6ef65c6eef # disable/exceptions-and-rtti
+    SHA256 513c81406c5dbc8e1840efbef41a924c7376d0538550bddde9bdf9ee5c62f320
+    DOWNLOAD_ONLY
+)
+FetchContent_MakeAvailable(nanobench)
+add_library(ov_nanobench STATIC ${nanobench_SOURCE_DIR}/src/test/app/nanobench.cpp)
+target_include_directories(ov_nanobench SYSTEM PUBLIC ${nanobench_SOURCE_DIR}/src/include)
+
+file(GLOB_RECURSE ovsim_benchmarks_sources CONFIGURE_DEPENDS src/*.cpp)
+
+# Benchmarks use snitch's macros too; snitch comes from the parent
+# tests/CMakeLists.txt, which always includes this directory.
+add_executable(openvic-simulation-benchmarks ${ovsim_benchmarks_sources})
+target_compile_definitions(openvic-simulation-benchmarks PRIVATE OPENVIC_SIMULATION_BENCHMARKS)
+target_include_directories(openvic-simulation-benchmarks PRIVATE src)
+target_link_libraries(openvic-simulation-benchmarks PRIVATE openvic::simulation snitch::snitch ov_nanobench)
+set_target_properties(
+    openvic-simulation-benchmarks
+    PROPERTIES
+        OUTPUT_NAME "openvic-simulation.benchmarks"
+        RUNTIME_OUTPUT_DIRECTORY "${CMAKE_BINARY_DIR}/bin/$<CONFIG>"
+)
+# No add_test: benchmarks are built in CI for compile coverage but run
+# manually (they take real time and their numbers are meaningless on shared
+# runners).
diff --git a/tests/benchmarks/SCsub b/tests/benchmarks/SCsub
deleted file mode 100644
index 66e1fdf76..000000000
--- a/tests/benchmarks/SCsub
+++ /dev/null
@@ -1,67 +0,0 @@
-#!/usr/bin/env python
-import os
-import subprocess
-
-from SCons.Script.SConscript import SConsEnvironment
-
-
-def BenchmarkPostAction(target=None, source=None, env=None):
-    print()
-    return subprocess.run([source[0].path]).returncode
-
-
-def BuildBenchmarks(env, **kwargs):
-    benchmark = env.Program(**kwargs)
-    env.NoCache(benchmark)
-    return benchmark
-
-
-def RunBenchmarks(env):
-    # Alias-based runner: AlwaysBuild on the alias makes the run-action fire on
-    # every invocation, but the benchmark Program target underneath is only
-    # re-linked when its sources/lib deps actually changed.
-    run_alias = env.Alias("run_benchmarks", env.benchmark, env.Action(BenchmarkPostAction, None))
-    env.AlwaysBuild(run_alias)
-    Default(run_alias)
-
-
-SConsEnvironment.BuildBenchmarks = BuildBenchmarks
-SConsEnvironment.RunBenchmarks = RunBenchmarks
-
-Import("env")
-
-BINDIR = "bin"
-
-env.openvic_simulation_benchmarks = {}
-
-# For the reference:
-# - CCFLAGS are compilation flags shared between C and C++
-# - CFLAGS are for C-specific compilation flags
-# - CXXFLAGS are for C++-specific compilation flags
-# - CPPFLAGS are for pre-processor flags
-# - CPPDEFINES are for pre-processor defines
-# - LINKFLAGS are for linking flags
-
-# tweak this if you want to use different folders, or more folders, to store your source code in.
-source_path = "src"  # relative to this SCsub (i.e. tests/benchmarks/src under the sim root)
-
-benchmarks_name = "openvic-simulation"
-benchmarks_env = env.Clone()
-# Out-of-source build: variant tree holds object files only.
-benchmarks_variant = env["build_dir"] + "/tests/benchmarks/" + source_path
-benchmarks_env.VariantDir(benchmarks_variant, source_path, duplicate=False)
-benchmarks_env.Append(CPPDEFINES=["OPENVIC_SIMULATION_BENCHMARKS"])
-
-benchmarks_env.Append(CPPPATH=[benchmarks_env.Dir(benchmarks_variant), benchmarks_env.Dir(source_path)])
-benchmarks_env.benchmarks_sources = env.GlobRecursiveVariant("*.cpp", source_path, benchmarks_variant)
-
-SConscript("deps/SCsub", {"env": benchmarks_env, "parent_env": env})
-
-benchmarks_env.benchmark = benchmarks_env.BuildBenchmarks(
-    source=benchmarks_env.benchmarks_sources,
-    target=os.path.join(BINDIR, benchmarks_name),
-    PROGSUFFIX=".benchmarks" + env["PROGSUFFIX"],
-)
-Default(benchmarks_env.benchmark)
-
-Return("benchmarks_env")
diff --git a/tests/benchmarks/deps/SCsub b/tests/benchmarks/deps/SCsub
deleted file mode 100644
index a93a3e90b..000000000
--- a/tests/benchmarks/deps/SCsub
+++ /dev/null
@@ -1,40 +0,0 @@
-#!/usr/bin/env python
-import os
-
-Import("env")
-Import("parent_env")
-
-
-def build_nanobench(env):
-    nanobench_env = env.Clone()
-
-    include_path = "nanobench/src/include"
-    source_path = "nanobench/src/test/app"
-    # Out-of-source build: variant tree holds object files and the static library,
-    # so the nanobench submodule working tree stays clean.
-    nanobench_variant = env["build_dir"] + "/tests/benchmarks/deps/nanobench"
-    variant_source_path = nanobench_variant + "/src/test/app"
-    nanobench_env.VariantDir(nanobench_variant, "nanobench", duplicate=False)
-
-    nanobench_env.Append(CPPPATH=[nanobench_env.Dir(include_path)])
-    sources = nanobench_env.GlobRecursiveVariant("nanobench.cpp", source_path, variant_source_path)
-
-    library_name = "libnanobench" + env["LIBSUFFIX"]
-    library = nanobench_env.StaticLibrary(target=os.path.join(nanobench_variant, library_name), source=sources)
-    Default(library)
-
-    env.Append(CPPPATH=[nanobench_env.Dir(include_path)])
-    env.Append(LIBPATH=[nanobench_env.Dir(nanobench_variant)])
-    env.Prepend(LIBS=[library_name])
-
-
-def build_snitch(env, parent_env):
-    try:
-        parent_env.snitch_env
-        SConscript("../../deps/SCsub", {"env": parent_env})
-    except AttributeError:
-        SConscript("../../deps/SCsub", {"env": env})
-
-
-build_nanobench(env)
-build_snitch(env, parent_env)
diff --git a/tests/benchmarks/deps/nanobench b/tests/benchmarks/deps/nanobench
deleted file mode 160000
index f49b052a6..000000000
--- a/tests/benchmarks/deps/nanobench
+++ /dev/null
@@ -1 +0,0 @@
-Subproject commit f49b052a68bc05d5497acf1f6c0c2d6ef65c6eef
diff --git a/tests/deps/SCsub b/tests/deps/SCsub
deleted file mode 100644
index 67f2797da..000000000
--- a/tests/deps/SCsub
+++ /dev/null
@@ -1,217 +0,0 @@
-#!/usr/bin/env python
-import subprocess
-from pathlib import Path
-
-Import("env")
-
-
-def generate_snitch_config_header(target, source, env):
-    header = []
-
-    header.append("// THIS FILE IS GENERATED. EDITS WILL BE LOST.")
-    header.append("")
-
-    header_file_path = Path(str(target[0]))
-
-    include_gen_folder = Path(str(header_file_path.parent))
-    include_gen_folder.mkdir(parents=True, exist_ok=True)
-
-    header_guard = "SNITCH_CONFIG_HPP"
-    header.append(f"#ifndef {header_guard}")
-    header.append(f"#define {header_guard}")
-    header.append("")
-    header.append("#include <version> // for C++ feature check macros")
-
-    for key, val in env.config_data.items():
-        if val is True:
-            val = 1
-        elif val is False:
-            val = 0
-        header += (
-            f"""
-#if !defined({key})
-#   define {key} {val}
-#endif"""
-        ).split("\n")
-
-    header += """
-#if defined(_MSC_VER)
-#    if defined(_KERNEL_MODE) || (defined(_HAS_EXCEPTIONS) && !_HAS_EXCEPTIONS)
-#        define SNITCH_EXCEPTIONS_NOT_AVAILABLE
-#    endif
-#elif defined(__clang__) || defined(__GNUC__)
-#    if !defined(__EXCEPTIONS)
-#        define SNITCH_EXCEPTIONS_NOT_AVAILABLE
-#    endif
-#endif
-
-#if defined(SNITCH_EXCEPTIONS_NOT_AVAILABLE)
-#    undef SNITCH_WITH_EXCEPTIONS
-#    define SNITCH_WITH_EXCEPTIONS 0
-#endif
-
-#if SNITCH_WITH_MULTITHREADING
-#    define SNITCH_THREAD_LOCAL thread_local
-#else
-#    define SNITCH_THREAD_LOCAL
-#endif
-
-#if !defined(__cpp_lib_bit_cast)
-#    undef SNITCH_CONSTEXPR_FLOAT_USE_BITCAST
-#    define SNITCH_CONSTEXPR_FLOAT_USE_BITCAST 0
-#endif
-
-#if (!defined(__cpp_lib_to_chars)) || (defined(_GLIBCXX_RELEASE) && _GLIBCXX_RELEASE <= 11) ||     \
-    (defined(_LIBCPP_VERSION) && _LIBCPP_VERSION <= 14000) ||                                      \
-    (defined(_MSC_VER) && _MSC_VER <= 1924)
-#    undef SNITCH_APPEND_TO_CHARS
-#    define SNITCH_APPEND_TO_CHARS 0
-#endif
-
-#if SNITCH_SHARED_LIBRARY
-#    if defined(_MSC_VER)
-#        if defined(SNITCH_EXPORTS)
-#            define SNITCH_EXPORT __declspec(dllexport)
-#        else
-#            define SNITCH_EXPORT __declspec(dllimport)
-#        endif
-#    elif defined(__clang__) || defined(__GNUC__)
-#        define SNITCH_EXPORT [[gnu::visibility("default")]]
-#    else
-#        define SNITCH_EXPORT
-#    endif
-#else
-#    define SNITCH_EXPORT
-#endif
-    """.split("\n")
-
-    header.append("")
-    header.append(f"#endif // {header_guard}")
-    header.append("")
-
-    with header_file_path.open("w+", encoding="utf-8") as header_file:
-        header_file.write("\n".join(header))
-
-
-def build_snitch(env):
-    major_version = 1
-    minor_version = 2
-    patch_version = 5
-    git_run = subprocess.run(["git", "describe", "--tags", "--abbrev=0"], text=True, cwd="snitch/", capture_output=True)
-    if git_run.returncode == 0:
-        tag = git_run.stdout.strip().removeprefix("v").split(".")
-        major_version = tag[0]
-        minor_version = tag[1]
-        patch_version = tag[2]
-
-    SHARED_BUILD = False
-    HEADER_ONLY = False
-    UNITY_BUILD = True
-
-    SNITCH_VERSION = "{}.{}.{}".format(major_version, minor_version, patch_version)
-
-    snitch_env = None
-    try:
-        snitch_env = env.snitch_env
-    except AttributeError:
-        snitch_env = env.Clone()
-
-    snitch_env.Append(BUILDERS={"GenerateSnitchConfig": snitch_env.Builder(action=generate_snitch_config_header)})
-
-    snitch_env.config_data = {
-        "SNITCH_ENABLE": True,  # Enable snitch at build time.
-        "SNITCH_MAX_TEST_CASES": 5000,  # "Maximum number of test cases in a test application."
-        "SNITCH_MAX_NESTED_SECTIONS": 8,  # "Maximum depth of nested sections in a test case."
-        "SNITCH_MAX_EXPR_LENGTH": 1024,  # "Maximum length of a printed expression when reporting failure."
-        "SNITCH_MAX_MESSAGE_LENGTH": 1024,  # "Maximum length of error or status messages."
-        "SNITCH_MAX_TEST_NAME_LENGTH": 1024,  # "Maximum length of a test case name."
-        "SNITCH_MAX_TAG_LENGTH": 256,  # "Maximum length of a test tag."
-        "SNITCH_MAX_CAPTURES": 8,  # "Maximum number of captured expressions in a test case."
-        "SNITCH_MAX_CAPTURE_LENGTH": 256,  # "Maximum length of a captured expression."
-        "SNITCH_MAX_UNIQUE_TAGS": 1024,  # "Maximum number of unique tags in a test application."
-        "SNITCH_MAX_COMMAND_LINE_ARGS": 1024,  # "Maximum number of command line arguments to a test application."
-        "SNITCH_MAX_REGISTERED_REPORTERS": 8,  # "Maximum number of registered reporter that can be selected from the command line."
-        "SNITCH_MAX_PATH_LENGTH": 1024,  # "Maximum length of a file path when writing output to file."
-        "SNITCH_MAX_REPORTER_SIZE_BYTES": 128,  # "Maximum size (in bytes) of a reporter object."
-        "SNITCH_DEFINE_MAIN": True,  # "Define main() in snitch -- disable to provide your own main() function."
-        "SNITCH_WITH_EXCEPTIONS": False,  # "Use exceptions in snitch implementation -- will be forced OFF if exceptions are not available."
-        "SNITCH_WITH_MULTITHREADING": False,  # "Make the testing framework thread-safe -- disable if multithreading is not needed."
-        "SNITCH_WITH_TIMINGS": True,  # "Measure the time taken by each test case -- disable to speed up tests."
-        "SNITCH_WITH_SHORTHAND_MACROS": True,  # "Use short names for test macros -- disable if this causes conflicts."
-        "SNITCH_CONSTEXPR_FLOAT_USE_BITCAST": True,  # "Use std::bit_cast if available to implement exact constexpr float-to-string conversion."
-        "SNITCH_APPEND_TO_CHARS": True,  # "Use std::to_chars for string conversions -- disable for greater compatibility with a slight performance cost."
-        "SNITCH_DEFAULT_WITH_COLOR": True,  # "Enable terminal colors by default -- can also be controlled by command line interface."
-        "SNITCH_DECOMPOSE_SUCCESSFUL_ASSERTIONS": False,  # "Enable expression decomposition even for successful assertions -- more expensive."
-        "SNITCH_WITH_ALL_REPORTERS": True,  # "Allow all built-in reporters to be selected from the command line -- disable for faster compilation."
-        "SNITCH_WITH_TEAMCITY_REPORTER": False,  # "Allow the TeamCity reporter to be selected from the command line -- enable if needed."
-        "SNITCH_WITH_CATCH2_XML_REPORTER": False,  # "Allow the Catch2 XML reporter to be selected from the command line -- enable if needed."
-        "SNITCH_HEADER_ONLY": False,  # "Create a single-header header-only version of snitch."
-        "SNITCH_UNITY_BUILD": UNITY_BUILD,  # "Build sources as single file instead of separate files (faster full build)."
-        "SNITCH_DO_TEST": True,  # "Build tests."
-    }
-
-    snitch_full_version = ""
-    git_run = subprocess.run(["git", "log", "-1", "--format=%h"], text=True, cwd="snitch/", capture_output=True)
-    if git_run.returncode == 0:
-        snitch_full_version = git_run.stdout.strip()
-
-    snitch_full_version = SNITCH_VERSION + (".{}".format(snitch_full_version) if snitch_full_version else "")
-
-    snitch_env.Append(
-        CPPDEFINES=[
-            ("SNITCH_VERSION", f'\\"{SNITCH_VERSION}\\"'),
-            ("SNITCH_FULL_VERSION", f'\\"{snitch_full_version}\\"'),
-            ("SNITCH_VERSION_MAJOR", major_version),
-            ("SNITCH_VERSION_MINOR", minor_version),
-            ("SNITCH_VERSION_PATCH", patch_version),
-        ]
-    )
-    snitch_env.Append(CPPDEFINES=[("SNITCH_SHARED_LIBRARY", 1 if not HEADER_ONLY and SHARED_BUILD else 0)])
-
-    include_path = "snitch/include"
-    source_path = "snitch/src"
-    unity_source = "snitch.cpp"
-
-    config = snitch_env.GenerateSnitchConfig(
-        snitch_env.File(Path(include_path) / "snitch" / "snitch_config.hpp"), "SCsub"
-    )
-
-    snitch_env.Append(CPPPATH=[[snitch_env.Dir(p) for p in [source_path, include_path]]])
-    # Mirror snitch's source tree into the per-config build dir
-    build_root = (env.get("build_dir") or env.Dir("#").abspath).replace("\\", "/")
-    snitch_out = build_root + "/snitch/" + source_path
-    snitch_env.VariantDir(snitch_out, source_path, duplicate=True)
-    sources = snitch_env.GlobRecursive("*.cpp", [snitch_out], snitch_out + "/" + unity_source)
-
-    if UNITY_BUILD:
-        sources = [snitch_env.File(snitch_out + "/" + unity_source)]
-
-    env.snitch_sources = sources
-
-    if not HEADER_ONLY:
-        library = None
-        project_name = "snitch"
-        library_name = "lib" + project_name + env.get("suffix", "") + env["LIBSUFFIX"]
-        if SHARED_BUILD:
-            if snitch_env.get("is_msvc", False):
-                pass
-            else:
-                snitch_env.Append(CXXFLAGS=["-fvisibility=hidden", "-fvisibility-inlines-hidden"])
-
-            library = snitch_env.SharedLibrary(target=snitch_out + "/" + library_name, source=sources)
-        else:
-            library = snitch_env.StaticLibrary(target=snitch_out + "/" + library_name, source=sources)
-            env.Append(LIBPATH=[snitch_env.Dir(snitch_out)])
-            env.Prepend(LIBS=[library_name])
-
-        Default([config, library])
-
-        env.Append(CPPPATH=[snitch_env.Dir(include_path)])
-    else:
-        env.Append(CPPPATH=[[snitch_env.Dir(p) for p in [source_path, include_path]]])
-        Default(config)
-
-    env.snitch_env = snitch_env
-
-
-build_snitch(env)
diff --git a/tests/deps/snitch b/tests/deps/snitch
deleted file mode 160000
index 2a5943330..000000000
--- a/tests/deps/snitch
+++ /dev/null
@@ -1 +0,0 @@
-Subproject commit 2a594333016b9cfc1f90b116184cb0bdbf98a480
