From 4f041e4ff86af096146526cf617c5f81822ca69d Mon Sep 17 00:00:00 2001 From: Dylan Baker Date: Thu, 20 Nov 2025 09:57:04 -0800 Subject: [PATCH 1/3] tests: fix test_templates for vala Vala really needs a known working C compiler, and once we fix its sanity_check, it will fail without one, so ensure it's there. --- unittests/allplatformstests.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/unittests/allplatformstests.py b/unittests/allplatformstests.py index 1304658daeb9..1a078960a73d 100644 --- a/unittests/allplatformstests.py +++ b/unittests/allplatformstests.py @@ -2508,9 +2508,9 @@ def test_templates(self): if ninja is None: raise SkipTest('This test currently requires ninja. Fix this once "meson build" works.') - langs = ['c'] + langs = [] env = get_fake_env() - for l in ['cpp', 'cs', 'cuda', 'd', 'fortran', 'java', 'objc', 'objcpp', 'rust', 'vala']: + for l in ['c', 'cpp', 'cs', 'cuda', 'd', 'fortran', 'java', 'objc', 'objcpp', 'rust', 'vala']: try: comp = detect_compiler_for(env, l, MachineChoice.HOST, True, '') with tempfile.TemporaryDirectory() as d: From 3784d7c86842fae46153ec92efdd2b3e6a7e9eec Mon Sep 17 00:00:00 2001 From: Dylan Baker Date: Wed, 19 Nov 2025 11:58:57 -0800 Subject: [PATCH 2/3] compilers: refactor sanity checking code, again The goal is to reduce code duplication, and allow each language to implement as little as possible to get good checking. The main motivation is that half of the checks are fragile, as they add the work directory to the paths of the generated files they want to use. This works when run inside mesonmain because we always have an absolute build directory, but when put into run_project_tests.py it doesn't work because that gives a relative build directory. Additionally, this fixes the implementation of sanity checking for transpiled languages like Vala and Cython, which previously didn't test the output of their compilers at all, but re-ran the C compiler test for itself. --- mesonbuild/compilers/asm.py | 6 +- mesonbuild/compilers/c.py | 5 +- mesonbuild/compilers/compilers.py | 147 +++++++++++++++++++++++++-- mesonbuild/compilers/cpp.py | 5 +- mesonbuild/compilers/cs.py | 31 ++---- mesonbuild/compilers/cuda.py | 115 +++++++++------------ mesonbuild/compilers/cython.py | 61 +++++++++-- mesonbuild/compilers/d.py | 25 ++--- mesonbuild/compilers/fortran.py | 11 +- mesonbuild/compilers/java.py | 46 +++++---- mesonbuild/compilers/mixins/clike.py | 51 ++-------- mesonbuild/compilers/objc.py | 5 +- mesonbuild/compilers/objcpp.py | 5 +- mesonbuild/compilers/rust.py | 65 ++++++------ mesonbuild/compilers/swift.py | 31 +++--- mesonbuild/compilers/vala.py | 50 ++++++--- mesonbuild/dependencies/base.py | 4 +- 17 files changed, 397 insertions(+), 266 deletions(-) diff --git a/mesonbuild/compilers/asm.py b/mesonbuild/compilers/asm.py index c298933a1861..ff4825beec12 100644 --- a/mesonbuild/compilers/asm.py +++ b/mesonbuild/compilers/asm.py @@ -11,9 +11,9 @@ from .mixins.ti import TICompiler if T.TYPE_CHECKING: + from ..environment import Environment from ..linkers.linkers import DynamicLinker from ..mesonlib import MachineChoice - from ..environment import Environment nasm_optimization_args: T.Dict[str, T.List[str]] = { 'plain': [], @@ -44,6 +44,10 @@ def __init__(self, ccache: T.List[str], exelist: T.List[str], version: str, def sanity_check(self, work_dir: str) -> None: return None + def _sanity_check_source_code(self) -> str: + # TODO: Stub implementation to be replaced in future patch + return '' + class NasmCompiler(ASMCompiler): language = 'nasm' diff --git a/mesonbuild/compilers/c.py b/mesonbuild/compilers/c.py index 7143d9e92d7a..4d05ab9c4a8b 100644 --- a/mesonbuild/compilers/c.py +++ b/mesonbuild/compilers/c.py @@ -74,9 +74,8 @@ def __init__(self, ccache: T.List[str], exelist: T.List[str], version: str, for_ def get_no_stdinc_args(self) -> T.List[str]: return ['-nostdinc'] - def sanity_check(self, work_dir: str) -> None: - code = 'int main(void) { int class=0; return class; }\n' - return self._sanity_check_impl(work_dir, 'sanitycheckc.c', code) + def _sanity_check_source_code(self) -> str: + return 'int main(void) { int class=0; return class; }\n' def has_header_symbol(self, hname: str, symbol: str, prefix: str, *, extra_args: T.Union[None, T.List[str], T.Callable[['CompileCheckMode'], T.List[str]]] = None, diff --git a/mesonbuild/compilers/compilers.py b/mesonbuild/compilers/compilers.py index 2d813eb9b1a5..dc2c598849ec 100644 --- a/mesonbuild/compilers/compilers.py +++ b/mesonbuild/compilers/compilers.py @@ -1210,7 +1210,6 @@ def get_has_func_attribute_extra_args(self, name: str) -> T.List[str]: def name_string(self) -> str: return ' '.join(self.exelist) - @abc.abstractmethod def sanity_check(self, work_dir: str) -> None: """Check that this compiler actually works. @@ -1219,24 +1218,152 @@ def sanity_check(self, work_dir: str) -> None: main(): return 0 ``` is good enough here. + + :param work_dir: A directory to put temporary artifacts + :raises mesonlib.EnvironmentException: If building the binary fails + :raises mesonlib.EnvironmentException: If running the binary is attempted and fails """ + sourcename, transpiled, binname = self._sanity_check_filenames() + + with open(os.path.join(work_dir, sourcename), 'w', encoding='utf-8') as f: + f.write(self._sanity_check_source_code()) + + if transpiled: + cmdlist, linker_args = self._sanity_check_compile_args(sourcename, transpiled) + cmdlist.extend(linker_args) + pc, stdo, stde = mesonlib.Popen_safe(cmdlist, cwd=work_dir) + mlog.debug('Sanity check transpiler command line:', mesonlib.join_args(cmdlist)) + mlog.debug('Sanity check transpiler stdout:') + mlog.debug(stdo) + mlog.debug('-----\nSanity check transpiler stderr:') + mlog.debug(stde) + mlog.debug('-----') + if pc.returncode != 0: + raise mesonlib.EnvironmentException(f'Compiler {self.name_string()} cannot transpile programs.') + + comp_lang = SUFFIX_TO_LANG[transpiled.rsplit('.', maxsplit=1)[1]] + comp = self.environment.coredata.compilers[self.for_machine].get(comp_lang) + if not comp: + raise mesonlib.MesonBugException(f'Need a {comp_lang} compiler for {self.language} compiler test, but one doesnt exist') + + cmdlist, linker_args = self._transpiled_sanity_check_compile_args(comp, transpiled, binname) + + mlog.debug('checking compilation of transpiled source:') + else: + cmdlist, linker_args = self._sanity_check_compile_args(sourcename, binname) + + cmdlist.extend(linker_args) + 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([os.path.join(work_dir, binname)], work_dir) + + def _sanity_check_filenames(self) -> T.Tuple[str, T.Optional[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. + + The return value consists of a source name, a transpiled source name (if + there is one), and the final binary name. + + :return: A tuple of (sourcename, transpiled, binaryname) + """ + default_ext = lang_suffixes[self.language][0] + template = f'sanity_check_for_{self.language}' + sourcename = f'{template}.{default_ext}' + cross_or_not = "_cross" if self.is_cross else "" + binaryname = f'{template}{cross_or_not}.exe' + return sourcename, None, binaryname + + def _transpiled_sanity_check_compile_args( + self, compiler: Compiler, sourcename: str, binname: str + ) -> T.Tuple[T.List[str], T.List[str]]: + """Get arguments to run compiler for sanity check. + + By default this will just return the compile arguments for the compiler in question. + + Linker arguments are separated from compiler arguments because some + compilers do not allow linker and compiler arguments to be mixed together + + Overriding this is useful when needing to change the kind of output + produced, or adding extra arguments. + + :param compiler: The :class:`Compiler` that is used for compiling the transpiled sources + :param sourcename: the name of the source file to generate + :param binname: the name of the binary file to generate + :return: a tuple of arguments, the first is the executable and compiler + arguments, the second is linker arguments + """ + return compiler._sanity_check_compile_args(sourcename, binname) + + def _sanity_check_compile_args(self, sourcename: str, binname: str + ) -> T.Tuple[T.List[str], T.List[str]]: + """Get arguments to run compiler for sanity check. + + Linker arguments are separated from compiler arguments because some + compilers do not allow linker and compiler arguments to be mixed together - def run_sanity_check(self, 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 self.environment.has_exe_wrapper(): - # Can't check if the binaries run so we have to assume they do - return ('', '') - cmdlist = self.environment.exe_wrapper.get_command() + cmdlist - mlog.debug('Running test binary command: ', mesonlib.join_args(cmdlist)) + :param sourcename: the name of the source file to generate + :param binname: the name of the binary file to generate + :return: a tuple of arguments, the first is the executable and compiler + arguments, the second is linker arguments + """ + return self.exelist_no_ccache + self.get_always_args() + self.get_output_args(binname) + [sourcename], [] + + @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, 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 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 self.environment.has_exe_wrapper(): + assert self.environment.exe_wrapper is not None, 'for mypy' + return self.environment.exe_wrapper.get_command() + command + return command + + def _run_sanity_check(self, cmdlist: T.List[str], work_dir: str) -> None: + """Run a sanity test binary + + :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 self.environment.has_exe_wrapper(): + mlog.debug('Cannot run cross check') + return + + cmdlist = self._sanity_check_run_with_exe_wrapper(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)) 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 bdf60f6c23a2..8d8c0cd42017 100644 --- a/mesonbuild/compilers/cpp.py +++ b/mesonbuild/compilers/cpp.py @@ -84,9 +84,8 @@ def get_no_stdinc_args(self) -> T.List[str]: def get_no_stdlib_link_args(self) -> T.List[str]: return ['-nostdlib++'] - def sanity_check(self, work_dir: str) -> None: - code = 'class breakCCompiler;int main(void) { return 0; }\n' - return self._sanity_check_impl(work_dir, 'sanitycheckcpp.cc', code) + def _sanity_check_source_code(self) -> str: + return 'class breakCCompiler;int main(void) { return 0; }\n' 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 cacd9ee47d41..863e240897c4 100644 --- a/mesonbuild/compilers/cs.py +++ b/mesonbuild/compilers/cs.py @@ -3,11 +3,10 @@ from __future__ import annotations -import os.path, subprocess +import os.path import textwrap import typing as T -from ..mesonlib import EnvironmentException from ..linkers import RSPFileSyntax from .compilers import Compiler @@ -82,26 +81,18 @@ def get_pch_use_args(self, pch_dir: str, header: str) -> T.List[str]: def get_pch_name(self, header_name: str) -> str: return '' - def sanity_check(self, work_dir: str) -> 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_source_code(self) -> str: + return textwrap.dedent(''' + public class Sanity { + static public void Main () { } - ''')) - 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()) + } + ''') + + def _sanity_check_run_with_exe_wrapper(self, command: T.List[str]) -> T.List[str]: if self.runner: - cmdlist = [self.runner, obj] - else: - cmdlist = [os.path.join(work_dir, obj)] - self.run_sanity_check(cmdlist, work_dir, use_exe_wrapper_for_cross=False) + return [self.runner] + command + return command def needs_static_linker(self) -> bool: return False diff --git a/mesonbuild/compilers/cuda.py b/mesonbuild/compilers/cuda.py index cf0fceca5340..e2defd62bbce 100644 --- a/mesonbuild/compilers/cuda.py +++ b/mesonbuild/compilers/cuda.py @@ -5,15 +5,14 @@ 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, Popen_safe, - is_windows, LibType, version_compare + EnvironmentException, is_windows, LibType, version_compare ) from .compilers import Compiler, CompileCheckMode @@ -185,6 +184,7 @@ def __init__(self, ccache: T.List[str], exelist: T.List[str], version: str, for_ host_compiler: Compiler, env: Environment, linker: T.Optional['DynamicLinker'] = None, full_version: T.Optional[str] = None): + self.detected_cc = '' super().__init__(ccache, exelist, version, for_machine, env, linker=linker, full_version=full_version) self.host_compiler = host_compiler self.base_options = host_compiler.base_options @@ -497,55 +497,37 @@ def needs_static_linker(self) -> bool: def thread_link_flags(self) -> T.List[str]: return self._to_host_flags(self.host_compiler.thread_link_flags(), Phase.LINKER) - def sanity_check(self, work_dir: str) -> None: - mlog.debug('Sanity testing ' + self.get_display_language() + ' compiler:', ' '.join(self.exelist)) - mlog.debug('Is cross compiler: %s.' % str(self.is_cross)) - - sname = 'sanitycheckcuda.cu' - code = r''' - #include - #include - - __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 str: + return r''' + #include + #include + + __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 T.Tuple[T.List[str], T.List[str]]: # Disable warnings, compile with statically-linked runtime for minimum # reliance on the system. - flags += ['-w', '-cudart', 'static', source_name] + flags = ['-w', '-cudart', 'static', sourcename] # Use the -ccbin option, if available, even during sanity checking. # Otherwise, on systems where CUDA does not support the default compiler, @@ -560,33 +542,30 @@ def sanity_check(self, work_dir: str) -> None: # 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(binary_name) - - # 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.') - - # Run sanity check (if possible) - if self.is_cross: + flags += self.get_output_args(binname) + + return self.exelist + flags, [] + + def _run_sanity_check(self, 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 self.environment.has_exe_wrapper(): + mlog.debug('Cannot run cross check') return - cmdlist = self.exelist + ['--run', f'"{binary_name}"'] + cmdlist = self._sanity_check_run_with_exe_wrapper(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)) try: - stdo, stde = self.run_sanity_check(cmdlist, work_dir) - except EnvironmentException: + 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: 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 b33147e34368..9bc14c10c1fd 100644 --- a/mesonbuild/compilers/cython.py +++ b/mesonbuild/compilers/cython.py @@ -1,13 +1,15 @@ # 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 EnvironmentException, version_compare +from .. import mlog +from ..mesonlib import version_compare, EnvironmentException from .compilers import Compiler if T.TYPE_CHECKING: @@ -48,16 +50,59 @@ def get_dependency_gen_args(self, outtarget: str, outfile: str) -> T.List[str]: def get_depfile_suffix(self) -> str: return 'dep' - def sanity_check(self, work_dir: str) -> None: - code = 'print("hello world")' - with self.cached_compile(code) 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_filenames(self) -> T.Tuple[str, T.Optional[str], str]: + sourcename, _, binname = super()._sanity_check_filenames() + + lang = self.get_compileropt_value('language', None) + assert isinstance(lang, str) + + # This is almost certainly not good enough + ext = 'dll' if self.environment.machines[self.for_machine].is_windows() else 'so' + + return (sourcename, f'{os.path.splitext(sourcename)[0]}.{lang}', + f'{os.path.splitext(binname)[0]}.{ext}') + + def _transpiled_sanity_check_compile_args( + self, compiler: Compiler, sourcename: str, binname: str + ) -> T.Tuple[T.List[str], T.List[str]]: + version = self.get_compileropt_value('version', None) + assert isinstance(version, str) + + from ..dependencies import find_external_dependency + with mlog.no_logging(): + dep = find_external_dependency(f'python{version}', self.environment, {'required': False}) + if not dep.found(): + raise EnvironmentException( + 'Cython requires python3 dependency for link testing, but it could not be found') + + args, largs = super()._transpiled_sanity_check_compile_args(compiler, sourcename, binname) + args.extend(compiler.get_pic_args()) + args.extend(dep.get_all_compile_args()) + + largs.extend(dep.get_all_link_args()) + largs.extend(compiler.get_std_shared_lib_link_args()) + largs.extend(compiler.get_allow_undefined_link_args()) + return args, largs + + def _sanity_check_compile_args(self, sourcename: str, binname: str + ) -> T.Tuple[T.List[str], T.List[str]]: + args, largs = super()._sanity_check_compile_args(sourcename, binname) + args.extend(self.get_option_compile_args(None)) + return args, largs + + def _sanity_check_source_code(self) -> str: + return 'def func():\n print("Hello world")' + + def _run_sanity_check(self, cmdlist: T.List[str], work_dir: str) -> None: + # XXX: this is a punt + # This means we transpile the Cython .pyx file into C or C++, and we + # link it, but we don't actually attempt to run it. + return + 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 dd120947dd52..bfeac85b8e02 100644 --- a/mesonbuild/compilers/d.py +++ b/mesonbuild/compilers/d.py @@ -5,7 +5,6 @@ import os.path import re -import subprocess import typing as T from .. import mesonlib @@ -436,24 +435,14 @@ def __init__(self, exelist: T.List[str], version: str, for_machine: MachineChoic full_version=full_version) self.arch = arch - def sanity_check(self, work_dir: str) -> 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_source_code(self) -> str: + return 'void main() { }' - 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 self.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([output_name], work_dir) + def _sanity_check_compile_args(self, sourcename: str, binname: str + ) -> T.Tuple[T.List[str], T.List[str]]: + args, largs = super()._sanity_check_compile_args(sourcename, binname) + largs.extend(self._get_target_arch_args()) + return args, largs def needs_static_linker(self) -> bool: return True diff --git a/mesonbuild/compilers/fortran.py b/mesonbuild/compilers/fortran.py index 7654b3ffab49..3e14123d967d 100644 --- a/mesonbuild/compilers/fortran.py +++ b/mesonbuild/compilers/fortran.py @@ -3,6 +3,7 @@ from __future__ import annotations +import textwrap import typing as T import functools import os @@ -59,10 +60,12 @@ def _get_basic_compiler_args(self, mode: CompileCheckMode) -> T.Tuple[T.List[str largs = self.environment.coredata.get_external_link_args(self.for_machine, self.language) return cargs, largs - def sanity_check(self, work_dir: str) -> None: - source_name = 'sanitycheckf.f' - code = ' PROGRAM MAIN\n PRINT *, "Fortran compilation is working."\n END\n' - return self._sanity_check_impl(work_dir, source_name, code) + def _sanity_check_source_code(self) -> str: + return textwrap.dedent(''' + PROGRAM MAIN + PRINT *, "Fortran compilation is working." + END + ''') 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 f60bc6bbb50a..8f9671f89cac 100644 --- a/mesonbuild/compilers/java.py +++ b/mesonbuild/compilers/java.py @@ -6,7 +6,6 @@ import os import os.path import shutil -import subprocess import textwrap import typing as T @@ -71,33 +70,38 @@ def compute_parameters_with_absolute_paths(self, parameter_list: T.List[str], return parameter_list - def sanity_check(self, work_dir: str) -> 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.') + def _sanity_check_filenames(self) -> T.Tuple[str, T.Optional[str], str]: + sup = super()._sanity_check_filenames() + return sup[0], None, 'SanityCheck' + + def _sanity_check_run_with_exe_wrapper(self, command: T.List[str]) -> T.List[str]: runner = shutil.which(self.javarunner) - if runner: - cmdlist = [runner, '-cp', '.', obj] - self.run_sanity_check(cmdlist, work_dir, use_exe_wrapper_for_cross=False) - else: + if runner is None: 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) + basedir = os.path.basename(command[0]) + return [runner, '-cp', basedir, basedir] + + def _sanity_check_source_code(self) -> str: + return textwrap.dedent( + '''class SanityCheck { + public static void main(String[] args) { + int i; + } + } + ''') + + def sanity_check(self, work_dir: str) -> None: + # Older versions of Java (At least 1.8), don't create this directory and + # error when it doesn't exist. Newer versions (11 at least), doesn't have + # this issue. + fname = self._sanity_check_filenames()[2] + os.makedirs(os.path.join(work_dir, fname), exist_ok=True) + return super().sanity_check(work_dir) def needs_static_linker(self) -> bool: return False diff --git a/mesonbuild/compilers/mixins/clike.py b/mesonbuild/compilers/mixins/clike.py index e6637cb1c525..1409f9a8fb74 100644 --- a/mesonbuild/compilers/mixins/clike.py +++ b/mesonbuild/compilers/mixins/clike.py @@ -266,49 +266,14 @@ def gen_export_dynamic_link_args(self) -> T.List[str]: def gen_import_library_args(self, implibname: str) -> T.List[str]: return self.linker.import_library_args(implibname) - def _sanity_check_impl(self, work_dir: str, 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 self.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(mode) - extra_flags = cargs + self.linker_to_compiler_args(largs) - - # 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([binary_name], work_dir) - - def sanity_check(self, work_dir: str) -> None: - code = 'int main(void) { int class=0; return class; }\n' - return self._sanity_check_impl(work_dir, 'sanitycheckc.c', code) + def _sanity_check_compile_args(self, sourcename: str, binname: str + ) -> T.Tuple[T.List[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 self.environment.has_exe_wrapper() else CompileCheckMode.LINK + cargs, b_largs = self._get_basic_compiler_args(mode) + largs = self.linker_to_compiler_args(b_largs) + s_args, s_largs = super()._sanity_check_compile_args(sourcename, binname) + return s_args + cargs, s_largs + largs def check_header(self, hname: str, prefix: str, *, 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 d62113b1bda5..7b196ccb9bb7 100644 --- a/mesonbuild/compilers/objc.py +++ b/mesonbuild/compilers/objc.py @@ -47,9 +47,8 @@ def get_options(self) -> MutableKeyedOptionDictType: def get_display_language() -> str: return 'Objective-C' - def sanity_check(self, work_dir: str) -> None: - code = '#import\nint main(void) { return 0; }\n' - return self._sanity_check_impl(work_dir, 'sanitycheckobjc.m', code) + def _sanity_check_source_code(self) -> str: + return '#import\nint main(void) { return 0; }\n' def form_compileropt_key(self, basename: str) -> OptionKey: if basename == 'std': diff --git a/mesonbuild/compilers/objcpp.py b/mesonbuild/compilers/objcpp.py index 927ca7e0b62c..ad047d78626b 100644 --- a/mesonbuild/compilers/objcpp.py +++ b/mesonbuild/compilers/objcpp.py @@ -48,9 +48,8 @@ def make_option_name(self, key: OptionKey) -> str: def get_display_language() -> str: return 'Objective-C++' - def sanity_check(self, work_dir: str) -> None: - code = '#import\nclass MyClass;int main(void) { return 0; }\n' - return self._sanity_check_impl(work_dir, 'sanitycheckobjcpp.mm', code) + def _sanity_check_source_code(self) -> str: + return '#import\nclass MyClass;int main(void) { return 0; }\n' def get_options(self) -> MutableKeyedOptionDictType: opts = super().get_options() diff --git a/mesonbuild/compilers/rust.py b/mesonbuild/compilers/rust.py index ab0706d26554..0d2ecc9d5e5d 100644 --- a/mesonbuild/compilers/rust.py +++ b/mesonbuild/compilers/rust.py @@ -122,44 +122,45 @@ def init_from_options(self) -> None: def needs_static_linker(self) -> bool: return False - def sanity_check(self, work_dir: str) -> None: - source_name = os.path.join(work_dir, 'sanity.rs') - output_name = os.path.join(work_dir, 'rusttest.exe') + def _sanity_check_compile_args(self, sourcename: str, binname: str + ) -> T.Tuple[T.List[str], T.List[str]]: cmdlist = self.exelist.copy() + largs: T.List[str] = [] + assert self.linker is not None, 'for mypy' + if self.info.kernel == 'none' and 'ld.' in self.linker.id: + largs.extend(rustc_link_args(['-nostartfiles'])) + cmdlist.extend(self.get_output_args(binname)) + cmdlist.append(sourcename) + return cmdlist, largs + + 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 {} + } + ''') - 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.') + def sanity_check(self, work_dir: str) -> None: + super().sanity_check(work_dir) + source_name = self._sanity_check_filenames()[0] self._native_static_libs(work_dir, source_name) - self.run_sanity_check([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 d5f87a23649f..fad6912261ae 100644 --- a/mesonbuild/compilers/swift.py +++ b/mesonbuild/compilers/swift.py @@ -175,22 +175,25 @@ def compute_parameters_with_absolute_paths(self, parameter_list: T.List[str], return parameter_list - def sanity_check(self, work_dir: str) -> 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 += self.environment.coredata.get_external_args(self.for_machine, self.language) + def _sanity_check_compile_args(self, sourcename: str, binname: str + ) -> T.Tuple[T.List[str], T.List[str]]: + args = self.exelist.copy() + largs: T.List[str] = [] + + # TODO: I can't test this, but it doesn't seem right if self.is_cross: - extra_flags += self.get_compile_only_args() + args.extend(self.get_compile_only_args()) else: - extra_flags += self.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([output_name], work_dir) + largs.extend(self.environment.coredata.get_external_link_args(self.for_machine, self.language)) + args.extend(self.get_output_args(binname)) + args.append(sourcename) + + largs.extend(self.get_std_exe_link_args()) + + return args, largs + + def _sanity_check_source_code(self) -> str: + return 'print("Swift compilation is working.")' 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 575cf400623b..53985c92a9d9 100644 --- a/mesonbuild/compilers/vala.py +++ b/mesonbuild/compilers/vala.py @@ -8,7 +8,7 @@ from .. import mlog from .. import mesonlib -from ..mesonlib import EnvironmentException, version_compare, LibType +from ..mesonlib import version_compare, LibType from ..options import OptionKey from .compilers import CompileCheckMode, Compiler @@ -31,6 +31,7 @@ def __init__(self, exelist: T.List[str], version: str, for_machine: MachineChoic self.base_options = {OptionKey('b_colorout')} self.force_link = False self._has_color_support = version_compare(self.version, '>=0.37.1') + self._has_posix_profile = version_compare(self.version, '>= 0.44') def needs_static_linker(self) -> bool: return False # Because compiles into C. @@ -106,18 +107,41 @@ def compute_parameters_with_absolute_paths(self, parameter_list: T.List[str], return parameter_list - def sanity_check(self, work_dir: str) -> None: - code = 'class MesonSanityCheck : Object { }' - extra_flags: T.List[str] = [] - extra_flags += self.environment.coredata.get_external_args(self.for_machine, self.language) - if self.is_cross: - extra_flags += self.get_compile_only_args() - else: - extra_flags += self.environment.coredata.get_external_link_args(self.for_machine, self.language) - with self.cached_compile(code, 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 _sanity_check_source_code(self) -> str: + return 'public static int main() { return 0; }' + + def _sanity_check_compile_args(self, sourcename: str, binname: str + ) -> T.Tuple[T.List[str], T.List[str]]: + args, largs = super()._sanity_check_compile_args(sourcename, binname) + if self._has_posix_profile: + # This removes the glib requirement. Posix and libc are equivalent, + # but posix is available in older versions of valac + args.append('--profile=posix') + return args, largs + + def _transpiled_sanity_check_compile_args( + self, compiler: Compiler, sourcename: str, binname: str + ) -> T.Tuple[T.List[str], T.List[str]]: + args, largs = super()._transpiled_sanity_check_compile_args(compiler, sourcename, binname) + if self._has_posix_profile: + return args, largs + + # If valac is too old for the posix profile then we need to find goobject-2.0 for linking. + from ..dependencies import find_external_dependency + with mlog.no_logging(): + dep = find_external_dependency('gobject-2.0', self.environment, + {'required': False, 'native': self.for_machine}) + if not dep.found(): + raise mesonlib.EnvironmentException( + 'Valac < 0.44 requires gobject-2.0 for link testing, bit it could not be found.') + + args.extend(dep.get_all_compile_args()) + largs.extend(dep.get_all_link_args()) + return args, largs + + def _sanity_check_filenames(self) -> T.Tuple[str, T.Optional[str], str]: + sourcename, _, binname = super()._sanity_check_filenames() + return sourcename, f'{os.path.splitext(sourcename)[0]}.c', binname def find_library(self, libname: str, 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 4536746d940f..7fe8d427272d 100644 --- a/mesonbuild/dependencies/base.py +++ b/mesonbuild/dependencies/base.py @@ -93,8 +93,8 @@ def get_optimization_args(self, optimization_level: str) -> T.List[str]: def get_output_args(self, outputname: str) -> T.List[str]: return [] - def sanity_check(self, work_dir: str) -> None: - return None + def _sanity_check_source_code(self) -> str: + return '' def __getattr__(self, item: str) -> T.Any: if item.startswith('__'): From 1db1c4ce71dae0dbcb549aeb346c5eba58f8c42d Mon Sep 17 00:00:00 2001 From: Dylan Baker Date: Wed, 19 Nov 2025 12:05:02 -0800 Subject: [PATCH 3/3] compilers/cuda: Use the `init_from_options` hook for version detection Instead of doing this in the sanity checking code. Apart from being more "correct", this allows us to remove the custom sanity_checking implementation. --- mesonbuild/compilers/cuda.py | 40 ++++++++++-------------------------- 1 file changed, 11 insertions(+), 29 deletions(-) diff --git a/mesonbuild/compilers/cuda.py b/mesonbuild/compilers/cuda.py index e2defd62bbce..70246915dcd5 100644 --- a/mesonbuild/compilers/cuda.py +++ b/mesonbuild/compilers/cuda.py @@ -9,12 +9,8 @@ import typing as T from .. import options -from .. import mlog -from .. import mesonlib -from ..mesonlib import ( - EnvironmentException, is_windows, LibType, version_compare -) -from .compilers import Compiler, CompileCheckMode +from ..mesonlib import is_windows, LibType, version_compare +from .compilers import Compiler, CompileCheckMode, CrossNoRunException if T.TYPE_CHECKING: from ..build import BuildTarget @@ -497,6 +493,15 @@ def needs_static_linker(self) -> bool: def thread_link_flags(self) -> T.List[str]: return self._to_host_flags(self.host_compiler.thread_link_flags(), Phase.LINKER) + def init_from_options(self) -> None: + super().init_from_options() + try: + res = self.run(self._sanity_check_source_code()) + if res.returncode == 0: + self.detected_cc = res.stdout.strip() + except CrossNoRunException: + pass + def _sanity_check_source_code(self) -> str: return r''' #include @@ -546,29 +551,6 @@ def _sanity_check_compile_args(self, sourcename: str, binname: str return self.exelist + flags, [] - def _run_sanity_check(self, 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 self.environment.has_exe_wrapper(): - mlog.debug('Cannot run cross check') - return - - cmdlist = self._sanity_check_run_with_exe_wrapper(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)) - 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: - raise EnvironmentException(f'Executables created by {self.language} compiler {self.name_string()} are not runnable.') - - if stde == '': - self.detected_cc = stdo - def has_header_symbol(self, hname: str, symbol: str, prefix: str, *, extra_args: T.Union[None, T.List[str], T.Callable[[CompileCheckMode], T.List[str]]] = None, dependencies: T.Optional[T.List['Dependency']] = None) -> T.Tuple[bool, bool]: