Merge "Merge cherrypicks of [1836175, 1836176, 1836177, 1836178, 1836179, 1836180, 1836181, 1836182, 1836183, 1836184, 1836185, 1836186, 1836187, 1836188, 1836189, 1836190, 1836195, 1836196] into ndk-release-r23" into ndk-release-r23
diff --git a/build/cmake/adjust_api_level.cmake b/build/cmake/adjust_api_level.cmake
new file mode 100644
index 0000000..780d76c
--- /dev/null
+++ b/build/cmake/adjust_api_level.cmake
@@ -0,0 +1,63 @@
+include(${CMAKE_ANDROID_NDK}/build/cmake/platforms.cmake)
+
+function(adjust_api_level api_level result_name)
+  # If no platform version was chosen by the user, default to the minimum
+  # version supported by this NDK.
+  if(NOT api_level)
+    message(STATUS
+      "ANDROID_PLATFORM not set. Defaulting to minimum supported version "
+      "${NDK_MIN_PLATFORM_LEVEL}.")
+
+    set(api_level "android-${NDK_MIN_PLATFORM_LEVEL}")
+  endif()
+
+  if(api_level STREQUAL "latest")
+    message(STATUS
+      "Using latest available ANDROID_PLATFORM: ${NDK_MAX_PLATFORM_LEVEL}.")
+    set(api_level "android-${NDK_MAX_PLATFORM_LEVEL}")
+  endif()
+
+  string(REPLACE "android-" "" result ${api_level})
+
+  # Aliases defined by meta/platforms.json include codename aliases for platform
+  # API levels as well as cover any gaps in platforms that may not have had NDK
+  # APIs.
+  if(NOT "${NDK_PLATFORM_ALIAS_${result}}" STREQUAL "")
+    message(STATUS
+      "${api_level} is an alias for ${NDK_PLATFORM_ALIAS_${result}}. Adjusting "
+      "ANDROID_PLATFORM to match.")
+    set(api_level "${NDK_PLATFORM_ALIAS_${result}}")
+    string(REPLACE "android-" "" result ${api_level})
+  endif()
+
+  # Pull up to the minimum supported version if an old API level was requested.
+  if(result LESS NDK_MIN_PLATFORM_LEVEL)
+    message(STATUS
+      "${api_level} is unsupported. Using minimum supported version "
+      "${NDK_MIN_PLATFORM_LEVEL}.")
+    set(api_level "android-${NDK_MIN_PLATFORM_LEVEL}")
+    string(REPLACE "android-" "" result ${api_level})
+  endif()
+
+  # And for LP64 we need to pull up to 21. No diagnostic is provided here
+  # because minSdkVersion < 21 is valid for the project even though it may not
+  # be for this ABI.
+  if(ANDROID_ABI MATCHES "64(-v8a)?$" AND result LESS 21)
+    message(STATUS
+      "android-${result} is not supported for ${ANDROID_ABI}. Using minimum "
+      "supported LP64 version 21.")
+    set(api_level android-21)
+    set(result 21)
+  endif()
+
+  # ANDROID_PLATFORM beyond the maximum is an error. The correct way to specify
+  # the latest version is ANDROID_PLATFORM=latest.
+  if(result GREATER NDK_MAX_PLATFORM_LEVEL)
+    message(SEND_ERROR
+      "${api_level} is above the maximum supported version "
+      "${NDK_MAX_PLATFORM_LEVEL}. Choose a supported API level or set "
+      "ANDROID_PLATFORM to \"latest\".")
+  endif()
+
+  set(${result_name} ${result} PARENT_SCOPE)
+endfunction()
diff --git a/build/cmake/android-legacy.toolchain.cmake b/build/cmake/android-legacy.toolchain.cmake
index aac7750..b2fdd10 100644
--- a/build/cmake/android-legacy.toolchain.cmake
+++ b/build/cmake/android-legacy.toolchain.cmake
@@ -456,12 +456,6 @@
 list(APPEND ANDROID_LINKER_FLAGS_EXE -Wl,--gc-sections)
 
 # Debug and release flags.
-list(APPEND ANDROID_COMPILER_FLAGS_DEBUG -O0)
-if(ANDROID_ABI MATCHES "^armeabi" AND ANDROID_ARM_MODE STREQUAL thumb)
-  list(APPEND ANDROID_COMPILER_FLAGS_RELEASE -Oz)
-else()
-  list(APPEND ANDROID_COMPILER_FLAGS_RELEASE -O2)
-endif()
 list(APPEND ANDROID_COMPILER_FLAGS_RELEASE -DNDEBUG)
 if(ANDROID_TOOLCHAIN STREQUAL clang)
   list(APPEND ANDROID_COMPILER_FLAGS_DEBUG -fno-limit-debug-info)
diff --git a/build/cmake/android.toolchain.cmake b/build/cmake/android.toolchain.cmake
index 2467cad..b01340e 100644
--- a/build/cmake/android.toolchain.cmake
+++ b/build/cmake/android.toolchain.cmake
@@ -166,34 +166,8 @@
   "https://android.googlesource.com/platform/ndk/+/master/docs/ClangMigration.md.")
 endif()
 
-include(${CMAKE_ANDROID_NDK}/build/cmake/platforms.cmake)
-
-if(ANDROID_NATIVE_API_LEVEL AND NOT ANDROID_PLATFORM)
-  if(ANDROID_NATIVE_API_LEVEL MATCHES "^android-[0-9]+$")
-    set(ANDROID_PLATFORM ${ANDROID_NATIVE_API_LEVEL})
-  elseif(ANDROID_NATIVE_API_LEVEL MATCHES "^[0-9]+$")
-    set(ANDROID_PLATFORM android-${ANDROID_NATIVE_API_LEVEL})
-  endif()
-endif()
-if(NOT CMAKE_SYSTEM_VERSION AND ANDROID_PLATFORM)
-  if(ANDROID_PLATFORM STREQUAL "latest")
-    message(STATUS
-      "Using latest available ANDROID_PLATFORM: ${NDK_MAX_PLATFORM_LEVEL}.")
-    set(CMAKE_SYSTEM_VERSION "${NDK_MAX_PLATFORM_LEVEL}")
-  else()
-    string(REPLACE "android-" "" CMAKE_SYSTEM_VERSION ${ANDROID_PLATFORM})
-    # Aliases defined by meta/platforms.json include codename aliases for platform
-    # API levels as well as cover any gaps in platforms that may not have had NDK
-    # APIs.
-    if(NOT "${NDK_PLATFORM_ALIAS_${CMAKE_SYSTEM_VERSION}}" STREQUAL "")
-      message(STATUS "\
-        ${CMAKE_SYSTEM_VERSION} is an alias for \
-        ${NDK_PLATFORM_ALIAS_${CMAKE_SYSTEM_VERSION}}.")
-      string(REPLACE "android-" "" CMAKE_SYSTEM_VERSION
-             "${NDK_PLATFORM_ALIAS_${CMAKE_SYSTEM_VERSION}}")
-    endif()
-  endif()
-endif()
+include(${CMAKE_ANDROID_NDK}/build/cmake/adjust_api_level.cmake)
+adjust_api_level("${ANDROID_PLATFORM}" CMAKE_SYSTEM_VERSION)
 
 if(NOT DEFINED CMAKE_ANDROID_STL_TYPE AND DEFINED ANDROID_STL)
   set(CMAKE_ANDROID_STL_TYPE ${ANDROID_STL})
@@ -265,6 +239,7 @@
   ANDROID_PLATFORM
   ANDROID_STL
   ANDROID_TOOLCHAIN
+  ANDROID_USE_LEGACY_TOOLCHAIN_FILE
 )
 
 if(DEFINED ANDROID_NO_UNDEFINED AND NOT DEFINED ANDROID_ALLOW_UNDEFINED_SYMBOLS)
@@ -278,11 +253,6 @@
   set(ANDROID_ALLOW_UNDEFINED_SYMBOLS "${ANDROID_SO_UNDEFINED}")
 endif()
 
-# https://github.com/android/ndk/issues/133
-if(CMAKE_ANDROID_ARCH_ABI MATCHES "^armeabi" AND NOT CMAKE_ANDROID_ARM_MODE)
-  string(APPEND _ANDROID_NDK_INIT_CFLAGS_RELEASE " -Oz")
-endif()
-
 # Exports compatible variables defined in exports.cmake.
 set(_ANDROID_EXPORT_COMPATIBILITY_VARIABLES TRUE)
 
diff --git a/build/cmake/hooks/pre/Determine-Compiler.cmake b/build/cmake/hooks/pre/Determine-Compiler.cmake
index b8ed1bf..ef0228f 100644
--- a/build/cmake/hooks/pre/Determine-Compiler.cmake
+++ b/build/cmake/hooks/pre/Determine-Compiler.cmake
@@ -15,3 +15,26 @@
 # This is a hook file that will be included by cmake at the beginning of
 # Modules/Platform/Android/Determine-Compiler.cmake.
 
+# Skip hook for the legacy toolchain workflow.
+if(CMAKE_SYSTEM_VERSION EQUAL 1)
+  return()
+endif()
+
+# If we don't explicitly set the target CMake will ID the compiler using the
+# default target, causing MINGW to be defined when a Windows host is used.
+# https://github.com/android/ndk/issues/1581
+# https://gitlab.kitware.com/cmake/cmake/-/issues/22647
+if(CMAKE_ANDROID_ARCH_ABI STREQUAL armeabi-v7a)
+  set(ANDROID_LLVM_TRIPLE armv7-none-linux-androideabi)
+elseif(CMAKE_ANDROID_ARCH_ABI STREQUAL arm64-v8a)
+  set(ANDROID_LLVM_TRIPLE aarch64-none-linux-android)
+elseif(CMAKE_ANDROID_ARCH_ABI STREQUAL x86)
+  set(ANDROID_LLVM_TRIPLE i686-none-linux-android)
+elseif(CMAKE_ANDROID_ARCH_ABI STREQUAL x86_64)
+  set(ANDROID_LLVM_TRIPLE x86_64-none-linux-android)
+else()
+  message(FATAL_ERROR "Invalid Android ABI: ${ANDROID_ABI}.")
+endif()
+set(CMAKE_ASM_COMPILER_TARGET "${ANDROID_LLVM_TRIPLE}${CMAKE_SYSTEM_VERSION}")
+set(CMAKE_C_COMPILER_TARGET "${ANDROID_LLVM_TRIPLE}${CMAKE_SYSTEM_VERSION}")
+set(CMAKE_CXX_COMPILER_TARGET "${ANDROID_LLVM_TRIPLE}${CMAKE_SYSTEM_VERSION}")
diff --git a/docs/changelogs/Changelog-r23.md b/docs/changelogs/Changelog-r23.md
index 3fca9f4..7df6a5a 100644
--- a/docs/changelogs/Changelog-r23.md
+++ b/docs/changelogs/Changelog-r23.md
@@ -28,6 +28,51 @@
 
 [Clang Migration Notes]: ClangMigration.md
 
+## r23b
+
+* Update LLVM to clang-r416183c1, based on LLVM 12 development.
+  * [Issue 1540]: Fixed compiler crash when using coroutines.
+  * [Issue 1544]: Now uses universal binaries for M1 Macs.
+  * [Issue 1551]: Prevent each translation unit from receiving its own copy of
+    emulated thread-local global variables.
+  * [Issue 1555]: Fixed compiler crash for armeabi-v7a.
+* [Issue 1492]: ndk-build.cmd: Stop using make's `-O` (`--output-sync`) flag on
+  Windows to avoid `fcntl(): Bad file descriptor` error.
+* [Issue 1553]: Updated sysroot to latest Android 12.
+* [Issue 1569]: Fixed `-fno-integrated-as` not being able to find the assembler.
+* CMake changes:
+  * [Issue 1536]: Make optimization flags used with CMake more consistent.
+    Historically thumb release builds used `-Oz`, but AGP switched to using
+    `RelWithDebInfo` for release builds in the latest release which was not
+    using `-Oz`. To reduce per-arch differences and behavior differences
+    compared to CMake's defaults, `-Oz` use was removed. You may see code size
+    increases for armeabi-v7a due to this, but also increased optimization. To
+    restore the prior behavior, add `-Oz` to your cflags.
+  * [Issue 1560]: Fixed pull-up of unsupported API levels when using the new
+    CMake toolchain file. This affects CMake 3.21 and
+    `ANDROID_USE_LEGACY_TOOLCHAIN_FILE=ON` use cases, and was the common case
+    for AGP users with a `minSdkVersion` below 21.
+  * [Issue 1573]: Fixed `ANDROID_USE_LEGACY_TOOLCHAIN_FILE` not being obeyed
+    during CMake try-compile.
+  * [Issue 1581]: Added workaround for [CMake Issue 22647], which was causing
+    `MINGW` to be incorrectly defined by CMake when building for Android on a
+    Windows host. This only affected those using the Android toolchain file when
+    CMake 3.21 or newer was used. This likely was not a regression for users not
+    using the Android toolchain. The change will fix both use cases.
+
+[CMake Issue 22647]: https://gitlab.kitware.com/cmake/cmake/-/issues/22647
+[Issue 1492]: https://github.com/android/ndk/issues/1492
+[Issue 1536]: https://github.com/android/ndk/issues/1536
+[Issue 1540]: https://github.com/android/ndk/issues/1540
+[Issue 1544]: https://github.com/android/ndk/issues/1544
+[Issue 1551]: https://github.com/android/ndk/issues/1551
+[Issue 1553]: https://github.com/android/ndk/issues/1553
+[Issue 1555]: https://github.com/android/ndk/issues/1555
+[Issue 1560]: https://github.com/android/ndk/issues/1560
+[Issue 1569]: https://github.com/android/ndk/issues/1569
+[Issue 1573]: https://github.com/android/ndk/issues/1573
+[Issue 1581]: https://github.com/android/ndk/issues/1581
+
 ## Changes
 
 * Includes Android 12 APIs.
diff --git a/ndk/checkbuild.py b/ndk/checkbuild.py
index 25360cf..b32d7a3 100755
--- a/ndk/checkbuild.py
+++ b/ndk/checkbuild.py
@@ -400,12 +400,12 @@
         # https://github.com/android-ndk/ndk/issues/564#issuecomment-342307128
         shutil.rmtree(install_path / 'include')
 
-        if not self.host.is_windows:
-            # The Linux and Darwin toolchains have Python compiler wrappers
-            # that currently do nothing. We don't have these for Windows and we
-            # want to make sure Windows behavior is consistent with the other
-            # platforms, so just unwrap the compilers until they do something
-            # useful and are available on Windows.
+        if self.host is Host.Linux:
+            # The Linux toolchain wraps the compiler to inject some behavior
+            # for the platform. They aren't used for every platform and we want
+            # consistent behavior across platforms, and we also don't want the
+            # extra cost they incur (fork/exec is cheap, but CreateProcess is
+            # expensive), so remove them.
             (bin_dir / 'clang++.real').unlink()
             (bin_dir / 'clang++').unlink()
             (bin_dir / 'clang-cl').unlink()
diff --git a/ndk/cmake.py b/ndk/cmake.py
index fd8e71c..7290de9 100644
--- a/ndk/cmake.py
+++ b/ndk/cmake.py
@@ -15,6 +15,7 @@
 #
 """APIs for dealing with cmake scripts."""
 
+from functools import cached_property
 import os
 from pathlib import Path
 import pprint
@@ -23,7 +24,7 @@
 import subprocess
 from typing import Dict, List, Optional
 
-from ndk.hosts import Host, get_default_host
+from ndk.hosts import Host
 import ndk.paths
 import ndk.toolchains
 
@@ -40,6 +41,18 @@
 }
 
 
+def find_cmake() -> Path:
+    host = Host.current()
+    return (ndk.paths.ANDROID_DIR / 'prebuilts' / 'cmake' / host.platform_tag /
+            'bin' / 'cmake').with_suffix(host.exe_suffix)
+
+
+def find_ninja() -> Path:
+    host = Host.current()
+    return (ndk.paths.ANDROID_DIR / 'prebuilts' / 'ninja' / host.platform_tag /
+            'ninja').with_suffix(host.exe_suffix)
+
+
 class CMakeBuilder:
     """Builder for an cmake project."""
 
@@ -116,20 +129,20 @@
 
         subprocess.check_call(cmd, env=subproc_env, cwd=self.working_directory)
 
-    @property
+    @cached_property
     def _cmake(self) -> Path:
-        return (ndk.paths.ANDROID_DIR / 'prebuilts' / 'cmake' /
-                (get_default_host().value + '-x86') / 'bin' / 'cmake')
+        return find_cmake()
 
-    @property
+    @cached_property
     def _ninja(self) -> Path:
-        return (ndk.paths.ANDROID_DIR / 'prebuilts' / 'ninja' /
-                (get_default_host().value + '-x86') / 'ninja')
+        return find_ninja()
 
     @property
     def _ctest(self) -> Path:
+        host = Host.current()
         return (ndk.paths.ANDROID_DIR / 'prebuilts' / 'cmake' /
-                (get_default_host().value + '-x86') / 'bin' / 'ctest')
+                host.platform_tag / 'bin' / 'ctest').with_suffix(
+                    host.exe_suffix)
 
     @property
     def cmake_defines(self) -> Dict[str, str]:
diff --git a/ndk/config.py b/ndk/config.py
index b7e1179..6066c5a 100644
--- a/ndk/config.py
+++ b/ndk/config.py
@@ -2,7 +2,7 @@
 
 
 major = 23
-hotfix = 0
+hotfix = 1
 hotfix_str = chr(ord('a') + hotfix) if hotfix else ''
 beta = 0
 beta_str = '-beta{}'.format(beta) if beta > 0 else ''
diff --git a/ndk/hosts.py b/ndk/hosts.py
index e313e5b..ad00324 100644
--- a/ndk/hosts.py
+++ b/ndk/hosts.py
@@ -39,6 +39,26 @@
     def tag(self) -> str:
         return host_to_tag(self)
 
+    @property
+    def platform_tag(self) -> str:
+        """Returns the tag used for this host in the platform tree.
+
+        The NDK uses full architecture names like x86_64, whereas the platform
+        has always used just x86, even for the 64-bit tools.
+        """
+        if self is Host.Windows64:
+            # The value for this is still "windows64" since we historically
+            # supported 32-bit Windows. Can clean this up if we ever fix the
+            # value of the enum.
+            return 'windows-x86'
+        return f'{self.value}-x86'
+
+    @property
+    def exe_suffix(self) -> str:
+        if self is Host.Windows64:
+            return '.exe'
+        return ''
+
     @classmethod
     def current(cls) -> Host:
         """Returns the Host matching the current machine."""
diff --git a/ndk/test/scanner.py b/ndk/test/scanner.py
index 47c3ff9..8870db8 100644
--- a/ndk/test/scanner.py
+++ b/ndk/test/scanner.py
@@ -95,7 +95,6 @@
         return [
             PythonBuildTest(name, path, config, self.ndk_path)
             for config in self.build_configurations
-            if config.toolchain_file == CMakeToolchainFile.Default
         ]
 
     def make_ndk_build_tests(self, path: str, name: str) -> List[Test]:
diff --git a/ndk/test/types.py b/ndk/test/types.py
index 22a5e08..23e22ab 100644
--- a/ndk/test/types.py
+++ b/ndk/test/types.py
@@ -32,11 +32,11 @@
 
 from ndk.abis import Abi
 import ndk.ansi
+from ndk.cmake import find_cmake, find_ninja
 import ndk.ext.os
 import ndk.ext.shutil
 import ndk.ext.subprocess
 import ndk.hosts
-from ndk.hosts import Host
 import ndk.ndkbuild
 import ndk.paths
 from ndk.test.config import LibcxxTestConfig, TestConfig
@@ -185,9 +185,8 @@
         _prep_build_dir(self.test_dir, build_dir)
         with ndk.ext.os.cd(build_dir):
             module = imp.load_source('test', 'test.py')
-            assert self.api is not None
             success, failure_message = module.run_test(  # type: ignore
-                self.ndk_path, self.abi, self.api)
+                self.ndk_path, self.config)
             if success:
                 return Success(self), []
             else:
@@ -384,23 +383,8 @@
                           use_legacy_toolchain_file: bool) -> TestResult:
     _prep_build_dir(test_dir, obj_dir)
 
-    # Add prebuilts to PATH.
-    host = ndk.hosts.get_default_host()
-    if host == Host.Windows64:
-        # The value for this is still "windows64" since we historically
-        # supported 32-bit Windows. Can clean this up if we ever fix the value
-        # of the enum.
-        prebuilts_host_tag = 'windows-x86'
-    else:
-        prebuilts_host_tag = ndk.hosts.get_default_host().value + '-x86'
-    cmake_bin = ndk.paths.android_path(
-        'prebuilts', 'cmake', prebuilts_host_tag, 'bin', 'cmake')
-    ninja_bin = ndk.paths.android_path(
-        'prebuilts', 'ninja', prebuilts_host_tag, 'ninja')
-
-    if host == Host.Windows64:
-        cmake_bin += '.exe'
-        ninja_bin += '.exe'
+    cmake_bin = find_cmake()
+    ninja_bin = find_ninja()
 
     toolchain_file = os.path.join(ndk_path, 'build', 'cmake',
                                   'android.toolchain.cmake')
@@ -423,11 +407,11 @@
     else:
         args.append('-DANDROID_USE_LEGACY_TOOLCHAIN_FILE=OFF')
     rc, out = ndk.ext.subprocess.call_output(
-        [cmake_bin] + cmake_flags + args, encoding='utf-8')
+        [str(cmake_bin)] + cmake_flags + args, encoding='utf-8')
     if rc != 0:
         return Failure(test, out)
     rc, out = ndk.ext.subprocess.call_output(
-        [cmake_bin, '--build', abi_obj_dir, '--'] + _get_jobs_args(),
+        [str(cmake_bin), '--build', abi_obj_dir, '--'] + _get_jobs_args(),
         encoding='utf-8')
     if rc != 0:
         return Failure(test, out)
diff --git a/ndk/testing/flag_verifier.py b/ndk/testing/flag_verifier.py
index 68acf08..82ed7e3 100644
--- a/ndk/testing/flag_verifier.py
+++ b/ndk/testing/flag_verifier.py
@@ -14,14 +14,16 @@
 # limitations under the License.
 #
 """Tools for verifying the presence or absence of flags in builds."""
+from __future__ import annotations
+
 from pathlib import Path
 import shutil
 import subprocess
-from typing import List, Optional, Tuple
+from typing import Optional
 
-from ndk.abis import Abi
 from ndk.hosts import Host
 import ndk.paths
+from ndk.test.spec import BuildConfiguration, CMakeToolchainFile
 
 
 class FlagVerifierResult:
@@ -34,7 +36,7 @@
         """Returns True if verification failed."""
         raise NotImplementedError
 
-    def make_test_result_tuple(self) -> Tuple[bool, Optional[str]]:
+    def make_test_result_tuple(self) -> tuple[bool, Optional[str]]:
         """Creates a test result tuple in the format expect by run_test."""
         return not self.failed(), self.error_message
 
@@ -60,14 +62,22 @@
 class FlagVerifier:
     """Verifies that a build receives the expected flags."""
 
-    def __init__(self, project: Path, ndk_path: Path, abi: Abi,
-                 api: int) -> None:
+    def __init__(self, project: Path, ndk_path: Path,
+                 config: BuildConfiguration) -> None:
         self.project = project
         self.ndk_path = ndk_path
-        self.abi = abi
+        self.abi = config.abi
+        self.api = config.api
+        if config.toolchain_file is CMakeToolchainFile.Legacy:
+            self.toolchain_mode = 'ON'
+        else:
+            self.toolchain_mode = 'OFF'
+        self.expected_flags: list[str] = []
+        self.not_expected_flags: list[str] = []
+
+    def with_api(self, api: int) -> FlagVerifier:
         self.api = api
-        self.expected_flags: List[str] = []
-        self.not_expected_flags: List[str] = []
+        return self
 
     def expect_flag(self, flag: str) -> None:
         """Verify that the given string is present in the build output.
@@ -93,7 +103,7 @@
             raise ValueError(f'Flag {flag} both expected and not expected')
         self.not_expected_flags.append(flag)
 
-    def _check_build(self, cmd: List[str]) -> FlagVerifierResult:
+    def _check_build(self, cmd: list[str]) -> FlagVerifierResult:
         result = subprocess.run(cmd,
                                 check=False,
                                 stdout=subprocess.PIPE,
@@ -103,8 +113,8 @@
             return FlagVerifierFailure(result.stdout)
 
         words = result.stdout.split(' ')
-        missing_flags: List[str] = []
-        wrong_flags: List[str] = []
+        missing_flags: list[str] = []
+        wrong_flags: list[str] = []
         for expected in self.expected_flags:
             if expected not in words:
                 missing_flags.append(expected)
@@ -151,12 +161,17 @@
             f'APP_PLATFORM=android-{self.api}',
         ])
 
-    def verify_cmake(self) -> FlagVerifierResult:
+    def verify_cmake(
+            self,
+            cmake_flags: Optional[list[str]] = None) -> FlagVerifierResult:
         """Verifies that CMake behaves as specified.
 
         Returns:
             A FlagVerifierResult object describing the verification result.
         """
+        if cmake_flags is None:
+            cmake_flags = []
+
         host = Host.current()
         if host == Host.Windows64:
             tag = 'windows-x86'
@@ -182,9 +197,10 @@
             f'-DCMAKE_TOOLCHAIN_FILE={toolchain_file}',
             f'-DANDROID_ABI={self.abi}',
             f'-DANDROID_PLATFORM=android-{self.api}',
+            f'-DANDROID_USE_LEGACY_TOOLCHAIN_FILE={self.toolchain_mode}',
             '-GNinja',
             f'-DCMAKE_MAKE_PROGRAM={ninja}',
-        ]
+        ] + cmake_flags
         result = subprocess.run(cmd,
                                 check=False,
                                 stdout=subprocess.PIPE,
diff --git a/ndk/testing/standalone_toolchain.py b/ndk/testing/standalone_toolchain.py
index 88f24a8..5854600 100644
--- a/ndk/testing/standalone_toolchain.py
+++ b/ndk/testing/standalone_toolchain.py
@@ -18,16 +18,17 @@
 import shutil
 import subprocess
 import tempfile
-from typing import Any, List, Tuple
+from typing import Any
 
 import ndk.abis
+from ndk.test.spec import BuildConfiguration
 
 
 def logger() -> logging.Logger:
     return logging.getLogger(__name__)
 
 
-def call_output(cmd: List[str], *args: Any, **kwargs: Any) -> Tuple[int, Any]:
+def call_output(cmd: list[str], *args: Any, **kwargs: Any) -> tuple[int, Any]:
     logger().info('COMMAND: %s', ' '.join(cmd))
     kwargs.update({
         'stdout': subprocess.PIPE,
@@ -38,15 +39,16 @@
         return proc.returncode, out
 
 
-def make_standalone_toolchain(ndk_path: str, arch: str, api: int,
-                              extra_args: List[str],
-                              install_dir: str) -> Tuple[bool, str]:
+def make_standalone_toolchain(ndk_path: str, config: BuildConfiguration,
+                              extra_args: list[str],
+                              install_dir: str) -> tuple[bool, str]:
     make_standalone_toolchain_path = os.path.join(
         ndk_path, 'build/tools/make_standalone_toolchain.py')
 
+    arch = ndk.abis.abi_to_arch(config.abi)
     cmd = [make_standalone_toolchain_path, '--force',
            '--install-dir=' + install_dir, '--arch=' + arch,
-           '--api={}'.format(api)] + extra_args
+           '--api={}'.format(config.api)] + extra_args
 
     if os.name == 'nt':
         # Windows doesn't process shebang lines, and we wouldn't be pointing at
@@ -67,7 +69,7 @@
 
 
 def test_standalone_toolchain(install_dir: str, test_source: str,
-                              flags: List[str]) -> Tuple[bool, str]:
+                              flags: list[str]) -> tuple[bool, str]:
     compiler_name = 'clang++'
 
     compiler = os.path.join(install_dir, 'bin', compiler_name)
@@ -81,14 +83,13 @@
     return rc == 0, out.decode('utf-8')
 
 
-def run_test(ndk_path: str, abi: ndk.abis.Abi, api: int, test_source: str,
-             extra_args: List[str], flags: List[str]) -> Tuple[bool, str]:
-    arch = ndk.abis.abi_to_arch(abi)
+def run_test(ndk_path: str, config: BuildConfiguration, test_source: str,
+             extra_args: list[str], flags: list[str]) -> tuple[bool, str]:
 
     install_dir = tempfile.mkdtemp()
     try:
         success, out = make_standalone_toolchain(
-            ndk_path, arch, api, extra_args, install_dir)
+            ndk_path, config, extra_args, install_dir)
         if not success:
             return success, out
         return test_standalone_toolchain(install_dir, test_source, flags)
diff --git a/ndk/toolchains.py b/ndk/toolchains.py
index 550533b..d5ea189 100644
--- a/ndk/toolchains.py
+++ b/ndk/toolchains.py
@@ -22,7 +22,7 @@
 import ndk.paths
 
 
-CLANG_VERSION = 'clang-r416183b'
+CLANG_VERSION = 'clang-r416183c1'
 
 
 HOST_TRIPLE_MAP = {
diff --git a/ndk/workqueue.py b/ndk/workqueue.py
index 82cade9..57aac48 100644
--- a/ndk/workqueue.py
+++ b/ndk/workqueue.py
@@ -47,7 +47,7 @@
 
 
 if IS_WINDOWS:
-    import ctypes
+    import ctypes.wintypes
     ProcessGroup = Optional[ctypes.wintypes.HANDLE]
 
 
diff --git a/tests/build/NDK_ANALYZE/test.py b/tests/build/NDK_ANALYZE/test.py
index 54e8181..1113fbe 100644
--- a/tests/build/NDK_ANALYZE/test.py
+++ b/tests/build/NDK_ANALYZE/test.py
@@ -16,20 +16,19 @@
 import os
 import subprocess
 import sys
-from typing import Tuple
 
-from ndk.abis import Abi
+from ndk.test.spec import BuildConfiguration
 
 
-def run_test(ndk_path: str, abi: Abi, api: int) -> Tuple[bool, str]:
+def run_test(ndk_path: str, config: BuildConfiguration) -> tuple[bool, str]:
     """Checks ndk-build output for clang-tidy warnings."""
     ndk_build = os.path.join(ndk_path, 'ndk-build')
     if sys.platform == 'win32':
         ndk_build += '.cmd'
     project_path = 'project'
     ndk_args = [
-        f'APP_ABI={abi}',
-        f'APP_PLATFORM=android-{api}',
+        f'APP_ABI={config.abi}',
+        f'APP_PLATFORM=android-{config.api}',
         'NDK_ANALYZE=1',
     ]
     proc = subprocess.Popen([ndk_build, '-C', project_path] + ndk_args,
diff --git a/tests/build/build_id/test.py b/tests/build/build_id/test.py
index d49c4a3..bb6b43a 100644
--- a/tests/build/build_id/test.py
+++ b/tests/build/build_id/test.py
@@ -21,15 +21,16 @@
 Studio.
 """
 from pathlib import Path
-from typing import Optional, Tuple
+from typing import Optional
 
-from ndk.abis import Abi
+from ndk.test.spec import BuildConfiguration
 from ndk.testing.flag_verifier import FlagVerifier
 
 
-def run_test(ndk_path: str, abi: Abi, api: int) -> Tuple[bool, Optional[str]]:
+def run_test(ndk_path: str,
+             config: BuildConfiguration) -> tuple[bool, Optional[str]]:
     """Checks correct --build-id use."""
-    verifier = FlagVerifier(Path('project'), Path(ndk_path), abi, api)
+    verifier = FlagVerifier(Path('project'), Path(ndk_path), config)
     verifier.expect_flag('-Wl,--build-id=sha1')
     verifier.expect_not_flag('-Wl,--build-id')
     return verifier.verify().make_test_result_tuple()
diff --git a/tests/build/clang_tidy/test.py b/tests/build/clang_tidy/test.py
index 6416bcc..888d0a1 100644
--- a/tests/build/clang_tidy/test.py
+++ b/tests/build/clang_tidy/test.py
@@ -17,20 +17,19 @@
 import os
 import subprocess
 import sys
-from typing import Tuple
 
-from ndk.abis import Abi
+from ndk.test.spec import BuildConfiguration
 
 
-def run_test(ndk_path: str, abi: Abi, api: int) -> Tuple[bool, str]:
+def run_test(ndk_path: str, config: BuildConfiguration) -> tuple[bool, str]:
     """Checks ndk-build V=1 output for clang-tidy warnings."""
     ndk_build = os.path.join(ndk_path, 'ndk-build')
     if sys.platform == 'win32':
         ndk_build += '.cmd'
     project_path = 'project'
     ndk_args = [
-        f'APP_ABI={abi}',
-        f'APP_PLATFORM=android-{api}',
+        f'APP_ABI={config.abi}',
+        f'APP_PLATFORM=android-{config.api}',
         'V=1',
     ]
     proc = subprocess.Popen([ndk_build, '-C', project_path] + ndk_args,
diff --git a/tests/build/cmake_api_pull_up/__init__.py b/tests/build/cmake_api_pull_up/__init__.py
new file mode 100644
index 0000000..e69de29
--- /dev/null
+++ b/tests/build/cmake_api_pull_up/__init__.py
diff --git a/tests/build/cmake_api_pull_up/project/CMakeLists.txt b/tests/build/cmake_api_pull_up/project/CMakeLists.txt
new file mode 100644
index 0000000..ddcd353
--- /dev/null
+++ b/tests/build/cmake_api_pull_up/project/CMakeLists.txt
@@ -0,0 +1,4 @@
+cmake_minimum_required(VERSION 3.6)
+project(ApiLevelPullUpTest CXX)
+
+add_library(foo SHARED foo.cpp)
diff --git a/tests/build/cmake_api_pull_up/project/foo.cpp b/tests/build/cmake_api_pull_up/project/foo.cpp
new file mode 100644
index 0000000..b876b5c
--- /dev/null
+++ b/tests/build/cmake_api_pull_up/project/foo.cpp
@@ -0,0 +1,3 @@
+#if __ANDROID_API__ != 21
+#error API level was not pulled up to 21
+#endif
diff --git a/tests/build/cmake_api_pull_up/test.py b/tests/build/cmake_api_pull_up/test.py
new file mode 100644
index 0000000..c1c8be8
--- /dev/null
+++ b/tests/build/cmake_api_pull_up/test.py
@@ -0,0 +1,48 @@
+#
+# Copyright (C) 2021 The Android Open Source Project
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+#      http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+#
+"""Check that pre-LP64 API levels are correctly pulled-up for CMake."""
+from pathlib import Path
+import subprocess
+
+from ndk.cmake import find_cmake, find_ninja
+from ndk.test.spec import BuildConfiguration, CMakeToolchainFile
+
+
+def run_test(ndk_path: str, config: BuildConfiguration) -> tuple[bool, str]:
+    """Check that pre-LP64 API levels are correctly pulled-up for CMake."""
+    cmake = find_cmake()
+    ninja = find_ninja()
+    toolchain_path = Path(ndk_path) / 'build/cmake/android.toolchain.cmake'
+    project_path = 'project'
+    if config.toolchain_file is CMakeToolchainFile.Legacy:
+        toolchain_mode = 'ON'
+    else:
+        toolchain_mode = 'OFF'
+    cmake_cmd = [
+        str(cmake),
+        f'-DCMAKE_TOOLCHAIN_FILE={toolchain_path}',
+        f'-DANDROID_ABI={config.abi}',
+        '-DANDROID_PLATFORM=android-19',
+        f'-DCMAKE_MAKE_PROGRAM={ninja}',
+        f'-DANDROID_USE_LEGACY_TOOLCHAIN_FILE={toolchain_mode}',
+        '-GNinja',
+    ]
+    result = subprocess.run(cmake_cmd,
+                            check=False,
+                            cwd=project_path,
+                            capture_output=True,
+                            text=True)
+    return result.returncode == 0, result.stdout
diff --git a/tests/build/cmake_api_pull_up/test_config.py b/tests/build/cmake_api_pull_up/test_config.py
new file mode 100644
index 0000000..aa58ee5
--- /dev/null
+++ b/tests/build/cmake_api_pull_up/test_config.py
@@ -0,0 +1,10 @@
+from typing import Optional
+
+from ndk.abis import LP32_ABIS
+from ndk.test.types import Test
+
+
+def build_unsupported(test: Test) -> Optional[str]:
+    if test.config.abi in LP32_ABIS:
+        return test.config.abi
+    return None
diff --git a/tests/build/cmake_default_flags/__init__.py b/tests/build/cmake_default_flags/__init__.py
new file mode 100644
index 0000000..e69de29
--- /dev/null
+++ b/tests/build/cmake_default_flags/__init__.py
diff --git a/tests/build/cmake_default_flags/project/CMakeLists.txt b/tests/build/cmake_default_flags/project/CMakeLists.txt
new file mode 100644
index 0000000..4cf4562
--- /dev/null
+++ b/tests/build/cmake_default_flags/project/CMakeLists.txt
@@ -0,0 +1,4 @@
+cmake_minimum_required(VERSION 3.6)
+project(CMakeDefaultFlagsTest CXX)
+
+add_library(foo SHARED foo.cpp)
diff --git a/tests/build/cmake_default_flags/project/foo.cpp b/tests/build/cmake_default_flags/project/foo.cpp
new file mode 100644
index 0000000..e69de29
--- /dev/null
+++ b/tests/build/cmake_default_flags/project/foo.cpp
diff --git a/tests/build/cmake_default_flags/test.py b/tests/build/cmake_default_flags/test.py
new file mode 100644
index 0000000..a7ead75
--- /dev/null
+++ b/tests/build/cmake_default_flags/test.py
@@ -0,0 +1,34 @@
+#
+# Copyright (C) 2021 The Android Open Source Project
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+#      http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+#
+"""Check that the CMake toolchain uses the correct default flags."""
+from pathlib import Path
+from typing import Optional
+
+from ndk.test.spec import BuildConfiguration
+from ndk.testing.flag_verifier import FlagVerifier
+
+
+def run_test(ndk_path: str,
+             config: BuildConfiguration) -> tuple[bool, Optional[str]]:
+    """Check that the CMake toolchain uses the correct default flags.
+
+    Currently this only tests the optimization flags for RelWithDebInfo, but
+    it's probably worth expanding in the future.
+    """
+    verifier = FlagVerifier(Path('project'), Path(ndk_path), config)
+    verifier.expect_flag('-O2')
+    return verifier.verify_cmake(['-DCMAKE_BUILD_TYPE=RelWithDebInfo'
+                                  ]).make_test_result_tuple()
diff --git a/tests/build/cmake_not_mingw/CMakeLists.txt b/tests/build/cmake_not_mingw/CMakeLists.txt
new file mode 100644
index 0000000..9008214
--- /dev/null
+++ b/tests/build/cmake_not_mingw/CMakeLists.txt
@@ -0,0 +1,6 @@
+cmake_minimum_required(VERSION 3.6)
+project(CMakeNotMinGW ASM C CXX)
+
+if(DEFINED MINGW)
+  message(FATAL_ERROR "MINGW should not be defined")
+endif()
diff --git a/tests/build/cmake_toolchain_defaults/__init__.py b/tests/build/cmake_toolchain_defaults/__init__.py
new file mode 100644
index 0000000..e69de29
--- /dev/null
+++ b/tests/build/cmake_toolchain_defaults/__init__.py
diff --git a/tests/build/cmake_toolchain_defaults/project/CMakeLists.txt b/tests/build/cmake_toolchain_defaults/project/CMakeLists.txt
new file mode 100644
index 0000000..350b9bf
--- /dev/null
+++ b/tests/build/cmake_toolchain_defaults/project/CMakeLists.txt
@@ -0,0 +1,4 @@
+cmake_minimum_required(VERSION 3.6)
+project(ToolchainDefaultsTest CXX)
+
+add_library(foo SHARED foo.cpp)
diff --git a/tests/build/cmake_toolchain_defaults/project/foo.cpp b/tests/build/cmake_toolchain_defaults/project/foo.cpp
new file mode 100644
index 0000000..83e1781
--- /dev/null
+++ b/tests/build/cmake_toolchain_defaults/project/foo.cpp
@@ -0,0 +1,8 @@
+#if !defined(__ARM_ARCH_7A__)
+#error ABI did not default to armeabi-v7a
+#endif
+
+// Update this whenever we raise the minimum API level in the NDK.
+#if __ANDROID_API__ != 19
+#error API level did not default to 19
+#endif
diff --git a/tests/build/cmake_toolchain_defaults/test.py b/tests/build/cmake_toolchain_defaults/test.py
new file mode 100644
index 0000000..f48a2a7
--- /dev/null
+++ b/tests/build/cmake_toolchain_defaults/test.py
@@ -0,0 +1,51 @@
+#
+# Copyright (C) 2021 The Android Open Source Project
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+#      http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+#
+"""Check that the default CMake toolchain behavior works."""
+from pathlib import Path
+import subprocess
+
+from ndk.cmake import find_cmake, find_ninja
+from ndk.test.spec import BuildConfiguration, CMakeToolchainFile
+
+
+def run_test(ndk_path: str, config: BuildConfiguration) -> tuple[bool, str]:
+    """Check that the default CMake toolchain behavior works.
+
+    All our regular CMake tests pass the API level and ABI explicitly. This
+    test checks that the defaults (armeabi-v7a, minimum supported API level)
+    work.
+    """
+    cmake = find_cmake()
+    ninja = find_ninja()
+    toolchain_path = Path(ndk_path) / 'build/cmake/android.toolchain.cmake'
+    project_path = 'project'
+    if config.toolchain_file is CMakeToolchainFile.Legacy:
+        toolchain_mode = 'ON'
+    else:
+        toolchain_mode = 'OFF'
+    cmake_cmd = [
+        str(cmake),
+        f'-DCMAKE_TOOLCHAIN_FILE={toolchain_path}',
+        f'-DCMAKE_MAKE_PROGRAM={ninja}',
+        f'-DANDROID_USE_LEGACY_TOOLCHAIN_FILE={toolchain_mode}',
+        '-GNinja',
+    ]
+    result = subprocess.run(cmake_cmd,
+                            check=False,
+                            cwd=project_path,
+                            capture_output=True,
+                            text=True)
+    return result.returncode == 0, result.stdout
diff --git a/tests/build/cmake_toolchain_defaults/test_config.py b/tests/build/cmake_toolchain_defaults/test_config.py
new file mode 100644
index 0000000..aa58ee5
--- /dev/null
+++ b/tests/build/cmake_toolchain_defaults/test_config.py
@@ -0,0 +1,10 @@
+from typing import Optional
+
+from ndk.abis import LP32_ABIS
+from ndk.test.types import Test
+
+
+def build_unsupported(test: Test) -> Optional[str]:
+    if test.config.abi in LP32_ABIS:
+        return test.config.abi
+    return None
diff --git a/tests/build/link_order/test.py b/tests/build/link_order/test.py
index 2a6f6ee..e73fd88 100644
--- a/tests/build/link_order/test.py
+++ b/tests/build/link_order/test.py
@@ -21,12 +21,13 @@
 import shlex
 import subprocess
 import sys
-from typing import Iterator, Optional, Tuple
+from typing import Iterator, Optional
 
 from ndk.abis import Abi
+from ndk.test.spec import BuildConfiguration
 
 
-def is_linked_item(arg):
+def is_linked_item(arg: str) -> bool:
     """Returns True if the argument is an object or library to be linked."""
     if arg.endswith('.a'):
         return True
@@ -39,7 +40,7 @@
     return False
 
 
-def find_link_args(link_line):
+def find_link_args(link_line: str) -> list[str]:
     """Returns a list of objects and libraries in the link command."""
     args = []
 
@@ -67,7 +68,7 @@
     return args
 
 
-def builtins_basename(abi: Abi):
+def builtins_basename(abi: Abi) -> str:
     runtimes_arch = {
         'armeabi-v7a': 'arm',
         'arm64-v8a': 'aarch64',
@@ -77,12 +78,14 @@
     return 'libclang_rt.builtins-' + runtimes_arch + '-android.a'
 
 
-def check_link_order(link_line: str, abi: Abi,
-                     api: int) -> Tuple[bool, Optional[Iterator[str]]]:
+def check_link_order(
+        link_line: str,
+        config: BuildConfiguration) -> tuple[bool, Optional[Iterator[str]]]:
     """Determines if a given link command has the correct ordering.
 
     Args:
-        link_line (string): The full ld command.
+        link_line: The full ld command.
+        config: The test's build configuration.
 
     Returns:
         Tuple of (success, diff). The diff will be None on success or a
@@ -90,7 +93,8 @@
         suitable for use with `' '.join()`. The diff represents the changes
         between the expected link order and the actual link order.
     """
-    android_support_arg = ['libandroid_support.a'] if api < 21 else []
+    assert config.api is not None
+    android_support_arg = ['libandroid_support.a'] if config.api < 21 else []
     expected = [
         'crtbegin_so.o',
         'foo.o',
@@ -105,11 +109,11 @@
         '-lc',
         '-lm',
         '-lm',
-        builtins_basename(abi),
+        builtins_basename(config.abi),
         '-l:libunwind.a',
         '-ldl',
         '-lc',
-        builtins_basename(abi),
+        builtins_basename(config.abi),
         '-l:libunwind.a',
         '-ldl',
         'crtend_so.o',
@@ -120,15 +124,15 @@
     return False, difflib.unified_diff(expected, link_args, lineterm='')
 
 
-def run_test(ndk_path: str, abi: Abi, api: int) -> Tuple[bool, str]:
+def run_test(ndk_path: str, config: BuildConfiguration) -> tuple[bool, str]:
     """Checks clang's -v output for proper link ordering."""
     ndk_build = os.path.join(ndk_path, 'ndk-build')
     if sys.platform == 'win32':
         ndk_build += '.cmd'
     project_path = 'project'
     ndk_args = [
-        f'APP_ABI={abi}',
-        f'APP_PLATFORM=android-{api}',
+        f'APP_ABI={config.abi}',
+        f'APP_PLATFORM=android-{config.api}',
     ]
     proc = subprocess.Popen([ndk_build, '-C', project_path] + ndk_args,
                             stdout=subprocess.PIPE, stderr=subprocess.STDOUT,
@@ -150,5 +154,5 @@
     if link_line is None:
         return False, 'Did not find link line in out:\n{}'.format(out)
 
-    result, diff = check_link_order(link_line, abi, api)
+    result, diff = check_link_order(link_line, config)
     return result, '' if diff is None else os.linesep.join(diff)
diff --git a/tests/build/lld_rosegment/test.py b/tests/build/lld_rosegment/test.py
index 68713d7..b36fd18 100644
--- a/tests/build/lld_rosegment/test.py
+++ b/tests/build/lld_rosegment/test.py
@@ -18,21 +18,24 @@
 https://github.com/android/ndk/issues/1196
 """
 from pathlib import Path
-from typing import Optional, Tuple
+from typing import Optional
 
-from ndk.abis import Abi
+from ndk.test.spec import BuildConfiguration
 from ndk.testing.flag_verifier import FlagVerifier
 
 
-def run_test(ndk_path: str, abi: Abi, _api: int) -> Tuple[bool, Optional[str]]:
+def run_test(ndk_path: str,
+             config: BuildConfiguration) -> tuple[bool, Optional[str]]:
     """Checks correct --no-rosegment use."""
-    verifier = FlagVerifier(Path('project'), Path(ndk_path), abi, 28)
+    verifier = FlagVerifier(Path('project'), Path(ndk_path),
+                            config).with_api(28)
     verifier.expect_flag('-Wl,--no-rosegment')
     verifier.expect_not_flag('-Wl,--rosegment')
     result = verifier.verify()
     if result.failed():
         return result.make_test_result_tuple()
 
-    verifier = FlagVerifier(Path('project'), Path(ndk_path), abi, 29)
+    verifier = FlagVerifier(Path('project'), Path(ndk_path),
+                            config).with_api(29)
     verifier.expect_not_flag('-Wl,--no-rosegment')
     return verifier.verify().make_test_result_tuple()
diff --git a/tests/build/mstackrealign/test.py b/tests/build/mstackrealign/test.py
index ab3e4c9..5e581bb 100644
--- a/tests/build/mstackrealign/test.py
+++ b/tests/build/mstackrealign/test.py
@@ -19,16 +19,19 @@
 issues. For these devices, verify that mstackrealign is used.
 """
 from pathlib import Path
-from typing import Optional, Tuple
+from typing import Optional
 
 from ndk.abis import Abi
+from ndk.test.spec import BuildConfiguration
 from ndk.testing.flag_verifier import FlagVerifier
 
 
-def run_test(ndk_path: str, abi: Abi, api: int) -> Tuple[bool, Optional[str]]:
+def run_test(ndk_path: str,
+             config: BuildConfiguration) -> tuple[bool, Optional[str]]:
     """Checks ndk-build V=1 output for mstackrealign flag."""
-    verifier = FlagVerifier(Path('project'), Path(ndk_path), abi, api)
-    if abi == Abi('x86') and api < 24:
+    verifier = FlagVerifier(Path('project'), Path(ndk_path), config)
+    assert config.api is not None
+    if config.abi == Abi('x86') and config.api < 24:
         verifier.expect_flag('-mstackrealign')
     else:
         verifier.expect_not_flag('-mstackrealign')
diff --git a/tests/build/no_platform_gaps/test.py b/tests/build/no_platform_gaps/test.py
index 3b224a1..d6c0861 100644
--- a/tests/build/no_platform_gaps/test.py
+++ b/tests/build/no_platform_gaps/test.py
@@ -28,23 +28,22 @@
 from pathlib import Path
 import subprocess
 import sys
-from typing import Tuple
 
 import ndk.testing.standalone_toolchain
 
 import ndk.abis
-from ndk.abis import Abi
 from ndk.hosts import Host
+from ndk.test.spec import BuildConfiguration
 
 
-def build(ndk_dir: str, abi: Abi, api: int) -> Tuple[bool, str]:
+def build(ndk_dir: str, config: BuildConfiguration) -> tuple[bool, str]:
     ndk_build = os.path.join(ndk_dir, 'ndk-build')
     if sys.platform == 'win32':
         ndk_build += '.cmd'
     project_path = 'project'
     ndk_args = [
-        f'APP_ABI={abi}',
-        f'APP_PLATFORM=android-{api}',
+        f'APP_ABI={config.abi}',
+        f'APP_PLATFORM=android-{config.api}',
         'V=1',
     ]
     proc = subprocess.Popen([ndk_build, '-C', project_path] + ndk_args,
@@ -53,13 +52,13 @@
     return proc.returncode == 0, out.decode('utf-8')
 
 
-def run_test(ndk_path: str, abi: Abi, _api: int) -> Tuple[bool, str]:
+def run_test(ndk_path: str, config: BuildConfiguration) -> tuple[bool, str]:
     """Checks ndk-build V=1 output for correct compiler."""
     min_api = None
     max_api = None
     apis = []
     host = Host.current().tag
-    triple = ndk.abis.arch_to_triple(ndk.abis.abi_to_arch(abi))
+    triple = ndk.abis.arch_to_triple(ndk.abis.abi_to_arch(config.abi))
     toolchain_dir = Path(ndk_path) / f'toolchains/llvm/prebuilt/{host}'
     lib_dir = toolchain_dir / f'sysroot/usr/lib/{triple}'
     for path in lib_dir.iterdir():
@@ -83,7 +82,7 @@
 
     missing_platforms = sorted(list(set(range(min_api, max_api)) - set(apis)))
     for api in missing_platforms:
-        result, out = build(ndk_path, abi, api)
+        result, out = build(ndk_path, config)
         if not result:
             return result, out
 
diff --git a/tests/build/standalone_toolchain/test.py b/tests/build/standalone_toolchain/test.py
index a9a4512..4b803f1 100644
--- a/tests/build/standalone_toolchain/test.py
+++ b/tests/build/standalone_toolchain/test.py
@@ -13,13 +13,11 @@
 # See the License for the specific language governing permissions and
 # limitations under the License.
 #
-from typing import Tuple
-
-from ndk.abis import Abi
+from ndk.test.spec import BuildConfiguration
 import ndk.testing.standalone_toolchain
 
 
-def run_test(ndk_path: str, abi: Abi, api: int) -> Tuple[bool, str]:
-    return ndk.testing.standalone_toolchain.run_test(ndk_path, abi, api,
+def run_test(ndk_path: str, config: BuildConfiguration) -> tuple[bool, str]:
+    return ndk.testing.standalone_toolchain.run_test(ndk_path, config,
                                                      'foo.cpp',
                                                      ['--stl=libc++'], [])
diff --git a/tests/build/standalone_toolchain_no_android_support/test.py b/tests/build/standalone_toolchain_no_android_support/test.py
index 4ead3a2..d42c168 100644
--- a/tests/build/standalone_toolchain_no_android_support/test.py
+++ b/tests/build/standalone_toolchain_no_android_support/test.py
@@ -13,12 +13,10 @@
 # See the License for the specific language governing permissions and
 # limitations under the License.
 #
-from typing import Tuple
-
-from ndk.abis import Abi
+from ndk.test.spec import BuildConfiguration
 import ndk.testing.standalone_toolchain
 
 
-def run_test(ndk_path: str, abi: Abi, _api: int) -> Tuple[bool, str]:
-    return ndk.testing.standalone_toolchain.run_test(ndk_path, abi, 21,
+def run_test(ndk_path: str, config: BuildConfiguration) -> tuple[bool, str]:
+    return ndk.testing.standalone_toolchain.run_test(ndk_path, config,
                                                      'foo.cpp', [], [])
diff --git a/tests/build/standalone_toolchain_thumb/test.py b/tests/build/standalone_toolchain_thumb/test.py
index f0502cc..3fc2cbb 100644
--- a/tests/build/standalone_toolchain_thumb/test.py
+++ b/tests/build/standalone_toolchain_thumb/test.py
@@ -13,12 +13,10 @@
 # See the License for the specific language governing permissions and
 # limitations under the License.
 #
-from typing import Tuple
-
-from ndk.abis import Abi
+from ndk.test.spec import BuildConfiguration
 import ndk.testing.standalone_toolchain
 
 
-def run_test(ndk_path: str, abi: Abi, api: int) -> Tuple[bool, str]:
+def run_test(ndk_path: str, config: BuildConfiguration) -> tuple[bool, str]:
     return ndk.testing.standalone_toolchain.run_test(
-        ndk_path, abi, api, 'foo.cpp', ['--stl=libc++'], ['-mthumb'])
+        ndk_path, config, 'foo.cpp', ['--stl=libc++'], ['-mthumb'])
diff --git a/tests/build/strip/test.py b/tests/build/strip/test.py
index d9e1be5..a0fcf36 100644
--- a/tests/build/strip/test.py
+++ b/tests/build/strip/test.py
@@ -15,14 +15,15 @@
 #
 """Check for strip --strip-unneeded use."""
 from pathlib import Path
-from typing import Optional, Tuple
+from typing import Optional
 
-from ndk.abis import Abi
+from ndk.test.spec import BuildConfiguration
 from ndk.testing.flag_verifier import FlagVerifier
 
 
-def run_test(ndk_path: str, abi: Abi, api: int) -> Tuple[bool, Optional[str]]:
+def run_test(ndk_path: str,
+             config: BuildConfiguration) -> tuple[bool, Optional[str]]:
     """Checks ndk-build V=1 output for --strip-unneeded flag."""
-    verifier = FlagVerifier(Path('project'), Path(ndk_path), abi, api)
+    verifier = FlagVerifier(Path('project'), Path(ndk_path), config)
     verifier.expect_flag('--strip-unneeded')
     return verifier.verify_ndk_build().make_test_result_tuple()
diff --git a/tests/build/strip_keep_symbols/test.py b/tests/build/strip_keep_symbols/test.py
index bf2ba85..fe8f94f 100644
--- a/tests/build/strip_keep_symbols/test.py
+++ b/tests/build/strip_keep_symbols/test.py
@@ -15,15 +15,16 @@
 #
 """Check for strip --strip-debug use."""
 from pathlib import Path
-from typing import Optional, Tuple
+from typing import Optional
 
-from ndk.abis import Abi
+from ndk.test.spec import BuildConfiguration
 from ndk.testing.flag_verifier import FlagVerifier
 
 
-def run_test(ndk_path: str, abi: Abi, api: int) -> Tuple[bool, Optional[str]]:
+def run_test(ndk_path: str,
+             config: BuildConfiguration) -> tuple[bool, Optional[str]]:
     """Checks ndk-build V=1 output for --strip-debug flag."""
-    verifier = FlagVerifier(Path('project'), Path(ndk_path), abi, api)
+    verifier = FlagVerifier(Path('project'), Path(ndk_path), config)
     verifier.expect_flag('--strip-debug')
     verifier.expect_not_flag('--strip-unneeded')
     return verifier.verify_ndk_build().make_test_result_tuple()
diff --git a/tests/build/strip_keep_symbols_app/test.py b/tests/build/strip_keep_symbols_app/test.py
index bf2ba85..fe8f94f 100644
--- a/tests/build/strip_keep_symbols_app/test.py
+++ b/tests/build/strip_keep_symbols_app/test.py
@@ -15,15 +15,16 @@
 #
 """Check for strip --strip-debug use."""
 from pathlib import Path
-from typing import Optional, Tuple
+from typing import Optional
 
-from ndk.abis import Abi
+from ndk.test.spec import BuildConfiguration
 from ndk.testing.flag_verifier import FlagVerifier
 
 
-def run_test(ndk_path: str, abi: Abi, api: int) -> Tuple[bool, Optional[str]]:
+def run_test(ndk_path: str,
+             config: BuildConfiguration) -> tuple[bool, Optional[str]]:
     """Checks ndk-build V=1 output for --strip-debug flag."""
-    verifier = FlagVerifier(Path('project'), Path(ndk_path), abi, api)
+    verifier = FlagVerifier(Path('project'), Path(ndk_path), config)
     verifier.expect_flag('--strip-debug')
     verifier.expect_not_flag('--strip-unneeded')
     return verifier.verify_ndk_build().make_test_result_tuple()
diff --git a/tests/build/strip_local_overrides_app/test.py b/tests/build/strip_local_overrides_app/test.py
index 5b566d1..c237130 100644
--- a/tests/build/strip_local_overrides_app/test.py
+++ b/tests/build/strip_local_overrides_app/test.py
@@ -15,15 +15,16 @@
 #
 """Check for strip --strip-unneeded use."""
 from pathlib import Path
-from typing import Optional, Tuple
+from typing import Optional
 
-from ndk.abis import Abi
+from ndk.test.spec import BuildConfiguration
 from ndk.testing.flag_verifier import FlagVerifier
 
 
-def run_test(ndk_path: str, abi: Abi, api: int) -> Tuple[bool, Optional[str]]:
+def run_test(ndk_path: str,
+             config: BuildConfiguration) -> tuple[bool, Optional[str]]:
     """Checks ndk-build V=1 output for --strip-unneeded flag."""
-    verifier = FlagVerifier(Path('project'), Path(ndk_path), abi, api)
+    verifier = FlagVerifier(Path('project'), Path(ndk_path), config)
     verifier.expect_not_flag('--strip-debug')
     verifier.expect_flag('--strip-unneeded')
     return verifier.verify_ndk_build().make_test_result_tuple()
diff --git a/tests/build/strip_none/test.py b/tests/build/strip_none/test.py
index d655926..ed70424 100644
--- a/tests/build/strip_none/test.py
+++ b/tests/build/strip_none/test.py
@@ -15,15 +15,16 @@
 #
 """Check that strip is not used."""
 from pathlib import Path
-from typing import Optional, Tuple
+from typing import Optional
 
-from ndk.abis import Abi
+from ndk.test.spec import BuildConfiguration
 from ndk.testing.flag_verifier import FlagVerifier
 
 
-def run_test(ndk_path: str, abi: Abi, api: int) -> Tuple[bool, Optional[str]]:
+def run_test(ndk_path: str,
+             config: BuildConfiguration) -> tuple[bool, Optional[str]]:
     """Checks ndk-build V=1 output for lack of strip."""
-    verifier = FlagVerifier(Path('project'), Path(ndk_path), abi, api)
+    verifier = FlagVerifier(Path('project'), Path(ndk_path), config)
     # TODO: Fix this test.
     # This test has always been wrong, since it was only doing whole word
     # search for 'strip' and we call strip with its full path.
diff --git a/tests/build/unwinder_hidden/test.py b/tests/build/unwinder_hidden/test.py
index cd23ce8..00b82ff 100644
--- a/tests/build/unwinder_hidden/test.py
+++ b/tests/build/unwinder_hidden/test.py
@@ -18,10 +18,10 @@
 from pathlib import Path
 import re
 import subprocess
-from typing import Iterator, Tuple
+from typing import Iterator
 
-from ndk.abis import Abi
 import ndk.hosts
+from ndk.test.spec import BuildConfiguration
 
 
 def find_public_unwind_symbols(output: str) -> Iterator[str]:
@@ -55,7 +55,7 @@
         stderr=subprocess.STDOUT).stdout
 
 
-def run_test(ndk_path: str, abi: Abi, api: int) -> Tuple[bool, str]:
+def run_test(ndk_path: str, config: BuildConfiguration) -> tuple[bool, str]:
     """Check that unwinder symbols are hidden in outputs."""
     ndk_build = Path(ndk_path) / 'ndk-build'
     host = ndk.hosts.get_default_host()
@@ -63,8 +63,8 @@
         ndk_build = ndk_build.with_suffix('.cmd')
     project_path = Path('project')
     ndk_args = [
-        f'APP_ABI={abi}',
-        f'APP_PLATFORM=android-{api}',
+        f'APP_ABI={config.abi}',
+        f'APP_PLATFORM=android-{config.api}',
     ]
     subprocess.run(
         [str(ndk_build), '-C', str(project_path)] + ndk_args,
@@ -72,7 +72,7 @@
         stdout=subprocess.PIPE,
         stderr=subprocess.STDOUT)
 
-    library = project_path / 'libs' / str(abi) / 'libfoo.so'
+    library = project_path / 'libs' / str(config.abi) / 'libfoo.so'
     readelf_output = readelf(Path(ndk_path), host, library, '-sW')
     for symbol in find_public_unwind_symbols(readelf_output):
         return False, f'Found public unwind symbol: {symbol}'
diff --git a/tests/build/wrap_sh/test.py b/tests/build/wrap_sh/test.py
index 45d28d1..6cf9744 100644
--- a/tests/build/wrap_sh/test.py
+++ b/tests/build/wrap_sh/test.py
@@ -19,20 +19,19 @@
 import subprocess
 import sys
 import textwrap
-from typing import Tuple
 
-from ndk.abis import Abi
+from ndk.test.spec import BuildConfiguration
 
 
-def run_test(ndk_path: str, abi: Abi, api: int) -> Tuple[bool, str]:
+def run_test(ndk_path: str, config: BuildConfiguration) -> tuple[bool, str]:
     """Checks that the proper wrap.sh scripts were installed."""
     ndk_build = os.path.join(ndk_path, 'ndk-build')
     if sys.platform == 'win32':
         ndk_build += '.cmd'
     project_path = 'project'
     ndk_args = [
-        f'APP_ABI={abi}',
-        f'APP_PLATFORM=android-{api}',
+        f'APP_ABI={config.abi}',
+        f'APP_PLATFORM=android-{config.api}',
     ]
     proc = subprocess.Popen([ndk_build, '-C', project_path] + ndk_args,
                             stdout=subprocess.PIPE, stderr=subprocess.STDOUT,
@@ -41,16 +40,16 @@
     if proc.returncode != 0:
         return proc.returncode == 0, out
 
-    wrap_sh = os.path.join(project_path, 'libs', abi, 'wrap.sh')
+    wrap_sh = os.path.join(project_path, 'libs', config.abi, 'wrap.sh')
     if not os.path.exists(wrap_sh):
         return False, f'{wrap_sh} does not exist'
 
     with open(wrap_sh) as wrap_sh_file:
         contents = wrap_sh_file.read().strip()
-    if contents != abi:
+    if contents != config.abi:
         return False, textwrap.dedent(f"""\
             wrap.sh file had wrong contents:
-            Expected: {abi}
+            Expected: {config.abi}
             Actual: {contents}""")
 
     return True, ''
diff --git a/tests/build/wrap_sh_generic/test.py b/tests/build/wrap_sh_generic/test.py
index 13b1a68..b9d8f9a 100644
--- a/tests/build/wrap_sh_generic/test.py
+++ b/tests/build/wrap_sh_generic/test.py
@@ -19,20 +19,19 @@
 import subprocess
 import sys
 import textwrap
-from typing import Tuple
 
-from ndk.abis import Abi
+from ndk.test.spec import BuildConfiguration
 
 
-def run_test(ndk_path: str, abi: Abi, api: int) -> Tuple[bool, str]:
+def run_test(ndk_path: str, config: BuildConfiguration) -> tuple[bool, str]:
     """Checks that the proper wrap.sh scripts were installed."""
     ndk_build = os.path.join(ndk_path, 'ndk-build')
     if sys.platform == 'win32':
         ndk_build += '.cmd'
     project_path = 'project'
     ndk_args = [
-        f'APP_ABI={abi}',
-        f'APP_PLATFORM=android-{api}',
+        f'APP_ABI={config.abi}',
+        f'APP_PLATFORM=android-{config.api}',
     ]
     proc = subprocess.Popen([ndk_build, '-C', project_path] + ndk_args,
                             stdout=subprocess.PIPE, stderr=subprocess.STDOUT,
@@ -41,7 +40,7 @@
     if proc.returncode != 0:
         return proc.returncode == 0, out
 
-    wrap_sh = os.path.join(project_path, 'libs', abi, 'wrap.sh')
+    wrap_sh = os.path.join(project_path, 'libs', config.abi, 'wrap.sh')
     if not os.path.exists(wrap_sh):
         return False, '{} does not exist'.format(wrap_sh)
 
@@ -49,7 +48,7 @@
         contents = wrap_sh_file.read().strip()
     if contents != 'generic':
         return False, textwrap.dedent(f"""\
-            {abi} wrap.sh file had wrong contents:
+            {config.abi} wrap.sh file had wrong contents:
             Expected: generic
             Actual: {contents}""")
 
diff --git a/tests/build/wrap_sh_none/test.py b/tests/build/wrap_sh_none/test.py
index 370b1ce..8611614 100644
--- a/tests/build/wrap_sh_none/test.py
+++ b/tests/build/wrap_sh_none/test.py
@@ -17,20 +17,19 @@
 import os
 import subprocess
 import sys
-from typing import Tuple
 
-from ndk.abis import Abi
+from ndk.test.spec import BuildConfiguration
 
 
-def run_test(ndk_path: str, abi: Abi, api: int) -> Tuple[bool, str]:
+def run_test(ndk_path: str, config: BuildConfiguration) -> tuple[bool, str]:
     """Checks that the proper wrap.sh scripts were installed."""
     ndk_build = os.path.join(ndk_path, 'ndk-build')
     if sys.platform == 'win32':
         ndk_build += '.cmd'
     project_path = 'project'
     ndk_args = [
-        f'APP_ABI={abi}',
-        f'APP_PLATFORM=android-{api}',
+        f'APP_ABI={config.abi}',
+        f'APP_PLATFORM=android-{config.api}',
     ]
     proc = subprocess.Popen([ndk_build, '-C', project_path] + ndk_args,
                             stdout=subprocess.PIPE, stderr=subprocess.STDOUT,
@@ -39,7 +38,7 @@
     if proc.returncode != 0:
         return proc.returncode == 0, out
 
-    wrap_sh = os.path.join(project_path, 'libs', abi, 'wrap.sh')
+    wrap_sh = os.path.join(project_path, 'libs', config.abi, 'wrap.sh')
     if os.path.exists(wrap_sh):
         return False, '{} should not exist'.format(wrap_sh)
     return True, ''