Skip to content

Commit a94afa8

Browse files
committed
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 langauges 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.
1 parent a6af0ad commit a94afa8

File tree

17 files changed

+344
-266
lines changed

17 files changed

+344
-266
lines changed

mesonbuild/compilers/asm.py

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,8 @@
33
import os
44
import typing as T
55

6+
from mesonbuild.environment import Environment
7+
68
from ..mesonlib import EnvironmentException, get_meson_command
79
from ..options import OptionKey
810
from .compilers import Compiler
@@ -39,6 +41,9 @@ def __init__(self, ccache: T.List[str], exelist: T.List[str], version: str,
3941
raise EnvironmentException(f'ASM Compiler {self.id} does not support building for {info.cpu_family} CPU family.')
4042
super().__init__(ccache, exelist, version, for_machine, info, linker, full_version, is_cross)
4143

44+
def sanity_check(self, work_dir: str, env: Environment) -> None:
45+
return
46+
4247

4348
class NasmCompiler(ASMCompiler):
4449
language = 'nasm'

mesonbuild/compilers/c.py

Lines changed: 2 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -76,9 +76,8 @@ def __init__(self, ccache: T.List[str], exelist: T.List[str], version: str, for_
7676
def get_no_stdinc_args(self) -> T.List[str]:
7777
return ['-nostdinc']
7878

79-
def sanity_check(self, work_dir: str, environment: 'Environment') -> None:
80-
code = 'int main(void) { int class=0; return class; }\n'
81-
return self._sanity_check_impl(work_dir, environment, 'sanitycheckc.c', code)
79+
def _sanity_check_source_code(self) -> str:
80+
return 'int main(void) { int class=0; return class; }\n'
8281

8382
def has_header_symbol(self, hname: str, symbol: str, prefix: str,
8483
env: 'Environment', *,

mesonbuild/compilers/compilers.py

Lines changed: 134 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -1196,33 +1196,156 @@ def get_has_func_attribute_extra_args(self, name: str) -> T.List[str]:
11961196
def name_string(self) -> str:
11971197
return ' '.join(self.exelist)
11981198

1199-
@abc.abstractmethod
1200-
def sanity_check(self, work_dir: str, environment: 'Environment') -> None:
1199+
def sanity_check(self, work_dir: str, env: Environment) -> None:
12011200
"""Check that this compiler actually works.
12021201
12031202
This should provide a simple compile/link test. Something as simple as:
12041203
```python
12051204
main(): return 0
12061205
```
12071206
is good enough here.
1207+
1208+
:param work_dir: A directory to put temporary artifacts
1209+
:param env: The :class:`environment.Environment` instance to use with
1210+
this check
1211+
:raises mesonlib.EnvironmentException: If building the binary fails
1212+
:raises mesonlib.EnvironmentException: If running the binary is attempted and fails
1213+
"""
1214+
sourcename, transpiled, binname = self._sanity_check_filenames(env)
1215+
1216+
with open(os.path.join(work_dir, sourcename), 'w', encoding='utf-8') as f:
1217+
f.write(self._sanity_check_source_code())
1218+
1219+
if transpiled:
1220+
cmdlist = self._sanity_check_compile_args(env, sourcename, transpiled)
1221+
pc, stdo, stde = mesonlib.Popen_safe(cmdlist, cwd=work_dir)
1222+
mlog.debug('Sanity check transpiler command line:', mesonlib.join_args(cmdlist))
1223+
mlog.debug('Sanity check transpiler stdout:')
1224+
mlog.debug(stdo)
1225+
mlog.debug('-----\nSanity check transpiler stderr:')
1226+
mlog.debug(stde)
1227+
mlog.debug('-----')
1228+
if pc.returncode != 0:
1229+
raise mesonlib.EnvironmentException(f'Compiler {self.name_string()} cannot transpile programs.')
1230+
1231+
sourcename = transpiled
1232+
comp_lang = SUFFIX_TO_LANG[transpiled.rsplit('.', maxsplit=1)[1]]
1233+
comp = env.coredata.compilers[self.for_machine].get(comp_lang)
1234+
if not comp:
1235+
raise mesonlib.MesonBugException(f'Need a {comp_lang} compiler for {self.language} compiler test, but one doesnt exist')
1236+
1237+
cmdlist = self._transpiled_sanity_check_compile_args(comp, env, transpiled, binname)
1238+
1239+
mlog.debug('checking compilation of transpiled source:')
1240+
else:
1241+
cmdlist = self._sanity_check_compile_args(env, sourcename, binname)
1242+
1243+
pc, stdo, stde = mesonlib.Popen_safe(cmdlist, cwd=work_dir)
1244+
mlog.debug('Sanity check compiler command line:', mesonlib.join_args(cmdlist))
1245+
mlog.debug('Sanity check compile stdout:')
1246+
mlog.debug(stdo)
1247+
mlog.debug('-----\nSanity check compile stderr:')
1248+
mlog.debug(stde)
1249+
mlog.debug('-----')
1250+
if pc.returncode != 0:
1251+
raise mesonlib.EnvironmentException(f'Compiler {self.name_string()} cannot compile programs.')
1252+
1253+
self._run_sanity_check(env, [os.path.join(work_dir, binname)], work_dir)
1254+
1255+
def _sanity_check_filenames(self, env: Environment) -> T.Tuple[str, T.Optional[str], str]:
1256+
"""Generate the name of the source and binary file for the sanity check.
1257+
1258+
The returned names should be just the names of the files with
1259+
extensions, but no paths.
1260+
1261+
The return value consists of a source name, a transpiled source name (if
1262+
there is one), and the final binary name.
1263+
1264+
:param env: The :class:`environment.Environment` instance to use
1265+
:return: A tuple of (sourcename, transpiled, binaryname)
1266+
"""
1267+
default_ext = lang_suffixes[self.language][0]
1268+
template = f'sanity_check_for_{self.language}'
1269+
sourcename = f'{template}.{default_ext}'
1270+
cross_or_not = "_cross" if self.is_cross else ""
1271+
binaryname = f'{template}{cross_or_not}.exe'
1272+
return sourcename, None, binaryname
1273+
1274+
def _transpiled_sanity_check_compile_args(self, compiler: Compiler, env: Environment,
1275+
sourcename: str, binname: str) -> T.List[str]:
1276+
"""Get arguments to run compiler for sanity check.
1277+
1278+
By default this will just return the compile arguments for the compiler in question.
1279+
1280+
Overriding this is useful when needing to change the kind of output
1281+
produced, or adding extra arguments.
1282+
1283+
:param compiler: The :class:`Compiler` that is used for compiling the transpiled sources
1284+
:param env: The :class:`environment.Environment` instance to use
1285+
:param sourcename: the name of the source file to generate
1286+
:param binname: the name of the binary file to generate
1287+
:return: a list of strings to pass to :func:`subprocess.run` or equivalent
12081288
"""
1289+
return compiler._sanity_check_compile_args(env, sourcename, binname)
12091290

1210-
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]:
1211-
# Run sanity check
1212-
if self.is_cross and use_exe_wrapper_for_cross:
1213-
if not environment.has_exe_wrapper():
1214-
# Can't check if the binaries run so we have to assume they do
1215-
return ('', '')
1216-
cmdlist = environment.exe_wrapper.get_command() + cmdlist
1217-
mlog.debug('Running test binary command: ', mesonlib.join_args(cmdlist))
1291+
def _sanity_check_compile_args(self, env: Environment, sourcename: str, binname: str) -> T.List[str]:
1292+
"""Get arguments to run compiler for sanity check.
1293+
1294+
:param env: The :class:`environment.Environment` instance to use
1295+
:param sourcename: the name of the source file to generate
1296+
:param binname: the name of the binary file to generate
1297+
:return: a list of strings to pass to :func:`subprocess.run` or equivalent
1298+
"""
1299+
return self.exelist_no_ccache + self.get_always_args() + self.get_output_args(binname) + [sourcename]
1300+
1301+
@abc.abstractmethod
1302+
def _sanity_check_source_code(self) -> str:
1303+
"""Get the source code to run for a sanity check
1304+
1305+
:return: A string to be written into a file and ran.
1306+
"""
1307+
1308+
def _sanity_check_run_with_exe_wrapper(self, env: Environment, command: T.List[str]) -> T.List[str]:
1309+
"""Wrap the binary to run in the test with the exe_wrapper if necessary
1310+
1311+
Languages that do no want to use an exe_wrapper (or always want to use
1312+
some kind of wrapper) should override this method
1313+
1314+
:param env: the :class:`environment.Environment` instance to use
1315+
:param command: The string list of commands to run
1316+
:return: The list of commands wrapped by the exe_wrapper if it is needed, otherwise the original commands
1317+
"""
1318+
if self.is_cross and env.has_exe_wrapper():
1319+
assert env.exe_wrapper is not None, 'for mypy'
1320+
return env.exe_wrapper.get_command() + command
1321+
return command
1322+
1323+
def _run_sanity_check(self, env: Environment, cmdlist: T.List[str], work_dir: str) -> None:
1324+
"""Run a sanity test binary
1325+
1326+
:param env: the :class:`environment.Environment` instance to use
1327+
:param cmdlist: A list of strings to pass to :func:`subprocess.run` or equivalent to run the test
1328+
:param work_dir: A directory to place temporary artifacts
1329+
:raises mesonlib.EnvironmentException: If the binary cannot be run or if it returns a non-zero exit code
1330+
"""
1331+
# Can't check binaries, so we have to assume they work
1332+
if self.is_cross and not env.has_exe_wrapper():
1333+
mlog.debug('Cannot run cross check')
1334+
return
1335+
1336+
cmdlist = self._sanity_check_run_with_exe_wrapper(env, cmdlist)
1337+
mlog.debug('Sanity check built target output for', self.for_machine, self.language, 'compiler')
1338+
mlog.debug(' -- Running test binary command: ', mesonlib.join_args(cmdlist))
12181339
try:
12191340
pe, stdo, stde = Popen_safe_logged(cmdlist, 'Sanity check', cwd=work_dir)
1341+
mlog.debug(' -- stdout:\n', stdo)
1342+
mlog.debug(' -- stderr:\n', stde)
1343+
mlog.debug(' -- returncode:', pe.returncode)
12201344
except Exception as e:
12211345
raise mesonlib.EnvironmentException(f'Could not invoke sanity check executable: {e!s}.')
12221346

12231347
if pe.returncode != 0:
12241348
raise mesonlib.EnvironmentException(f'Executables created by {self.language} compiler {self.name_string()} are not runnable.')
1225-
return stdo, stde
12261349

12271350
def split_shlib_to_parts(self, fname: str) -> T.Tuple[T.Optional[str], str]:
12281351
return None, fname

mesonbuild/compilers/cpp.py

Lines changed: 2 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -86,9 +86,8 @@ def get_no_stdinc_args(self) -> T.List[str]:
8686
def get_no_stdlib_link_args(self) -> T.List[str]:
8787
return ['-nostdlib++']
8888

89-
def sanity_check(self, work_dir: str, environment: 'Environment') -> None:
90-
code = 'class breakCCompiler;int main(void) { return 0; }\n'
91-
return self._sanity_check_impl(work_dir, environment, 'sanitycheckcpp.cc', code)
89+
def _sanity_check_source_code(self) -> str:
90+
return 'class breakCCompiler;int main(void) { return 0; }\n'
9291

9392
def get_compiler_check_args(self, mode: CompileCheckMode) -> T.List[str]:
9493
# -fpermissive allows non-conforming code to compile which is necessary

mesonbuild/compilers/cs.py

Lines changed: 11 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -3,11 +3,10 @@
33

44
from __future__ import annotations
55

6-
import os.path, subprocess
6+
import os.path
77
import textwrap
88
import typing as T
99

10-
from ..mesonlib import EnvironmentException
1110
from ..linkers import RSPFileSyntax
1211

1312
from .compilers import Compiler
@@ -83,26 +82,18 @@ def get_pch_use_args(self, pch_dir: str, header: str) -> T.List[str]:
8382
def get_pch_name(self, header_name: str) -> str:
8483
return ''
8584

86-
def sanity_check(self, work_dir: str, environment: 'Environment') -> None:
87-
src = 'sanity.cs'
88-
obj = 'sanity.exe'
89-
source_name = os.path.join(work_dir, src)
90-
with open(source_name, 'w', encoding='utf-8') as ofile:
91-
ofile.write(textwrap.dedent('''
92-
public class Sanity {
93-
static public void Main () {
94-
}
85+
def _sanity_check_source_code(self) -> str:
86+
return textwrap.dedent('''
87+
public class Sanity {
88+
static public void Main () {
9589
}
96-
'''))
97-
pc = subprocess.Popen(self.exelist + self.get_always_args() + [src], cwd=work_dir)
98-
pc.wait()
99-
if pc.returncode != 0:
100-
raise EnvironmentException('C# compiler %s cannot compile programs.' % self.name_string())
90+
}
91+
''')
92+
93+
def _sanity_check_run_with_exe_wrapper(self, env: Environment, command: T.List[str]) -> T.List[str]:
10194
if self.runner:
102-
cmdlist = [self.runner, obj]
103-
else:
104-
cmdlist = [os.path.join(work_dir, obj)]
105-
self.run_sanity_check(environment, cmdlist, work_dir, use_exe_wrapper_for_cross=False)
95+
return [self.runner] + command
96+
return command
10697

10798
def needs_static_linker(self) -> bool:
10899
return False

0 commit comments

Comments
 (0)