Partially revert sanity checking refactor

This broke Cython checking, which was allowed by causing it to be
disabled in CI.

This is a squash of three patches:
 - Revert "compilers/asm: Implement infrastructure for sanity checking"
   This reverts commit 08221c6d1fc4a6afe780a479bbc7e40899242601.

 - Revert "compilers/asm: Store the C Compiler inside the Assembly Compilers"
   This reverts commit 8847c938dd1c9e2c6e64e3050eb58f7ec54fccb3.

 - Revert "compilers: refactor sanity checking code"
   This reverts commit 806289a5d27958a084bc6cba41b7cf9ccee4ecf4.
diff --git a/mesonbuild/compilers/asm.py b/mesonbuild/compilers/asm.py
index 9eda37a..94439be 100644
--- a/mesonbuild/compilers/asm.py
+++ b/mesonbuild/compilers/asm.py
@@ -4,15 +4,14 @@
 import textwrap
 import typing as T
 
-from .. import mlog
-from ..mesonlib import EnvironmentException, Popen_safe, join_args, get_meson_command
+from ..mesonlib import EnvironmentException, get_meson_command
 from ..options import OptionKey
 from .compilers import Compiler
 from .mixins.metrowerks import MetrowerksCompiler, mwasmarm_instruction_set_args, mwasmeppc_instruction_set_args
 from .mixins.ti import TICompiler
 
 if T.TYPE_CHECKING:
-    from ..environment import Environment
+    from ..linkers.linkers import DynamicLinker
     from ..mesonlib import MachineChoice
     from ..envconfig import MachineInfo
 
@@ -27,10 +26,6 @@
 }
 
 
-class _CheckUnimplementedException(Exception):
-    pass
-
-
 class ASMCompiler(Compiler):
 
     """Shared base class for all ASM Compilers (Assemblers)"""
@@ -38,66 +33,12 @@
     _SUPPORTED_ARCHES: T.Set[str] = set()
 
     def __init__(self, ccache: T.List[str], exelist: T.List[str], version: str,
-                 for_machine: 'MachineChoice', info: 'MachineInfo', compiler: Compiler,
+                 for_machine: MachineChoice, info: MachineInfo,
+                 linker: T.Optional[DynamicLinker] = None,
                  full_version: T.Optional[str] = None, is_cross: bool = False):
-        assert compiler.linker is not None, 'for mypy'
         if self._SUPPORTED_ARCHES and info.cpu_family not in self._SUPPORTED_ARCHES:
             raise EnvironmentException(f'ASM Compiler {self.id} does not support building for {info.cpu_family} CPU family.')
-        super().__init__(ccache, exelist, version, for_machine, info, compiler.linker, full_version, is_cross)
-        self._compiler = compiler
-
-    def _sanity_check_compile_args(self, env: Environment, sourcename: str, binname: str) -> T.List[str]:
-        # TODO: fallback back to this is always wrong, it means that the
-        # concrete implementation is missing the sanity check implementation
-        return []
-
-    def _sanity_check_filenames(self) -> T.Tuple[str, str]:
-        src, bin = super()._sanity_check_filenames()
-        bin = '{}.obj'.format(os.path.splitext(bin)[0])
-        return src, bin
-
-    def _sanity_check_source_code(self) -> str:
-        raise _CheckUnimplementedException()
-
-    def _run_sanity_check(self, env: Environment, cmdlist: T.List[str], work_dir: str) -> None:
-        # This is a bit of a hack
-        return
-
-    def sanity_check(self, work_dir: str, env: Environment) -> None:
-        try:
-            super().sanity_check(work_dir, env)
-        except _CheckUnimplementedException:
-            if self.info.kernel:
-                name = self.info.kernel
-                if self.info.subsystem:
-                    name = f'{name} {self.info.subsystem}'
-            else:
-                name = self.info.system
-            mlog.warning(
-                f'Missing {self.id} sanity check code for {name}.',
-                'You can help by providing such an implementation',
-                fatal=False, once=True)
-            return
-
-        # This is the object from the compilation
-        src = self._sanity_check_filenames()[1]
-        bin = self._compiler._sanity_check_filenames()[1]
-
-        cmdlist = self._compiler._sanity_check_compile_args(env, src, bin)
-
-        pc, stdo, stde = Popen_safe(cmdlist, cwd=work_dir)
-        mlog.debug('Sanity check linker command line:', join_args(cmdlist))
-        mlog.debug('Sanity check linker stdout:')
-        mlog.debug(stdo)
-        mlog.debug('-----\nSanity check linker stderr:')
-        mlog.debug(stde)
-        mlog.debug('-----')
-        if pc.returncode != 0:
-            raise EnvironmentError(
-                f'Compiler {self._compiler.name_string()} could not link an object from the {self.name_string()} assembler')
-
-        # This is also a hack
-        return super()._run_sanity_check(env, [os.path.join(work_dir, bin)], work_dir)
+        super().__init__(ccache, exelist, version, for_machine, info, linker, full_version, is_cross)
 
 
 class NasmCompiler(ASMCompiler):
@@ -116,9 +57,10 @@
     _SUPPORTED_ARCHES = {'x86', 'x86_64'}
 
     def __init__(self, ccache: T.List[str], exelist: T.List[str], version: str,
-                 for_machine: 'MachineChoice', info: 'MachineInfo', compiler: Compiler,
+                 for_machine: 'MachineChoice', info: 'MachineInfo',
+                 linker: T.Optional['DynamicLinker'] = None,
                  full_version: T.Optional[str] = None, is_cross: bool = False):
-        super().__init__(ccache, exelist, version, for_machine, info, compiler, full_version, is_cross)
+        super().__init__(ccache, exelist, version, for_machine, info, linker, full_version, is_cross)
         self.links_with_msvc = False
         if 'link' in self.linker.id:
             self.base_options.add(OptionKey('b_vscrt'))
@@ -361,9 +303,10 @@
     _SUPPORTED_ARCHES = {'c6000'}
 
     def __init__(self, ccache: T.List[str], exelist: T.List[str], version: str,
-                 for_machine: MachineChoice, info: MachineInfo, compiler: Compiler,
+                 for_machine: MachineChoice, info: MachineInfo,
+                 linker: T.Optional[DynamicLinker] = None,
                  full_version: T.Optional[str] = None, is_cross: bool = False):
-        ASMCompiler.__init__(self, ccache, exelist, version, for_machine, info, compiler, full_version, is_cross)
+        ASMCompiler.__init__(self, ccache, exelist, version, for_machine, info, linker, full_version, is_cross)
         TICompiler.__init__(self)
 
     def needs_static_linker(self) -> bool:
@@ -383,9 +326,10 @@
     language = 'nasm'
 
     def __init__(self, ccache: T.List[str], exelist: T.List[str], version: str,
-                 for_machine: 'MachineChoice', info: 'MachineInfo', compiler: Compiler,
+                 for_machine: 'MachineChoice', info: 'MachineInfo',
+                 linker: T.Optional['DynamicLinker'] = None,
                  full_version: T.Optional[str] = None, is_cross: bool = False):
-        ASMCompiler.__init__(self, ccache, exelist, version, for_machine, info, compiler, full_version, is_cross)
+        ASMCompiler.__init__(self, ccache, exelist, version, for_machine, info, linker, full_version, is_cross)
         MetrowerksCompiler.__init__(self)
 
         self.warn_args: T.Dict[str, T.List[str]] = {
diff --git a/mesonbuild/compilers/c.py b/mesonbuild/compilers/c.py
index a0786d5..424b612 100644
--- a/mesonbuild/compilers/c.py
+++ b/mesonbuild/compilers/c.py
@@ -76,8 +76,9 @@
     def get_no_stdinc_args(self) -> T.List[str]:
         return ['-nostdinc']
 
-    def _sanity_check_source_code(self) -> str:
-        return 'int main(void) { int class=0; return class; }\n'
+    def sanity_check(self, work_dir: str, environment: 'Environment') -> None:
+        code = 'int main(void) { int class=0; return class; }\n'
+        return self._sanity_check_impl(work_dir, environment, 'sanitycheckc.c', code)
 
     def has_header_symbol(self, hname: str, symbol: str, prefix: str,
                           env: 'Environment', *,
diff --git a/mesonbuild/compilers/compilers.py b/mesonbuild/compilers/compilers.py
index 4c28eb2..624226d 100644
--- a/mesonbuild/compilers/compilers.py
+++ b/mesonbuild/compilers/compilers.py
@@ -1196,7 +1196,8 @@
     def name_string(self) -> str:
         return ' '.join(self.exelist)
 
-    def sanity_check(self, work_dir: str, env: Environment) -> None:
+    @abc.abstractmethod
+    def sanity_check(self, work_dir: str, environment: 'Environment') -> None:
         """Check that this compiler actually works.
 
         This should provide a simple compile/link test. Something as simple as:
@@ -1204,103 +1205,24 @@
         main(): return 0
         ```
         is good enough here.
-
-        :param work_dir: A directory to put temporary artifacts
-        :param env: The :class:`environment.Environment` instance to use with
-            this  check
-        :raises mesonlib.EnvironmentException: If building the binary fails
-        :raises mesonlib.EnvironmentException: If running the binary is attempted and fails
-        """
-        sourcename, binname = self._sanity_check_filenames()
-        cmdlist = self._sanity_check_compile_args(env, sourcename, binname)
-
-        with open(os.path.join(work_dir, sourcename), 'w', encoding='utf-8') as f:
-            f.write(self._sanity_check_source_code())
-
-        pc, stdo, stde = mesonlib.Popen_safe(cmdlist, cwd=work_dir)
-        mlog.debug('Sanity check compiler command line:', mesonlib.join_args(cmdlist))
-        mlog.debug('Sanity check compile stdout:')
-        mlog.debug(stdo)
-        mlog.debug('-----\nSanity check compile stderr:')
-        mlog.debug(stde)
-        mlog.debug('-----')
-        if pc.returncode != 0:
-            raise mesonlib.EnvironmentException(f'Compiler {self.name_string()} cannot compile programs.')
-
-        self._run_sanity_check(env, [os.path.join(work_dir, binname)], work_dir)
-
-    def _sanity_check_filenames(self) -> T.Tuple[str, str]:
-        """Generate the name of the source and binary file for the sanity check.
-
-        The returned names should be just the names of the files with
-        extensions, but no paths.
-
-        :return: A tuple of (sourcename, binaryname)
-        """
-        default_ext = lang_suffixes[self.language][0]
-        template = f'sanity_check_for_{self.language}'
-        sourcename = f'{template}.{default_ext}'
-        binaryname = f'{template}{"_cross" if self.is_cross else ""}.exe'
-        return sourcename, binaryname
-
-    @abc.abstractmethod
-    def _sanity_check_compile_args(self, env: Environment, sourcename: str, binname: str) -> T.List[str]:
-        """Get arguments to run compiler for sanity check.
-
-        :param env: The :class:`environment.Environment` instance to use
-        :param sourcename: the name of the source file to generate
-        :param binname: the name of the binary file to generate
-        :return: a list of strings to pass to :func:`subprocess.run` or equivalent
         """
 
-    @abc.abstractmethod
-    def _sanity_check_source_code(self) -> str:
-        """Get the source code to run for a sanity check
-
-        :return: A string to be written into a file and ran.
-        """
-
-    def _sanity_check_run_with_exe_wrapper(self, env: Environment, command: T.List[str]) -> T.List[str]:
-        """Wrap the binary to run in the test with the exe_wrapper if necessary
-
-        Languages that do no want to use an exe_wrapper (or always want to use
-        some kind of wrapper) should override this method
-
-        :param env: the :class:`environment.Environment` instance to use
-        :param command: The string list of commands to run
-        :return: The list of commands wrapped by the exe_wrapper if it is needed, otherwise the original commands
-        """
-        if self.is_cross and env.has_exe_wrapper():
-            assert env.exe_wrapper is not None, 'for mypy'
-            return env.exe_wrapper.get_command() + command
-        return command
-
-    def _run_sanity_check(self, env: Environment, cmdlist: T.List[str], work_dir: str) -> None:
-        """Run a sanity test binary
-
-        :param env: the :class:`environment.Environment` instance to use
-        :param cmdlist: A list of strings to pass to :func:`subprocess.run` or equivalent to run the test
-        :param work_dir: A directory to place temporary artifacts
-        :raises mesonlib.EnvironmentException: If the binary cannot be run or if it returns a non-zero exit code
-        """
-        # Can't check binaries, so we have to assume they work
-        if self.is_cross and not env.has_exe_wrapper():
-            mlog.debug('Cannot run cross check')
-            return
-
-        cmdlist = self._sanity_check_run_with_exe_wrapper(env, cmdlist)
-        mlog.debug('Sanity check built target output for', self.for_machine, self.language, 'compiler')
-        mlog.debug(' -- Running test binary command: ', mesonlib.join_args(cmdlist))
+    def run_sanity_check(self, environment: Environment, cmdlist: T.List[str], work_dir: str, use_exe_wrapper_for_cross: bool = True) -> T.Tuple[str, str]:
+        # Run sanity check
+        if self.is_cross and use_exe_wrapper_for_cross:
+            if not environment.has_exe_wrapper():
+                # Can't check if the binaries run so we have to assume they do
+                return ('', '')
+            cmdlist = environment.exe_wrapper.get_command() + cmdlist
+        mlog.debug('Running test binary command: ', mesonlib.join_args(cmdlist))
         try:
             pe, stdo, stde = Popen_safe_logged(cmdlist, 'Sanity check', cwd=work_dir)
-            mlog.debug(' -- stdout:\n', stdo)
-            mlog.debug(' -- stderr:\n', stde)
-            mlog.debug(' -- returncode:', pe.returncode)
         except Exception as e:
             raise mesonlib.EnvironmentException(f'Could not invoke sanity check executable: {e!s}.')
 
         if pe.returncode != 0:
             raise mesonlib.EnvironmentException(f'Executables created by {self.language} compiler {self.name_string()} are not runnable.')
+        return stdo, stde
 
     def split_shlib_to_parts(self, fname: str) -> T.Tuple[T.Optional[str], str]:
         return None, fname
diff --git a/mesonbuild/compilers/cpp.py b/mesonbuild/compilers/cpp.py
index 8da1004..fa032ec 100644
--- a/mesonbuild/compilers/cpp.py
+++ b/mesonbuild/compilers/cpp.py
@@ -86,8 +86,9 @@
     def get_no_stdlib_link_args(self) -> T.List[str]:
         return ['-nostdlib++']
 
-    def _sanity_check_source_code(self) -> str:
-        return 'class breakCCompiler;int main(void) { return 0; }\n'
+    def sanity_check(self, work_dir: str, environment: 'Environment') -> None:
+        code = 'class breakCCompiler;int main(void) { return 0; }\n'
+        return self._sanity_check_impl(work_dir, environment, 'sanitycheckcpp.cc', code)
 
     def get_compiler_check_args(self, mode: CompileCheckMode) -> T.List[str]:
         # -fpermissive allows non-conforming code to compile which is necessary
diff --git a/mesonbuild/compilers/cs.py b/mesonbuild/compilers/cs.py
index e515c33..4bbddeb 100644
--- a/mesonbuild/compilers/cs.py
+++ b/mesonbuild/compilers/cs.py
@@ -3,10 +3,11 @@
 
 from __future__ import annotations
 
-import os.path
+import os.path, subprocess
 import textwrap
 import typing as T
 
+from ..mesonlib import EnvironmentException
 from ..linkers import RSPFileSyntax
 
 from .compilers import Compiler
@@ -82,21 +83,26 @@
     def get_pch_name(self, header_name: str) -> str:
         return ''
 
-    def _sanity_check_source_code(self) -> str:
-        return textwrap.dedent('''
-            public class Sanity {
-                static public void Main () {
+    def sanity_check(self, work_dir: str, environment: 'Environment') -> None:
+        src = 'sanity.cs'
+        obj = 'sanity.exe'
+        source_name = os.path.join(work_dir, src)
+        with open(source_name, 'w', encoding='utf-8') as ofile:
+            ofile.write(textwrap.dedent('''
+                public class Sanity {
+                    static public void Main () {
+                    }
                 }
-            }
-            ''')
-
-    def _sanity_check_compile_args(self, env: Environment, sourcename: str, binname: str) -> T.List[str]:
-        return self.exelist + self.get_always_args() + [sourcename] + self.get_output_args(binname)
-
-    def _sanity_check_run_with_exe_wrapper(self, env: Environment, command: T.List[str]) -> T.List[str]:
+                '''))
+        pc = subprocess.Popen(self.exelist + self.get_always_args() + [src], cwd=work_dir)
+        pc.wait()
+        if pc.returncode != 0:
+            raise EnvironmentException('C# compiler %s cannot compile programs.' % self.name_string())
         if self.runner:
-            return [self.runner] + command
-        return command
+            cmdlist = [self.runner, obj]
+        else:
+            cmdlist = [os.path.join(work_dir, obj)]
+        self.run_sanity_check(environment, cmdlist, work_dir, use_exe_wrapper_for_cross=False)
 
     def needs_static_linker(self) -> bool:
         return False
diff --git a/mesonbuild/compilers/cuda.py b/mesonbuild/compilers/cuda.py
index a9e6a76..7e050f1 100644
--- a/mesonbuild/compilers/cuda.py
+++ b/mesonbuild/compilers/cuda.py
@@ -5,14 +5,15 @@
 from __future__ import annotations
 
 import enum
+import os.path
 import string
 import typing as T
 
 from .. import options
 from .. import mlog
-from .. import mesonlib
 from ..mesonlib import (
-    EnvironmentException, is_windows, LibType, version_compare
+    EnvironmentException, Popen_safe,
+    is_windows, LibType, version_compare
 )
 from .compilers import Compiler, CompileCheckMode
 
@@ -186,7 +187,6 @@
                  host_compiler: Compiler, info: 'MachineInfo',
                  linker: T.Optional['DynamicLinker'] = None,
                  full_version: T.Optional[str] = None):
-        self.detected_cc = ''
         super().__init__(ccache, exelist, version, for_machine, info, linker=linker, full_version=full_version, is_cross=is_cross)
         self.host_compiler = host_compiler
         self.base_options = host_compiler.base_options
@@ -499,36 +499,55 @@
     def thread_link_flags(self, environment: 'Environment') -> T.List[str]:
         return self._to_host_flags(self.host_compiler.thread_link_flags(environment), Phase.LINKER)
 
-    def _sanity_check_source_code(self) -> str:
-        return r'''
-            #include <cuda_runtime.h>
-            #include <stdio.h>
+    def sanity_check(self, work_dir: str, env: 'Environment') -> None:
+        mlog.debug('Sanity testing ' + self.get_display_language() + ' compiler:', ' '.join(self.exelist))
+        mlog.debug('Is cross compiler: %s.' % str(self.is_cross))
 
-            __global__ void kernel (void) {}
+        sname = 'sanitycheckcuda.cu'
+        code = r'''
+        #include <cuda_runtime.h>
+        #include <stdio.h>
 
-            int main(void){
-                struct cudaDeviceProp prop;
-                int count, i;
-                cudaError_t ret = cudaGetDeviceCount(&count);
-                if(ret != cudaSuccess){
-                    fprintf(stderr, "%d\n", (int)ret);
-                }else{
-                    for(i=0;i<count;i++){
-                        if(cudaGetDeviceProperties(&prop, i) == cudaSuccess){
-                            fprintf(stdout, "%d.%d\n", prop.major, prop.minor);
-                        }
+        __global__ void kernel (void) {}
+
+        int main(void){
+            struct cudaDeviceProp prop;
+            int count, i;
+            cudaError_t ret = cudaGetDeviceCount(&count);
+            if(ret != cudaSuccess){
+                fprintf(stderr, "%d\n", (int)ret);
+            }else{
+                for(i=0;i<count;i++){
+                    if(cudaGetDeviceProperties(&prop, i) == cudaSuccess){
+                        fprintf(stdout, "%d.%d\n", prop.major, prop.minor);
                     }
                 }
-                fflush(stderr);
-                fflush(stdout);
-                return 0;
             }
-            '''
+            fflush(stderr);
+            fflush(stdout);
+            return 0;
+        }
+        '''
+        binname = sname.rsplit('.', 1)[0]
+        binname += '_cross' if self.is_cross else ''
+        source_name = os.path.join(work_dir, sname)
+        binary_name = os.path.join(work_dir, binname + '.exe')
+        with open(source_name, 'w', encoding='utf-8') as ofile:
+            ofile.write(code)
 
-    def _sanity_check_compile_args(self, env: Environment, sourcename: str, binname: str) -> T.List[str]:
+        # The Sanity Test for CUDA language will serve as both a sanity test
+        # and a native-build GPU architecture detection test, useful later.
+        #
+        # For this second purpose, NVCC has very handy flags, --run and
+        # --run-args, that allow one to run an application with the
+        # environment set up properly. Of course, this only works for native
+        # builds; For cross builds we must still use the exe_wrapper (if any).
+        self.detected_cc = ''
+        flags = []
+
         # Disable warnings, compile with statically-linked runtime for minimum
         # reliance on the system.
-        flags = ['-w', '-cudart', 'static', sourcename]
+        flags += ['-w', '-cudart', 'static', source_name]
 
         # Use the -ccbin option, if available, even during sanity checking.
         # Otherwise, on systems where CUDA does not support the default compiler,
@@ -543,30 +562,33 @@
             # a ton of compiler flags to differentiate between
             # arm and x86_64. So just compile.
             flags += self.get_compile_only_args()
-        flags += self.get_output_args(binname)
+        flags += self.get_output_args(binary_name)
 
-        return self.exelist + flags
+        # Compile sanity check
+        cmdlist = self.exelist + flags
+        mlog.debug('Sanity check compiler command line: ', ' '.join(cmdlist))
+        pc, stdo, stde = Popen_safe(cmdlist, cwd=work_dir)
+        mlog.debug('Sanity check compile stdout: ')
+        mlog.debug(stdo)
+        mlog.debug('-----\nSanity check compile stderr:')
+        mlog.debug(stde)
+        mlog.debug('-----')
+        if pc.returncode != 0:
+            raise EnvironmentException(f'Compiler {self.name_string()} cannot compile programs.')
 
-    def _run_sanity_check(self, env: Environment, cmdlist: T.List[str], work_dir: str) -> None:
-        # Can't check binaries, so we have to assume they work
-        if self.is_cross and not env.has_exe_wrapper():
-            mlog.debug('Cannot run cross check')
+        # Run sanity check (if possible)
+        if self.is_cross:
             return
 
-        cmdlist = self._sanity_check_run_with_exe_wrapper(env, cmdlist)
-        mlog.debug('Sanity check built target output for', self.for_machine, self.language, 'compiler')
-        mlog.debug(' -- Running test binary command: ', mesonlib.join_args(cmdlist))
+        cmdlist = self.exelist + ['--run', f'"{binary_name}"']
         try:
-            pe, stdo, stde = mesonlib.Popen_safe_logged(cmdlist, 'Sanity check', cwd=work_dir)
-            mlog.debug(' -- stdout:\n', stdo)
-            mlog.debug(' -- stderr:\n', stde)
-            mlog.debug(' -- returncode:', pe.returncode)
-        except Exception as e:
-            raise EnvironmentException(f'Could not invoke sanity check executable: {e!s}.')
-
-        if pe.returncode != 0:
+            stdo, stde = self.run_sanity_check(env, cmdlist, work_dir)
+        except EnvironmentException:
             raise EnvironmentException(f'Executables created by {self.language} compiler {self.name_string()} are not runnable.')
 
+        # Interpret the result of the sanity test.
+        # As mentioned above, it is not only a sanity test but also a GPU
+        # architecture detection test.
         if stde == '':
             self.detected_cc = stdo
 
diff --git a/mesonbuild/compilers/cython.py b/mesonbuild/compilers/cython.py
index 2814ca0..50bb465 100644
--- a/mesonbuild/compilers/cython.py
+++ b/mesonbuild/compilers/cython.py
@@ -1,14 +1,13 @@
 # SPDX-License-Identifier: Apache-2.0
 # Copyright © 2021-2025 Intel Corporation
+from __future__ import annotations
 
 """Abstraction for Cython language compilers."""
 
-from __future__ import annotations
-import os
 import typing as T
 
 from .. import options
-from ..mesonlib import version_compare
+from ..mesonlib import EnvironmentException, version_compare
 from .compilers import Compiler
 
 if T.TYPE_CHECKING:
@@ -50,22 +49,16 @@
     def get_depfile_suffix(self) -> str:
         return 'dep'
 
+    def sanity_check(self, work_dir: str, environment: 'Environment') -> None:
+        code = 'print("hello world")'
+        with self.cached_compile(code, environment.coredata) as p:
+            if p.returncode != 0:
+                raise EnvironmentException(f'Cython compiler {self.id!r} cannot compile programs')
+
     def get_pic_args(self) -> T.List[str]:
         # We can lie here, it's fine
         return []
 
-    def _sanity_check_source_code(self) -> str:
-        return 'print("Hello world")'
-
-    def _sanity_check_compile_args(self, env: Environment, sourcename: str, binname: str) -> T.List[str]:
-        return self.exelist + self.get_always_args() + self.get_output_args(binname) + [sourcename]
-
-    def _run_sanity_check(self, env: Environment, cmdlist: T.List[str], work_dir: str) -> None:
-        # Cython will do a Cython -> C -> Exe, so the output file will actually have
-        # the name of the C compiler.
-        # TODO: find a way to not make this so hacky
-        return super()._run_sanity_check(env, [os.path.join(work_dir, 'sanity_check_for_c.exe')], work_dir)
-
     def compute_parameters_with_absolute_paths(self, parameter_list: T.List[str],
                                                build_dir: str) -> T.List[str]:
         new: T.List[str] = []
diff --git a/mesonbuild/compilers/d.py b/mesonbuild/compilers/d.py
index 4df8f65..9f662ad 100644
--- a/mesonbuild/compilers/d.py
+++ b/mesonbuild/compilers/d.py
@@ -5,6 +5,7 @@
 
 import os.path
 import re
+import subprocess
 import typing as T
 
 from .. import mesonlib
@@ -437,11 +438,24 @@
                          full_version=full_version, is_cross=is_cross)
         self.arch = arch
 
-    def _sanity_check_source_code(self) -> str:
-        return 'void main() { }'
+    def sanity_check(self, work_dir: str, environment: 'Environment') -> None:
+        source_name = os.path.join(work_dir, 'sanity.d')
+        output_name = os.path.join(work_dir, 'dtest')
+        with open(source_name, 'w', encoding='utf-8') as ofile:
+            ofile.write('''void main() { }''')
 
-    def _sanity_check_compile_args(self, env: Environment, sourcename: str, binname: str) -> T.List[str]:
-        return self.exelist + self.get_output_args(binname) + self._get_target_arch_args() + [sourcename]
+        compile_cmdlist = self.exelist + self.get_output_args(output_name) + self._get_target_arch_args() + [source_name]
+
+        # If cross-compiling, we can't run the sanity check, only compile it.
+        if self.is_cross and not environment.has_exe_wrapper():
+            compile_cmdlist += self.get_compile_only_args()
+
+        pc = subprocess.Popen(compile_cmdlist, cwd=work_dir)
+        pc.wait()
+        if pc.returncode != 0:
+            raise EnvironmentException('D compiler %s cannot compile programs.' % self.name_string())
+
+        stdo, stde = self.run_sanity_check(environment, [output_name], work_dir)
 
     def needs_static_linker(self) -> bool:
         return True
diff --git a/mesonbuild/compilers/detect.py b/mesonbuild/compilers/detect.py
index 483a584..a0ae810 100644
--- a/mesonbuild/compilers/detect.py
+++ b/mesonbuild/compilers/detect.py
@@ -22,7 +22,6 @@
 import typing as T
 
 if T.TYPE_CHECKING:
-    from .asm import ASMCompiler
     from .compilers import Compiler
     from .c import CCompiler
     from .cpp import CPPCompiler
@@ -1336,20 +1335,20 @@
         if 'NASM' in output:
             comp_class = NasmCompiler
             env.add_lang_args(comp_class.language, comp_class, for_machine)
-            return comp_class([], comp, version, for_machine, info, cc, is_cross=is_cross)
+            return comp_class([], comp, version, for_machine, info, cc.linker, is_cross=is_cross)
         elif 'yasm' in output:
             comp_class = YasmCompiler
             env.add_lang_args(comp_class.language, comp_class, for_machine)
-            return comp_class([], comp, version, for_machine, info, cc, is_cross=is_cross)
+            return comp_class([], comp, version, for_machine, info, cc.linker, is_cross=is_cross)
         elif 'Metrowerks' in output or 'Freescale' in output:
             if 'ARM' in output:
                 comp_class_mwasmarm = MetrowerksAsmCompilerARM
                 env.add_lang_args(comp_class_mwasmarm.language, comp_class_mwasmarm, for_machine)
-                return comp_class_mwasmarm([], comp, version, for_machine, info, cc, is_cross=is_cross)
+                return comp_class_mwasmarm([], comp, version, for_machine, info, cc.linker, is_cross=is_cross)
             else:
                 comp_class_mwasmeppc = MetrowerksAsmCompilerEmbeddedPowerPC
                 env.add_lang_args(comp_class_mwasmeppc.language, comp_class_mwasmeppc, for_machine)
-                return comp_class_mwasmeppc([], comp, version, for_machine, info, cc, is_cross=is_cross)
+                return comp_class_mwasmeppc([], comp, version, for_machine, info, cc.linker, is_cross=is_cross)
 
     _handle_exceptions(popen_exceptions, compilers)
     raise EnvironmentException('Unreachable code (exception to make mypy happy)')
@@ -1365,7 +1364,7 @@
         info = env.machines[for_machine]
 
     from .asm import MasmCompiler, MasmARMCompiler
-    comp_class: T.Type[ASMCompiler]
+    comp_class: T.Type[Compiler]
     if info.cpu_family == 'x86':
         comp = ['ml']
         comp_class = MasmCompiler
@@ -1390,7 +1389,7 @@
         output = Popen_safe(comp + [arg])[2]
         version = search_version(output)
         env.add_lang_args(comp_class.language, comp_class, for_machine)
-        return comp_class([], comp, version, for_machine, info, cc, is_cross=is_cross)
+        return comp_class([], comp, version, for_machine, info, cc.linker, is_cross=is_cross)
     except OSError as e:
         popen_exceptions[' '.join(comp + [arg])] = e
     _handle_exceptions(popen_exceptions, [comp])
diff --git a/mesonbuild/compilers/fortran.py b/mesonbuild/compilers/fortran.py
index 2853ee2..6f4f3d2 100644
--- a/mesonbuild/compilers/fortran.py
+++ b/mesonbuild/compilers/fortran.py
@@ -3,7 +3,6 @@
 
 from __future__ import annotations
 
-import textwrap
 import typing as T
 import functools
 import os
@@ -62,12 +61,10 @@
         largs = env.coredata.get_external_link_args(self.for_machine, self.language)
         return cargs, largs
 
-    def _sanity_check_source_code(self) -> str:
-        return textwrap.dedent('''
-            PROGRAM MAIN
-                PRINT *, "Fortran compilation is working."
-            END
-            ''')
+    def sanity_check(self, work_dir: str, environment: 'Environment') -> None:
+        source_name = 'sanitycheckf.f'
+        code = '      PROGRAM MAIN\n      PRINT *, "Fortran compilation is working."\n      END\n'
+        return self._sanity_check_impl(work_dir, environment, source_name, code)
 
     def get_optimization_args(self, optimization_level: str) -> T.List[str]:
         return gnu_optimization_args[optimization_level]
diff --git a/mesonbuild/compilers/java.py b/mesonbuild/compilers/java.py
index 13e4847..47d2ac9 100644
--- a/mesonbuild/compilers/java.py
+++ b/mesonbuild/compilers/java.py
@@ -6,6 +6,7 @@
 import os
 import os.path
 import shutil
+import subprocess
 import textwrap
 import typing as T
 
@@ -71,32 +72,33 @@
 
         return parameter_list
 
-    def _sanity_check_filenames(self) -> T.Tuple[str, str]:
-        sup = super()._sanity_check_filenames()
-        return sup[0], 'SanityCheck'
-
-    def _sanity_check_compile_args(self, env: Environment, sourcename: str, binname: str) -> T.List[str]:
-        return self.exelist + self.get_always_args() + [sourcename]
-
-    def _sanity_check_run_with_exe_wrapper(self, env: Environment, command: T.List[str]) -> T.List[str]:
+    def sanity_check(self, work_dir: str, environment: 'Environment') -> None:
+        src = 'SanityCheck.java'
+        obj = 'SanityCheck'
+        source_name = os.path.join(work_dir, src)
+        with open(source_name, 'w', encoding='utf-8') as ofile:
+            ofile.write(textwrap.dedent(
+                '''class SanityCheck {
+                  public static void main(String[] args) {
+                    int i;
+                  }
+                }
+                '''))
+        pc = subprocess.Popen(self.exelist + [src], cwd=work_dir)
+        pc.wait()
+        if pc.returncode != 0:
+            raise EnvironmentException(f'Java compiler {self.name_string()} cannot compile programs.')
         runner = shutil.which(self.javarunner)
-        if runner is None:
+        if runner:
+            cmdlist = [runner, '-cp', '.', obj]
+            self.run_sanity_check(environment, cmdlist, work_dir, use_exe_wrapper_for_cross=False)
+        else:
             m = "Java Virtual Machine wasn't found, but it's needed by Meson. " \
                 "Please install a JRE.\nIf you have specific needs where this " \
                 "requirement doesn't make sense, please open a bug at " \
                 "https://github.com/mesonbuild/meson/issues/new and tell us " \
                 "all about it."
             raise EnvironmentException(m)
-        return [runner, '-cp', '.', os.path.basename(command[0])]
-
-    def _sanity_check_source_code(self) -> str:
-        return textwrap.dedent(
-            '''class SanityCheck {
-                public static void main(String[] args) {
-                int i;
-                }
-            }
-            ''')
 
     def needs_static_linker(self) -> bool:
         return False
diff --git a/mesonbuild/compilers/mixins/clike.py b/mesonbuild/compilers/mixins/clike.py
index bd139d2..a492fff 100644
--- a/mesonbuild/compilers/mixins/clike.py
+++ b/mesonbuild/compilers/mixins/clike.py
@@ -268,15 +268,50 @@
     def gen_import_library_args(self, implibname: str) -> T.List[str]:
         return self.linker.import_library_args(implibname)
 
-    def _sanity_check_compile_args(self, env: Environment, sourcename: str, binname: str) -> T.List[str]:
-        # Cross-compiling is hard. For example, you might need -nostdlib, or to pass --target, etc.
-        mode = CompileCheckMode.COMPILE if self.is_cross and not env.has_exe_wrapper() else CompileCheckMode.LINK
-        cargs, largs = self._get_basic_compiler_args(env, mode)
+    def _sanity_check_impl(self, work_dir: str, environment: 'Environment',
+                           sname: str, code: str) -> None:
+        mlog.debug('Sanity testing ' + self.get_display_language() + ' compiler:', mesonlib.join_args(self.exelist))
+        mlog.debug(f'Is cross compiler: {self.is_cross!s}.')
+
+        source_name = os.path.join(work_dir, sname)
+        binname = sname.rsplit('.', 1)[0]
+        mode = CompileCheckMode.LINK
+        if self.is_cross:
+            binname += '_cross'
+            if not environment.has_exe_wrapper():
+                # Linking cross built C/C++ apps is painful. You can't really
+                # tell if you should use -nostdlib or not and for example
+                # on OSX the compiler binary is the same but you need
+                # a ton of compiler flags to differentiate between
+                # arm and x86_64. So just compile.
+                mode = CompileCheckMode.COMPILE
+        cargs, largs = self._get_basic_compiler_args(environment, mode)
         extra_flags = cargs + self.linker_to_compiler_args(largs)
-        # It is important that extra_flags is last as it may contain `/link`
-        # directives, MSVC-compatible compilers will pass all arguments after
-        # that to the linker
-        return self.exelist + [sourcename] + self.get_output_args(binname) + extra_flags
+
+        # Is a valid executable output for all toolchains and platforms
+        binname += '.exe'
+        # Write binary check source
+        binary_name = os.path.join(work_dir, binname)
+        with open(source_name, 'w', encoding='utf-8') as ofile:
+            ofile.write(code)
+        # Compile sanity check
+        # NOTE: extra_flags must be added at the end. On MSVC, it might contain a '/link' argument
+        # after which all further arguments will be passed directly to the linker
+        cmdlist = self.exelist + [sname] + self.get_output_args(binname) + extra_flags
+        pc, stdo, stde = mesonlib.Popen_safe(cmdlist, cwd=work_dir)
+        mlog.debug('Sanity check compiler command line:', mesonlib.join_args(cmdlist))
+        mlog.debug('Sanity check compile stdout:')
+        mlog.debug(stdo)
+        mlog.debug('-----\nSanity check compile stderr:')
+        mlog.debug(stde)
+        mlog.debug('-----')
+        if pc.returncode != 0:
+            raise mesonlib.EnvironmentException(f'Compiler {self.name_string()} cannot compile programs.')
+        self.run_sanity_check(environment, [binary_name], work_dir)
+
+    def sanity_check(self, work_dir: str, environment: 'Environment') -> None:
+        code = 'int main(void) { int class=0; return class; }\n'
+        return self._sanity_check_impl(work_dir, environment, 'sanitycheckc.c', code)
 
     def check_header(self, hname: str, prefix: str, env: 'Environment', *,
                      extra_args: T.Union[None, T.List[str], T.Callable[['CompileCheckMode'], T.List[str]]] = None,
diff --git a/mesonbuild/compilers/objc.py b/mesonbuild/compilers/objc.py
index b6deddd..d013417 100644
--- a/mesonbuild/compilers/objc.py
+++ b/mesonbuild/compilers/objc.py
@@ -48,8 +48,9 @@
     def get_display_language() -> str:
         return 'Objective-C'
 
-    def _sanity_check_source_code(self) -> str:
-        return '#import<stddef.h>\nint main(void) { return 0; }\n'
+    def sanity_check(self, work_dir: str, environment: 'Environment') -> None:
+        code = '#import<stddef.h>\nint main(void) { return 0; }\n'
+        return self._sanity_check_impl(work_dir, environment, 'sanitycheckobjc.m', code)
 
     def form_compileropt_key(self, basename: str) -> OptionKey:
         if basename == 'std':
diff --git a/mesonbuild/compilers/objcpp.py b/mesonbuild/compilers/objcpp.py
index e59d327..441428b 100644
--- a/mesonbuild/compilers/objcpp.py
+++ b/mesonbuild/compilers/objcpp.py
@@ -50,8 +50,9 @@
     def get_display_language() -> str:
         return 'Objective-C++'
 
-    def _sanity_check_source_code(self) -> str:
-        return '#import<stdio.h>\nclass MyClass;int main(void) { return 0; }\n'
+    def sanity_check(self, work_dir: str, environment: 'Environment') -> None:
+        code = '#import<stdio.h>\nclass MyClass;int main(void) { return 0; }\n'
+        return self._sanity_check_impl(work_dir, environment, 'sanitycheckobjcpp.mm', code)
 
     def get_options(self) -> MutableKeyedOptionDictType:
         opts = super().get_options()
diff --git a/mesonbuild/compilers/rust.py b/mesonbuild/compilers/rust.py
index 07faba8..d0f9266 100644
--- a/mesonbuild/compilers/rust.py
+++ b/mesonbuild/compilers/rust.py
@@ -107,43 +107,44 @@
     def needs_static_linker(self) -> bool:
         return False
 
-    def _sanity_check_compile_args(self, env: Environment, sourcename: str, binname: str) -> T.List[str]:
-        cmdlist = self.exelist.copy()
-        assert self.linker is not None, 'for mypy'
-        if self.info.kernel == 'none' and 'ld.' in self.linker.id:
-            cmdlist.extend(['-C', 'link-arg=-nostartfiles'])
-        cmdlist.extend(self.get_output_args(binname))
-        cmdlist.append(sourcename)
-        return cmdlist
-
-    def _sanity_check_source_code(self) -> str:
-        if self.info.kernel != 'none':
-            return textwrap.dedent(
-                '''fn main() {
-                }
-                ''')
-        return textwrap.dedent(
-            '''#![no_std]
-            #![no_main]
-            #[no_mangle]
-            pub fn _start() {
-            }
-            #[panic_handler]
-            fn panic(_info: &core::panic::PanicInfo) -> ! {
-                loop {}
-            }
-            ''')
-
     def sanity_check(self, work_dir: str, environment: Environment) -> None:
-        super().sanity_check(work_dir, environment)
-        source_name = self._sanity_check_filenames()[0]
+        source_name = os.path.join(work_dir, 'sanity.rs')
+        output_name = os.path.join(work_dir, 'rusttest.exe')
+        cmdlist = self.exelist.copy()
+
+        with open(source_name, 'w', encoding='utf-8') as ofile:
+            # If machine kernel is not `none`, try to compile a dummy program.
+            # If 'none', this is likely a `no-std`(i.e. bare metal) project.
+            if self.info.kernel != 'none':
+                ofile.write(textwrap.dedent(
+                    '''fn main() {
+                    }
+                    '''))
+            else:
+                # If rustc linker is gcc, add `-nostartfiles`
+                if 'ld.' in self.linker.id:
+                    cmdlist.extend(['-C', 'link-arg=-nostartfiles'])
+                ofile.write(textwrap.dedent(
+                    '''#![no_std]
+                    #![no_main]
+                    #[no_mangle]
+                    pub fn _start() {
+                    }
+                    #[panic_handler]
+                    fn panic(_info: &core::panic::PanicInfo) -> ! {
+                        loop {}
+                    }
+                    '''))
+
+        cmdlist.extend(['-o', output_name, source_name])
+        pc, stdo, stde = Popen_safe_logged(cmdlist, cwd=work_dir)
+        if pc.returncode != 0:
+            raise EnvironmentException(f'Rust compiler {self.name_string()} cannot compile programs.')
         self._native_static_libs(work_dir, source_name)
+        self.run_sanity_check(environment, [output_name], work_dir)
 
     def _native_static_libs(self, work_dir: str, source_name: str) -> None:
         # Get libraries needed to link with a Rust staticlib
-        if self.native_static_libs:
-            return
-
         cmdlist = self.exelist + ['--crate-type', 'staticlib', '--print', 'native-static-libs', source_name]
         p, stdo, stde = Popen_safe_logged(cmdlist, cwd=work_dir)
         if p.returncode != 0:
diff --git a/mesonbuild/compilers/swift.py b/mesonbuild/compilers/swift.py
index 94e565a..4ad3aff 100644
--- a/mesonbuild/compilers/swift.py
+++ b/mesonbuild/compilers/swift.py
@@ -177,21 +177,22 @@
 
         return parameter_list
 
-    def _sanity_check_compile_args(self, env: Environment, sourcename: str, binname: str) -> T.List[str]:
-        cmdlist = self.exelist.copy()
-        # TODO: I can't test this, but it doesn't seem right
+    def sanity_check(self, work_dir: str, environment: 'Environment') -> None:
+        src = 'swifttest.swift'
+        source_name = os.path.join(work_dir, src)
+        output_name = os.path.join(work_dir, 'swifttest')
+        extra_flags: T.List[str] = []
+        extra_flags += environment.coredata.get_external_args(self.for_machine, self.language)
         if self.is_cross:
-            cmdlist.extend(self.get_compile_only_args())
+            extra_flags += self.get_compile_only_args()
         else:
-            cmdlist.extend(env.coredata.get_external_link_args(self.for_machine, self.language))
-        cmdlist.extend(self.get_std_exe_link_args())
-        cmdlist.extend(self.get_output_args(binname))
-        cmdlist.append(sourcename)
-
-        return cmdlist
-
-    def _sanity_check_source_code(self) -> str:
-        return 'print("Swift compilation is working.")'
+            extra_flags += environment.coredata.get_external_link_args(self.for_machine, self.language)
+        with open(source_name, 'w', encoding='utf-8') as ofile:
+            ofile.write('''print("Swift compilation is working.")
+''')
+        pc = subprocess.Popen(self.exelist + extra_flags + ['-emit-executable', '-o', output_name, src], cwd=work_dir)
+        pc.wait()
+        self.run_sanity_check(environment, [output_name], work_dir)
 
     def get_debug_args(self, is_debug: bool) -> T.List[str]:
         return clike_debug_args[is_debug]
diff --git a/mesonbuild/compilers/vala.py b/mesonbuild/compilers/vala.py
index 230a773..bbaefed 100644
--- a/mesonbuild/compilers/vala.py
+++ b/mesonbuild/compilers/vala.py
@@ -8,7 +8,7 @@
 
 from .. import mlog
 from .. import mesonlib
-from ..mesonlib import version_compare, LibType
+from ..mesonlib import EnvironmentException, version_compare, LibType
 from ..options import OptionKey
 from .compilers import CompileCheckMode, Compiler
 
@@ -99,25 +99,18 @@
 
         return parameter_list
 
-    def _sanity_check_source_code(self) -> str:
-        return 'public static int main() { return 0; }'
-
-    def _sanity_check_compile_args(self, env: Environment, sourcename: str, binname: str) -> T.List[str]:
-        cmdlist = self.exelist.copy()
-        cmdlist.extend(env.coredata.get_external_args(self.for_machine, self.language))
+    def sanity_check(self, work_dir: str, environment: 'Environment') -> None:
+        code = 'class MesonSanityCheck : Object { }'
+        extra_flags: T.List[str] = []
+        extra_flags += environment.coredata.get_external_args(self.for_machine, self.language)
         if self.is_cross:
-            cmdlist.extend(self.get_compile_only_args())
+            extra_flags += self.get_compile_only_args()
         else:
-            cmdlist.extend(env.coredata.get_external_link_args(self.for_machine, self.language))
-        cmdlist.extend(self.get_output_args(binname))
-        cmdlist.append(sourcename)
-        return cmdlist
-
-    def _run_sanity_check(self, env: Environment, cmdlist: T.List[str], work_dir: str) -> None:
-        # Vala will do a Vala -> C -> Exe, so the output file will actually have
-        # the name of the C compiler.
-        # TODO: find a way to not make this so hacky
-        return super()._run_sanity_check(env, [os.path.join(work_dir, 'sanity_check_for_c.exe')], work_dir)
+            extra_flags += environment.coredata.get_external_link_args(self.for_machine, self.language)
+        with self.cached_compile(code, environment.coredata, extra_args=extra_flags, mode=CompileCheckMode.COMPILE) as p:
+            if p.returncode != 0:
+                msg = f'Vala compiler {self.name_string()!r} cannot compile programs'
+                raise EnvironmentException(msg)
 
     def find_library(self, libname: str, env: 'Environment', extra_dirs: T.List[str],
                      libtype: LibType = LibType.PREFER_SHARED, lib_prefix_warning: bool = True, ignore_system_dirs: bool = False) -> T.Optional[T.List[str]]:
diff --git a/mesonbuild/dependencies/base.py b/mesonbuild/dependencies/base.py
index b746fa8..72dbfdf 100644
--- a/mesonbuild/dependencies/base.py
+++ b/mesonbuild/dependencies/base.py
@@ -55,11 +55,8 @@
         def get_output_args(self, outputname: str) -> T.List[str]:
             return []
 
-        def _sanity_check_compile_args(self, env: Environment, sourcename: str, binname: str) -> T.List[str]:
-            return []
-
-        def _sanity_check_source_code(self) -> str:
-            return ''
+        def sanity_check(self, work_dir: str, environment: 'Environment') -> None:
+            return None
 
     def __getattr__(self, item: str) -> T.Any:
         if item.startswith('__'):