1861094cfSGordon Ross#!@PYTHON@ 2861094cfSGordon Ross# 3861094cfSGordon Ross# This file and its contents are supplied under the terms of the 4861094cfSGordon Ross# Common Development and Distribution License ("CDDL"), version 1.0. 5861094cfSGordon Ross# You may only use this file in accordance with the terms of version 6861094cfSGordon Ross# 1.0 of the CDDL. 7861094cfSGordon Ross# 8861094cfSGordon Ross# A full copy of the text of the CDDL should have accompanied this 9861094cfSGordon Ross# source. A copy of the CDDL is also available via the Internet at 10861094cfSGordon Ross# http://www.illumos.org/license/CDDL. 11861094cfSGordon Ross# 12861094cfSGordon Ross 13861094cfSGordon Ross# 14861094cfSGordon Ross# Copyright 2026 Gordon W. Ross 15861094cfSGordon Ross# 16861094cfSGordon Ross 17861094cfSGordon Ross""" 18861094cfSGordon Rosssymbol_test.py - C and C++ symbol visibility test driver. 19861094cfSGordon Ross 20861094cfSGordon RossReads an environment config file and one or more symbols config files, 21861094cfSGordon Rossgenerates probe programs for each (symbol, environment) pair, compiles 22861094cfSGordon Rossthem, and reports pass/fail. Supports parallel compilation jobs. 23861094cfSGordon Ross 24861094cfSGordon RossThis program is run separately for 64-bit compiles and (where supported) 25861094cfSGordon Ross32-bit compiles by the test driver scripts. See the setup.ksh scripts in 26861094cfSGordon Rossthe ../c-symbols/ and ../cxx-symbols/ directories for examples. 27861094cfSGordon Ross 28861094cfSGordon RossSee also tests/common/README.md. 29861094cfSGordon Ross""" 30861094cfSGordon Ross 31861094cfSGordon Rossimport argparse 32861094cfSGordon Rossimport io 33861094cfSGordon Rossimport os 34861094cfSGordon Rossimport signal 35861094cfSGordon Rossimport subprocess 36861094cfSGordon Rossimport sys 37861094cfSGordon Rossimport tempfile 38861094cfSGordon Rossimport threading 39861094cfSGordon Rossfrom concurrent.futures import ThreadPoolExecutor 40861094cfSGordon Ross 41861094cfSGordon Ross 42861094cfSGordon Ross# --------------------------------------------------------------------------- 43861094cfSGordon Ross# parse_cfg - shared cfg file parser 44861094cfSGordon Ross# --------------------------------------------------------------------------- 45861094cfSGordon Ross 46861094cfSGordon Rossdef parse_cfg(fileobj, filename, handlers): 47861094cfSGordon Ross """ 48861094cfSGordon Ross Parse a cfg file-like object, dispatching each directive line to handlers. 49861094cfSGordon Ross 50861094cfSGordon Ross Handles continuation lines (trailing backslash), blank lines, and comments 51861094cfSGordon Ross (lines starting with '#'). Each non-blank, non-comment line is split on 52861094cfSGordon Ross '|' with each field stripped, then dispatched as: 53861094cfSGordon Ross 54861094cfSGordon Ross handlers[keyword](fields, filename, lineno) 55861094cfSGordon Ross 56861094cfSGordon Ross Unknown keywords cause an error via sys.exit(). 57861094cfSGordon Ross """ 58861094cfSGordon Ross lineno = 0 59861094cfSGordon Ross accum = '' 60861094cfSGordon Ross for raw in fileobj: 61861094cfSGordon Ross lineno += 1 62861094cfSGordon Ross text = raw.rstrip('\n').rstrip('\r') 63861094cfSGordon Ross if text.endswith('\\'): 64861094cfSGordon Ross accum += text[:-1] 65861094cfSGordon Ross continue 66861094cfSGordon Ross accum += text 67861094cfSGordon Ross line = accum.strip() 68861094cfSGordon Ross accum = '' 69861094cfSGordon Ross if not line or line.startswith('#'): 70861094cfSGordon Ross continue 71861094cfSGordon Ross parts = [p.strip() for p in line.split('|')] 72861094cfSGordon Ross keyword, fields = parts[0], parts[1:] 73861094cfSGordon Ross if keyword not in handlers: 74861094cfSGordon Ross sys.exit( 75861094cfSGordon Ross f'error: {filename}:{lineno}: unknown keyword {keyword!r}') 76861094cfSGordon Ross handlers[keyword](fields, filename, lineno) 77861094cfSGordon Ross 78861094cfSGordon Ross 79861094cfSGordon Ross# --------------------------------------------------------------------------- 80861094cfSGordon Ross# CompileEnv - one compilation environment (name, lang standard, defines) 81861094cfSGordon Ross# --------------------------------------------------------------------------- 82861094cfSGordon Ross 83861094cfSGordon Rossclass CompileEnv: 84861094cfSGordon Ross """One compilation environment as defined by an 'env' line in the cfg.""" 85861094cfSGordon Ross 86861094cfSGordon Ross def __init__(self, name, lang, defs): 87861094cfSGordon Ross self.name = name 88861094cfSGordon Ross self.lang = lang # e.g. 'c++98', 'c99' 89861094cfSGordon Ross self.defs = defs # preprocessor defines string, may be empty 90861094cfSGordon Ross 91861094cfSGordon Ross 92861094cfSGordon Ross# --------------------------------------------------------------------------- 93861094cfSGordon Ross# EnvConfig - loads and represents the env cfg file 94861094cfSGordon Ross# See test_parse_env.py for examples of method calls and results. 95861094cfSGordon Ross# --------------------------------------------------------------------------- 96861094cfSGordon Ross 97861094cfSGordon Rossclass EnvConfig: 98861094cfSGordon Ross """ 99861094cfSGordon Ross Loads a compilation environment config file (c-symbols-env.cfg or 100861094cfSGordon Ross cxx-symbols-env.cfg) and provides environment and group lookup. 101861094cfSGordon Ross 102861094cfSGordon Ross Public attributes: 103861094cfSGordon Ross envs: dict name -> CompileEnv 104861094cfSGordon Ross groups: dict name -> frozenset of env names 105861094cfSGordon Ross """ 106861094cfSGordon Ross 107861094cfSGordon Ross def __init__(self, lang): 108861094cfSGordon Ross self.lang = lang 109861094cfSGordon Ross self.envs = {} 110861094cfSGordon Ross self.groups = {} 111861094cfSGordon Ross 112861094cfSGordon Ross def load(self, path): 113861094cfSGordon Ross """Load env cfg from a file path.""" 114861094cfSGordon Ross # Locations searched for the file: 115861094cfSGordon Ross # 1. path as given 116861094cfSGordon Ross # 2. $STF_SUITE/cfg/<path> 117861094cfSGordon Ross # 3. cfg/<path> 118861094cfSGordon Ross candidates = [path] 119861094cfSGordon Ross if not os.path.isabs(path): 120861094cfSGordon Ross stf = os.environ.get('STF_SUITE', '../..') 121861094cfSGordon Ross candidates.append(os.path.join(stf, 'cfg', path)) 122861094cfSGordon Ross candidates.append(os.path.join('cfg', path)) 123861094cfSGordon Ross 124861094cfSGordon Ross for candidate in candidates: 125861094cfSGordon Ross if os.path.exists(candidate): 126861094cfSGordon Ross with open(candidate) as f: 127861094cfSGordon Ross self._parse(f, filename=candidate) 128861094cfSGordon Ross return 129861094cfSGordon Ross 130861094cfSGordon Ross sys.exit(f'error: env cfg file not found: {path}') 131861094cfSGordon Ross 132861094cfSGordon Ross def _parse(self, fileobj, filename='<input>'): 133861094cfSGordon Ross """ 134861094cfSGordon Ross Parse a file-like object as an environment config file. 135861094cfSGordon Ross 136861094cfSGordon Ross self.lang must be 'c' or 'c++'. Each env line's lang field is 137861094cfSGordon Ross validated against this value; a mismatch is an error. 138861094cfSGordon Ross """ 139861094cfSGordon Ross handlers = { 140861094cfSGordon Ross 'env': lambda f, fn, ln: self._do_env(f, fn, ln), 141861094cfSGordon Ross 'env_group': self._do_env_group, 142861094cfSGordon Ross } 143861094cfSGordon Ross parse_cfg(fileobj, filename, handlers) 144861094cfSGordon Ross 145861094cfSGordon Ross def _do_env(self, fields, filename, lineno): 146861094cfSGordon Ross if len(fields) != 3: 147861094cfSGordon Ross sys.exit( 148861094cfSGordon Ross f'error: {filename}:{lineno}: env: expected 3 fields, ' 149861094cfSGordon Ross f'got {len(fields)}') 150861094cfSGordon Ross 151861094cfSGordon Ross name, env_lang, defs = fields 152861094cfSGordon Ross 153861094cfSGordon Ross # Validate lang consistency 154861094cfSGordon Ross is_cxx = env_lang.startswith('c++') 155861094cfSGordon Ross if self.lang == 'c++' and not is_cxx: 156861094cfSGordon Ross sys.exit( 157861094cfSGordon Ross f'error: --lang=c++ but {filename} line {lineno} ' 158861094cfSGordon Ross f'has lang {env_lang!r}') 159861094cfSGordon Ross if self.lang == 'c' and is_cxx: 160861094cfSGordon Ross sys.exit( 161861094cfSGordon Ross f'error: --lang=c but {filename} line {lineno} ' 162861094cfSGordon Ross f'has lang {env_lang!r}') 163861094cfSGordon Ross 164861094cfSGordon Ross self.envs[name] = CompileEnv(name=name, lang=env_lang, defs=defs) 165861094cfSGordon Ross 166861094cfSGordon Ross def _do_env_group(self, fields, filename, lineno): 167861094cfSGordon Ross if len(fields) != 2: 168861094cfSGordon Ross sys.exit( 169861094cfSGordon Ross f'error: {filename}:{lineno}: env_group: expected 2 fields, ' 170861094cfSGordon Ross f'got {len(fields)}') 171861094cfSGordon Ross 172861094cfSGordon Ross name, members_str = fields 173861094cfSGordon Ross members = set() 174861094cfSGordon Ross for member in members_str.split(): 175861094cfSGordon Ross if member in self.envs: 176861094cfSGordon Ross members.add(member) 177861094cfSGordon Ross elif member in self.groups: 178861094cfSGordon Ross members |= self.groups[member] 179861094cfSGordon Ross else: 180861094cfSGordon Ross sys.exit( 181861094cfSGordon Ross f'error: {filename}:{lineno}: ' 182861094cfSGordon Ross f'reference to undefined env {member!r}') 183861094cfSGordon Ross 184861094cfSGordon Ross self.groups[name] = frozenset(members) 185861094cfSGordon Ross 186861094cfSGordon Ross def resolve(self, spec): 187861094cfSGordon Ross """ 188861094cfSGordon Ross Parse an env spec string like 'CXX98+ -CXX11 +CXX98'. 189861094cfSGordon Ross 190861094cfSGordon Ross Returns (test_set, need_set) where both are sets of env names: 191861094cfSGordon Ross test_set - all envs to compile 192861094cfSGordon Ross need_set - envs where compilation must succeed (pass) 193861094cfSGordon Ross 194861094cfSGordon Ross Bare name or +name -> added to both test_set and need_set. 195861094cfSGordon Ross -name -> added to test_set only (must fail). 196861094cfSGordon Ross """ 197861094cfSGordon Ross test_set = set() 198861094cfSGordon Ross need_set = set() 199861094cfSGordon Ross 200861094cfSGordon Ross for token in spec.split(): 201861094cfSGordon Ross if token.startswith('+'): 202861094cfSGordon Ross act = True 203861094cfSGordon Ross token = token[1:] 204861094cfSGordon Ross elif token.startswith('-'): 205861094cfSGordon Ross act = False 206861094cfSGordon Ross token = token[1:] 207861094cfSGordon Ross else: 208861094cfSGordon Ross act = True 209861094cfSGordon Ross 210861094cfSGordon Ross # Expand token - may be a single env or a group 211861094cfSGordon Ross expanded = self._expand(token) 212861094cfSGordon Ross test_set |= expanded 213861094cfSGordon Ross if act: 214861094cfSGordon Ross need_set |= expanded 215861094cfSGordon Ross else: 216861094cfSGordon Ross need_set -= expanded 217861094cfSGordon Ross 218861094cfSGordon Ross return (test_set, need_set) 219861094cfSGordon Ross 220861094cfSGordon Ross def _expand(self, name): 221861094cfSGordon Ross """Return frozenset of env names for a single env name or group name.""" 222861094cfSGordon Ross if name in self.envs: 223861094cfSGordon Ross return frozenset({name}) 224861094cfSGordon Ross if name in self.groups: 225861094cfSGordon Ross return self.groups[name] 226861094cfSGordon Ross sys.exit(f'error: reference to undefined env or group {name!r}') 227861094cfSGordon Ross 228*e1e6b944SGordon Ross def expand(self, name): 229*e1e6b944SGordon Ross """Return the set of environments named by one env or group.""" 230*e1e6b944SGordon Ross return set(self._expand(name)) 231*e1e6b944SGordon Ross 232861094cfSGordon Ross 233861094cfSGordon Ross# --------------------------------------------------------------------------- 234861094cfSGordon Ross# SymEntry - one symbol entry from a symbols config file 235861094cfSGordon Ross# --------------------------------------------------------------------------- 236861094cfSGordon Ross 237861094cfSGordon Rossclass SymEntry: 238861094cfSGordon Ross """One test entry as parsed from a symbols config file.""" 239861094cfSGordon Ross 240861094cfSGordon Ross def __init__(self, directive, symbol, env_spec, headers, 241861094cfSGordon Ross rtype=None, atypes=None, defval=None): 242861094cfSGordon Ross self.directive = directive # 'func' | 'type' | 'value' | 'define' 243861094cfSGordon Ross self.symbol = symbol # name to test 244861094cfSGordon Ross self.env_spec = env_spec # raw env spec string e.g. 'CXX98+ -CXX11' 245861094cfSGordon Ross self.headers = headers # list of header filenames 246861094cfSGordon Ross self.rtype = rtype # return/declaration type (not for define) 247861094cfSGordon Ross self.atypes = atypes if atypes is not None else [] 248861094cfSGordon Ross self.defval = defval # expected value for 'define', else None 249861094cfSGordon Ross 250861094cfSGordon Ross 251861094cfSGordon Ross# --------------------------------------------------------------------------- 252861094cfSGordon Ross# SymConfig - loads symbols config files 253861094cfSGordon Ross# See test_parse_sym.py for examples of method calls and results. 254861094cfSGordon Ross# --------------------------------------------------------------------------- 255861094cfSGordon Ross 256861094cfSGordon Rossclass SymConfig: 257861094cfSGordon Ross """ 258861094cfSGordon Ross Loads one or more symbols config files. 259861094cfSGordon Ross 260861094cfSGordon Ross Public attributes: 261861094cfSGordon Ross entries: list of SymEntry, in file order 262*e1e6b944SGordon Ross primary_header: first header named by the loaded configuration files 263861094cfSGordon Ross """ 264861094cfSGordon Ross 265861094cfSGordon Ross def __init__(self): 266861094cfSGordon Ross self.entries = [] 267*e1e6b944SGordon Ross self.primary_header = None 268861094cfSGordon Ross 269861094cfSGordon Ross def load(self, path): 270861094cfSGordon Ross """Load symbols config from a file path, with STF_SUITE search.""" 271861094cfSGordon Ross candidates = [path] 272861094cfSGordon Ross if not os.path.isabs(path): 273861094cfSGordon Ross stf = os.environ.get('STF_SUITE', '../..') 274861094cfSGordon Ross candidates.append(os.path.join(stf, 'cfg', path)) 275861094cfSGordon Ross candidates.append(os.path.join('cfg', path)) 276861094cfSGordon Ross 277861094cfSGordon Ross for candidate in candidates: 278861094cfSGordon Ross if os.path.exists(candidate): 279861094cfSGordon Ross with open(candidate) as f: 280861094cfSGordon Ross self._parse(f, filename=candidate) 281861094cfSGordon Ross return 282861094cfSGordon Ross 283861094cfSGordon Ross sys.exit(f'error: symbols config file not found: {path}') 284861094cfSGordon Ross 285861094cfSGordon Ross def _parse(self, fileobj, filename='<input>'): 286861094cfSGordon Ross """Parse a file-like object as a symbols config file.""" 287*e1e6b944SGordon Ross self._file_primary_header = None 288*e1e6b944SGordon Ross self._warned_primary_headers = set() 289861094cfSGordon Ross handlers = { 290861094cfSGordon Ross 'type': self._do_type, 291861094cfSGordon Ross 'value': self._do_value, 292861094cfSGordon Ross 'define': self._do_define, 293861094cfSGordon Ross 'func': self._do_func, 294861094cfSGordon Ross } 295861094cfSGordon Ross parse_cfg(fileobj, filename, handlers) 296*e1e6b944SGordon Ross if self._file_primary_header is None: 297*e1e6b944SGordon Ross sys.exit(f'error: {filename}: symbols configuration is empty') 298861094cfSGordon Ross 299861094cfSGordon Ross @staticmethod 300861094cfSGordon Ross def _split_list(s): 301861094cfSGordon Ross """Split a semicolon-separated field, stripping each item.""" 302861094cfSGordon Ross return [item.strip() for item in s.split(';') if item.strip()] 303861094cfSGordon Ross 304*e1e6b944SGordon Ross def _add_entry(self, entry, filename, lineno): 305*e1e6b944SGordon Ross if not entry.headers: 306*e1e6b944SGordon Ross sys.exit(f'error: {filename}:{lineno}: no header specified') 307*e1e6b944SGordon Ross 308*e1e6b944SGordon Ross # A configuration describes one primary header. Later headers in an 309*e1e6b944SGordon Ross # entry are there to support its probe, but only the first identifies 310*e1e6b944SGordon Ross # the header whose positive coverage we track. 311*e1e6b944SGordon Ross header = entry.headers[0] 312*e1e6b944SGordon Ross if self._file_primary_header is None: 313*e1e6b944SGordon Ross self._file_primary_header = header 314*e1e6b944SGordon Ross if self.primary_header is None: 315*e1e6b944SGordon Ross self.primary_header = header 316*e1e6b944SGordon Ross elif header != self.primary_header: 317*e1e6b944SGordon Ross sys.exit( 318*e1e6b944SGordon Ross f'error: {filename}:{lineno}: primary header {header!r} ' 319*e1e6b944SGordon Ross f'differs from {self.primary_header!r}\n' 320*e1e6b944SGordon Ross 'Only one primary header per invocation is supported') 321*e1e6b944SGordon Ross elif header != self._file_primary_header: 322*e1e6b944SGordon Ross # Keep the entry for its ordinary symbol test, but it does not 323*e1e6b944SGordon Ross # establish positive coverage for the primary header. 324*e1e6b944SGordon Ross if header not in self._warned_primary_headers: 325*e1e6b944SGordon Ross print( 326*e1e6b944SGordon Ross f'warning: {filename}:{lineno}: primary header {header!r} ' 327*e1e6b944SGordon Ross f'differs from {self._file_primary_header!r}\n' 328*e1e6b944SGordon Ross 'Only one primary header per configuration file is ' 329*e1e6b944SGordon Ross 'expected', 330*e1e6b944SGordon Ross file=sys.stderr) 331*e1e6b944SGordon Ross self._warned_primary_headers.add(header) 332*e1e6b944SGordon Ross 333*e1e6b944SGordon Ross self.entries.append(entry) 334*e1e6b944SGordon Ross 335861094cfSGordon Ross def _do_type(self, fields, filename, lineno): 336861094cfSGordon Ross # type | decl | headers | envs 337861094cfSGordon Ross if len(fields) != 3: 338861094cfSGordon Ross sys.exit( 339861094cfSGordon Ross f'error: {filename}:{lineno}: type: expected 3 fields, ' 340861094cfSGordon Ross f'got {len(fields)}') 341861094cfSGordon Ross decl, hdrs, envs = fields 342*e1e6b944SGordon Ross self._add_entry(SymEntry( 343861094cfSGordon Ross directive='type', 344861094cfSGordon Ross symbol=decl, 345861094cfSGordon Ross rtype=decl, 346861094cfSGordon Ross headers=self._split_list(hdrs), 347861094cfSGordon Ross env_spec=envs, 348*e1e6b944SGordon Ross ), filename, lineno) 349861094cfSGordon Ross 350861094cfSGordon Ross def _do_value(self, fields, filename, lineno): 351861094cfSGordon Ross # value | name | type | headers | envs 352861094cfSGordon Ross if len(fields) != 4: 353861094cfSGordon Ross sys.exit( 354861094cfSGordon Ross f'error: {filename}:{lineno}: value: expected 4 fields, ' 355861094cfSGordon Ross f'got {len(fields)}') 356861094cfSGordon Ross name, rtype, hdrs, envs = fields 357*e1e6b944SGordon Ross self._add_entry(SymEntry( 358861094cfSGordon Ross directive='value', 359861094cfSGordon Ross symbol=name, 360861094cfSGordon Ross rtype=rtype, 361861094cfSGordon Ross headers=self._split_list(hdrs), 362861094cfSGordon Ross env_spec=envs, 363*e1e6b944SGordon Ross ), filename, lineno) 364861094cfSGordon Ross 365861094cfSGordon Ross def _do_define(self, fields, filename, lineno): 366861094cfSGordon Ross # define | name | value | headers | envs (value may be empty) 367861094cfSGordon Ross if len(fields) != 4: 368861094cfSGordon Ross sys.exit( 369861094cfSGordon Ross f'error: {filename}:{lineno}: define: expected 4 fields, ' 370861094cfSGordon Ross f'got {len(fields)}') 371861094cfSGordon Ross name, defval, hdrs, envs = fields 372*e1e6b944SGordon Ross self._add_entry(SymEntry( 373861094cfSGordon Ross directive='define', 374861094cfSGordon Ross symbol=name, 375861094cfSGordon Ross defval=defval if defval else None, 376861094cfSGordon Ross headers=self._split_list(hdrs), 377861094cfSGordon Ross env_spec=envs, 378*e1e6b944SGordon Ross ), filename, lineno) 379861094cfSGordon Ross 380861094cfSGordon Ross def _do_func(self, fields, filename, lineno): 381861094cfSGordon Ross # func | name | rtype | atypes | headers | envs 382861094cfSGordon Ross if len(fields) != 5: 383861094cfSGordon Ross sys.exit( 384861094cfSGordon Ross f'error: {filename}:{lineno}: func: expected 5 fields, ' 385861094cfSGordon Ross f'got {len(fields)}') 386861094cfSGordon Ross name, rtype, atypes, hdrs, envs = fields 387*e1e6b944SGordon Ross self._add_entry(SymEntry( 388861094cfSGordon Ross directive='func', 389861094cfSGordon Ross symbol=name, 390861094cfSGordon Ross rtype=rtype, 391861094cfSGordon Ross atypes=self._split_list(atypes), 392861094cfSGordon Ross headers=self._split_list(hdrs), 393861094cfSGordon Ross env_spec=envs, 394*e1e6b944SGordon Ross ), filename, lineno) 395861094cfSGordon Ross 396861094cfSGordon Ross 397861094cfSGordon Ross# --------------------------------------------------------------------------- 398861094cfSGordon Ross# ProbeGen - generates probe program source for a (SymEntry, lang) pair 399861094cfSGordon Ross# See test_gen_probe.py for examples of method calls and results. 400861094cfSGordon Ross# --------------------------------------------------------------------------- 401861094cfSGordon Ross 402861094cfSGordon Rossclass ProbeGen: 403861094cfSGordon Ross 404861094cfSGordon Ross RESULT_MACRO = ( 405861094cfSGordon Ross '#if __cplusplus >= 201103L\n' 406861094cfSGordon Ross '#define RESULT(v) result{v}\n' 407861094cfSGordon Ross '#else\n' 408861094cfSGordon Ross '#define RESULT(v) result = (v)\n' 409861094cfSGordon Ross '#endif\n' 410861094cfSGordon Ross ) 411861094cfSGordon Ross 412861094cfSGordon Ross @staticmethod 413861094cfSGordon Ross def gen_probe(entry, lang): 414861094cfSGordon Ross """ 415861094cfSGordon Ross Generate probe program source text for a SymEntry and language. 416861094cfSGordon Ross 417861094cfSGordon Ross """ 418861094cfSGordon Ross out = [] 419861094cfSGordon Ross 420861094cfSGordon Ross for h in entry.headers: 421861094cfSGordon Ross out.append(f'#include <{h}>\n') 422861094cfSGordon Ross 423861094cfSGordon Ross rtype = entry.rtype or '' 424861094cfSGordon Ross prefix, suffix = ProbeGen._split_rtype(rtype) 425861094cfSGordon Ross is_fnptr = suffix != '' 426861094cfSGordon Ross 427861094cfSGordon Ross # Emit the RESULT() macro for C++ func probes with a non-void, 428861094cfSGordon Ross # non-fnptr return value (brace-init catches narrowing errors in 429861094cfSGordon Ross # C++11+; plain assignment used for C++98). 430861094cfSGordon Ross has_rtype = (entry.directive == 'func' and 431861094cfSGordon Ross rtype != '' and rtype != 'void') 432861094cfSGordon Ross use_result = has_rtype and not is_fnptr and lang == 'c++' 433861094cfSGordon Ross 434861094cfSGordon Ross if use_result: 435861094cfSGordon Ross out.append(ProbeGen.RESULT_MACRO) 436861094cfSGordon Ross 437861094cfSGordon Ross # Emit return type prefix + trailing space (matches C programs' 438861094cfSGordon Ross # unconditional addprogch(' ') after the rtype loop). 439861094cfSGordon Ross if rtype: 440861094cfSGordon Ross out.append(prefix + ' ') 441861094cfSGordon Ross 442*e1e6b944SGordon Ross # Special "include-only" tests are used to ensure that a header 443*e1e6b944SGordon Ross # (at least) compiles in a trivial C program. 444*e1e6b944SGordon Ross if entry.directive == 'include-only': 445*e1e6b944SGordon Ross out.append('int header_compile_test;\n') 446*e1e6b944SGordon Ross 447*e1e6b944SGordon Ross elif entry.directive == 'type': 448861094cfSGordon Ross out.append('test_type;\n') 449861094cfSGordon Ross 450861094cfSGordon Ross elif entry.directive == 'value': 451861094cfSGordon Ross out.append(f'test_value{suffix};\n') 452861094cfSGordon Ross out.append('void\ntest_func(void)\n{\n') 453861094cfSGordon Ross out.append(f'\ttest_value = {entry.symbol};\n}}\n') 454861094cfSGordon Ross 455861094cfSGordon Ross elif entry.directive == 'define': 456861094cfSGordon Ross out.append(f'#if !defined({entry.symbol})') 457861094cfSGordon Ross if entry.defval: 458861094cfSGordon Ross out.append(f' || {entry.symbol} != {entry.defval}') 459861094cfSGordon Ross out.append(f'\n#error {entry.symbol} is not defined or has the wrong value') 460861094cfSGordon Ross out.append('\n#endif\n') 461861094cfSGordon Ross out.append('\n') 462861094cfSGordon Ross 463861094cfSGordon Ross elif entry.directive == 'func': 464861094cfSGordon Ross arglist = ProbeGen._build_arglist(entry.atypes) 465861094cfSGordon Ross out.append(f'\ntest_func({arglist}){suffix}\n{{\n\t') 466861094cfSGordon Ross 467861094cfSGordon Ross call = f'{entry.symbol}({ProbeGen._build_callargs(entry.atypes)})' 468861094cfSGordon Ross 469861094cfSGordon Ross if use_result: 470861094cfSGordon Ross out.append(f'{rtype} RESULT({call});\n\treturn result;\n}}') 471861094cfSGordon Ross elif has_rtype or is_fnptr: 472861094cfSGordon Ross out.append(f'return {call};\n}}') 473861094cfSGordon Ross else: 474861094cfSGordon Ross out.append(f'{call};\n}}') 475861094cfSGordon Ross out.append('\n') 476861094cfSGordon Ross 477861094cfSGordon Ross return ''.join(out) 478861094cfSGordon Ross 479861094cfSGordon Ross @staticmethod 480861094cfSGordon Ross def _split_rtype(rtype): 481861094cfSGordon Ross """ 482861094cfSGordon Ross Split rtype for function pointer support. 483861094cfSGordon Ross 484861094cfSGordon Ross For 'void (*)(int)': returns ('void (*', ')(int)') 485861094cfSGordon Ross For 'double': returns ('double', '') 486861094cfSGordon Ross """ 487861094cfSGordon Ross idx = rtype.find('(*') 488861094cfSGordon Ross if idx >= 0: 489861094cfSGordon Ross return rtype[:idx + 2], rtype[idx + 2:] 490861094cfSGordon Ross return rtype, '' 491861094cfSGordon Ross 492861094cfSGordon Ross @staticmethod 493861094cfSGordon Ross def _build_arglist(atypes): 494861094cfSGordon Ross """Build the formal parameter list string, matching C arg-name insertion.""" 495861094cfSGordon Ross if not atypes: 496861094cfSGordon Ross return 'void' 497861094cfSGordon Ross parts = [] 498861094cfSGordon Ross for i, atype in enumerate(atypes): 499861094cfSGordon Ross if atype == '': 500861094cfSGordon Ross parts.append('void') 501861094cfSGordon Ross elif atype == 'void': 502861094cfSGordon Ross parts.append('void') 503861094cfSGordon Ross elif '(*' in atype: 504861094cfSGordon Ross idx = atype.index('(*') 505861094cfSGordon Ross parts.append(f'{atype[:idx + 2]}a{i}{atype[idx + 2:]}') 506861094cfSGordon Ross elif '[' in atype: 507861094cfSGordon Ross idx = atype.index('[') 508861094cfSGordon Ross parts.append(f'{atype[:idx]}a{i}{atype[idx:]}') 509861094cfSGordon Ross else: 510861094cfSGordon Ross parts.append(f'{atype} a{i}') 511861094cfSGordon Ross return ', '.join(parts) 512861094cfSGordon Ross 513861094cfSGordon Ross @staticmethod 514861094cfSGordon Ross def _build_callargs(atypes): 515861094cfSGordon Ross """Build actual argument names for the function call, skipping void/empty.""" 516861094cfSGordon Ross return ', '.join( 517861094cfSGordon Ross f'a{i}' for i, atype in enumerate(atypes) 518861094cfSGordon Ross if atype not in ('', 'void') 519861094cfSGordon Ross ) 520861094cfSGordon Ross 521861094cfSGordon Ross 522861094cfSGordon Ross# --------------------------------------------------------------------------- 523861094cfSGordon Ross# Job - inputs to a single (symbol, env) compilation 524861094cfSGordon Ross# --------------------------------------------------------------------------- 525861094cfSGordon Ross 526861094cfSGordon Rossclass Job: 527861094cfSGordon Ross """All inputs needed to run one compilation job in a worker thread.""" 528861094cfSGordon Ross 529861094cfSGordon Ross __slots__ = ('index', 'entry', 'env', 'expect_pass', 'lang', 530861094cfSGordon Ross 'compiler', 'mflag', 'arch', 'std_flag', 'base_flags', 531181d1aa3SGordon Ross 'tmpdir', 'debug', 'extra_debug', 'force', 532181d1aa3SGordon Ross 'output', 'done') 533861094cfSGordon Ross 534861094cfSGordon Ross def __init__(self, index, entry, env, expect_pass, lang, 535861094cfSGordon Ross compiler, mflag, arch, std_flag, base_flags, 536861094cfSGordon Ross tmpdir, debug, extra_debug, force): 537861094cfSGordon Ross self.index = index 538861094cfSGordon Ross self.entry = entry 539861094cfSGordon Ross self.env = env 540861094cfSGordon Ross self.expect_pass = expect_pass 541861094cfSGordon Ross self.lang = lang 542861094cfSGordon Ross self.compiler = compiler 543861094cfSGordon Ross self.mflag = mflag 544861094cfSGordon Ross self.arch = arch # '64-bit' or '32-bit' 545861094cfSGordon Ross self.std_flag = std_flag # e.g. '-std=c99' 546861094cfSGordon Ross self.base_flags = base_flags # list of flags common to all jobs 547861094cfSGordon Ross self.tmpdir = tmpdir 548861094cfSGordon Ross self.debug = debug # -d: show probe + compiler output on failure 549f9db9ff7SGordon Ross self.extra_debug = extra_debug # -D: also show command + probe on pass 550861094cfSGordon Ross self.force = force # -f: continue after failures 551181d1aa3SGordon Ross self.output = None # buffered text, set once the job finishes 552181d1aa3SGordon Ross self.done = False # True once this job has finished 553861094cfSGordon Ross 554861094cfSGordon Ross 555861094cfSGordon Ross# --------------------------------------------------------------------------- 556861094cfSGordon Ross# TestDriver - builds job list, drives thread pool, reports results 557861094cfSGordon Ross# --------------------------------------------------------------------------- 558861094cfSGordon Ross 559861094cfSGordon Rossclass TestDriver: 560861094cfSGordon Ross """ 561861094cfSGordon Ross Expands symbol entries × environments into jobs, runs them in a thread 562861094cfSGordon Ross pool, and reports results. 563861094cfSGordon Ross """ 564861094cfSGordon Ross 565*e1e6b944SGordon Ross def run(self, sym_config, env_config, positive_coverage, 566*e1e6b944SGordon Ross compiler, mflag, arch, base_flags, tmpdir, opts): 567861094cfSGordon Ross """ 568861094cfSGordon Ross Run all (symbol, env) compilations. 569861094cfSGordon Ross 570861094cfSGordon Ross Returns True if all tests passed (and not interrupted), else False. 571861094cfSGordon Ross """ 572861094cfSGordon Ross lock = threading.Lock() 573861094cfSGordon Ross stop = threading.Event() 574861094cfSGordon Ross counters = {'pass': 0, 'fail': 0} 575181d1aa3SGordon Ross next_to_print = 0 576861094cfSGordon Ross 577861094cfSGordon Ross orig_sigint = signal.getsignal(signal.SIGINT) 578861094cfSGordon Ross orig_sigterm = signal.getsignal(signal.SIGTERM) 579861094cfSGordon Ross 580861094cfSGordon Ross def handle_signal(signum, frame): 581861094cfSGordon Ross stop.set() 582861094cfSGordon Ross 583861094cfSGordon Ross signal.signal(signal.SIGINT, handle_signal) 584861094cfSGordon Ross signal.signal(signal.SIGTERM, handle_signal) 585861094cfSGordon Ross 586181d1aa3SGordon Ross # drain() prints a run of now-finished jobs starting at 587181d1aa3SGordon Ross # next_to_print, stopping at the first unfinished job. 588181d1aa3SGordon Ross # This makes output appears in job (array) order even 589181d1aa3SGordon Ross # though jobs complete in a non-deterministic order. 590181d1aa3SGordon Ross # Must be called with lock held. 591181d1aa3SGordon Ross def drain(): 592181d1aa3SGordon Ross nonlocal next_to_print 593181d1aa3SGordon Ross while next_to_print < len(jobs) and jobs[next_to_print].done: 594181d1aa3SGordon Ross if jobs[next_to_print].output: 595181d1aa3SGordon Ross print(jobs[next_to_print].output, flush=True) 596181d1aa3SGordon Ross jobs[next_to_print].output = None 597181d1aa3SGordon Ross next_to_print += 1 598181d1aa3SGordon Ross 599861094cfSGordon Ross # run_job() is the worker function called by each thread in the pool. 600861094cfSGordon Ross # It captures all output for one job in a local buffer, then acquires 601181d1aa3SGordon Ross # the lock to record it and drain any now-printable jobs in order. 602861094cfSGordon Ross def run_job(job): 603861094cfSGordon Ross if stop.is_set(): 604861094cfSGordon Ross return 605861094cfSGordon Ross 606861094cfSGordon Ross # Generate probe source and write to temp file. 607861094cfSGordon Ross src = ProbeGen.gen_probe(job.entry, job.lang) 608861094cfSGordon Ross ext = 'cc' if job.lang == 'c++' else 'c' 609861094cfSGordon Ross base = os.path.join(job.tmpdir, f'job-{job.index}') 610861094cfSGordon Ross srcfile = f'{base}.{ext}' 611861094cfSGordon Ross objfile = f'{base}.o' 612861094cfSGordon Ross logfile = f'{base}.log' 613861094cfSGordon Ross 614861094cfSGordon Ross with open(srcfile, 'w') as f: 615861094cfSGordon Ross f.write(src) 616861094cfSGordon Ross 617861094cfSGordon Ross # Build compiler command. 618861094cfSGordon Ross env_defs = job.env.defs.split() if job.env.defs else [] 619861094cfSGordon Ross cmd = ([job.compiler, job.mflag, job.std_flag] + 620861094cfSGordon Ross job.base_flags + env_defs + 621861094cfSGordon Ross ['-c', srcfile, '-o', objfile]) 622861094cfSGordon Ross 623861094cfSGordon Ross with open(logfile, 'w') as lf: 624861094cfSGordon Ross proc = subprocess.run(cmd, stdout=lf, stderr=lf) 625861094cfSGordon Ross 626861094cfSGordon Ross compile_ok = (proc.returncode == 0) 627861094cfSGordon Ross passed = (compile_ok == job.expect_pass) 628861094cfSGordon Ross 629861094cfSGordon Ross # Output buffer (flushed at end of job) 630861094cfSGordon Ross sign = '+' if job.expect_pass else '-' 631861094cfSGordon Ross label = f'{job.entry.symbol} : {sign}{job.env.name} ({job.arch})' 632861094cfSGordon Ross out = [f'TEST STARTING {label}:'] 633861094cfSGordon Ross 634861094cfSGordon Ross if job.extra_debug: 635861094cfSGordon Ross out.append(f'TEST DEBUG {label}: command: {" ".join(cmd)}') 636861094cfSGordon Ross 637f9db9ff7SGordon Ross if job.extra_debug or (job.debug and not passed): 638861094cfSGordon Ross out.append(f'TEST DEBUG {label}: probe program:') 639861094cfSGordon Ross for line in src.splitlines(): 640861094cfSGordon Ross out.append(f'TEST DEBUG {label}: {line}') 641f9db9ff7SGordon Ross 642f9db9ff7SGordon Ross if job.debug and not passed: 643861094cfSGordon Ross with open(logfile) as lf: 644861094cfSGordon Ross cc_out = lf.read().strip() 645861094cfSGordon Ross if cc_out: 646861094cfSGordon Ross out.append(f'TEST DEBUG {label}: compiler output:') 647861094cfSGordon Ross for line in cc_out.splitlines(): 648861094cfSGordon Ross out.append(f'TEST DEBUG {label}: {line}') 649861094cfSGordon Ross 650861094cfSGordon Ross if passed: 651861094cfSGordon Ross out.append(f'TEST PASS: {label}') 652861094cfSGordon Ross else: 653861094cfSGordon Ross verb = 'FAILING' if job.force else 'FAILED' 654861094cfSGordon Ross reason = (f'error compiling in {job.env.name}' 655861094cfSGordon Ross if job.expect_pass 656861094cfSGordon Ross else f'symbol visible in {job.env.name}') 657861094cfSGordon Ross out.append(f'TEST {verb} {label}: {reason}') 658861094cfSGordon Ross 659861094cfSGordon Ross with lock: 660181d1aa3SGordon Ross job.output = '\n'.join(out) 661181d1aa3SGordon Ross job.done = True 662861094cfSGordon Ross if passed: 663861094cfSGordon Ross counters['pass'] += 1 664861094cfSGordon Ross else: 665861094cfSGordon Ross counters['fail'] += 1 666861094cfSGordon Ross if not job.force: 667861094cfSGordon Ross stop.set() 668181d1aa3SGordon Ross drain() 669861094cfSGordon Ross 670861094cfSGordon Ross # Build the list of jobs to run. 671*e1e6b944SGordon Ross # Order jobs by configuration entry and then environment name 672861094cfSGordon Ross jobs = [] 673*e1e6b944SGordon Ross 674*e1e6b944SGordon Ross # First add the synthetic include-only test configurations. 675*e1e6b944SGordon Ross # These are added when there is no positive test case for some 676*e1e6b944SGordon Ross # compilation environment. That ensures that negative cases 677*e1e6b944SGordon Ross # don't pass by accident if a header does not compile at all. 678*e1e6b944SGordon Ross # 679*e1e6b944SGordon Ross # Note that with symbol filtering (opts.sym) we skip this, 680*e1e6b944SGordon Ross # and with (opts.env) filter the envoronments the same as 681*e1e6b944SGordon Ross # how normal jobs from the test config would do. 682*e1e6b944SGordon Ross 683*e1e6b944SGordon Ross if not opts.sym: 684*e1e6b944SGordon Ross covered = set() 685*e1e6b944SGordon Ross for entry in sym_config.entries: 686*e1e6b944SGordon Ross if entry.headers[0] == sym_config.primary_header: 687*e1e6b944SGordon Ross _, need_set = env_config.resolve(entry.env_spec) 688*e1e6b944SGordon Ross covered |= need_set 689*e1e6b944SGordon Ross missing = positive_coverage - covered 690*e1e6b944SGordon Ross if opts.env: 691*e1e6b944SGordon Ross narrow, _ = env_config.resolve(opts.env) 692*e1e6b944SGordon Ross missing &= narrow 693*e1e6b944SGordon Ross entry = SymEntry( 694*e1e6b944SGordon Ross directive='include-only', 695*e1e6b944SGordon Ross symbol=f'include-only <{sym_config.primary_header}>', 696*e1e6b944SGordon Ross env_spec='', 697*e1e6b944SGordon Ross headers=[sym_config.primary_header], 698*e1e6b944SGordon Ross ) 699*e1e6b944SGordon Ross for env_name in sorted(missing): 700*e1e6b944SGordon Ross env = env_config.envs[env_name] 701*e1e6b944SGordon Ross jobs.append(Job( 702*e1e6b944SGordon Ross index=len(jobs), 703*e1e6b944SGordon Ross entry=entry, 704*e1e6b944SGordon Ross env=env, 705*e1e6b944SGordon Ross expect_pass=True, 706*e1e6b944SGordon Ross lang=opts.lang, 707*e1e6b944SGordon Ross compiler=compiler, 708*e1e6b944SGordon Ross mflag=mflag, 709*e1e6b944SGordon Ross arch=arch, 710*e1e6b944SGordon Ross std_flag=f'-std={env.lang}', 711*e1e6b944SGordon Ross base_flags=base_flags, 712*e1e6b944SGordon Ross tmpdir=tmpdir, 713*e1e6b944SGordon Ross debug=opts.debug, 714*e1e6b944SGordon Ross extra_debug=opts.extra_debug, 715*e1e6b944SGordon Ross force=opts.force, 716*e1e6b944SGordon Ross )) 717*e1e6b944SGordon Ross 718*e1e6b944SGordon Ross # 719*e1e6b944SGordon Ross # Now add jobs from the normal config file rows. 720*e1e6b944SGordon Ross # These may be filtered by symbol and/or environment 721*e1e6b944SGordon Ross # using opts.sym or opts.env 722*e1e6b944SGordon Ross # 723*e1e6b944SGordon Ross 724*e1e6b944SGordon Ross for entry in sym_config.entries: 725861094cfSGordon Ross if opts.sym and entry.symbol != opts.sym: 726861094cfSGordon Ross continue 727861094cfSGordon Ross test_set, need_set = env_config.resolve(entry.env_spec) 728861094cfSGordon Ross if opts.env: 729861094cfSGordon Ross narrow, _ = env_config.resolve(opts.env) 730861094cfSGordon Ross test_set &= narrow 731861094cfSGordon Ross need_set &= narrow 732861094cfSGordon Ross for env_name in sorted(test_set): 733861094cfSGordon Ross env = env_config.envs[env_name] 734861094cfSGordon Ross jobs.append(Job( 735861094cfSGordon Ross index=len(jobs), 736861094cfSGordon Ross entry=entry, 737861094cfSGordon Ross env=env, 738861094cfSGordon Ross expect_pass=(env_name in need_set), 739861094cfSGordon Ross lang=opts.lang, 740861094cfSGordon Ross compiler=compiler, 741861094cfSGordon Ross mflag=mflag, 742861094cfSGordon Ross arch=arch, 743861094cfSGordon Ross std_flag=f'-std={env.lang}', 744861094cfSGordon Ross base_flags=base_flags, 745861094cfSGordon Ross tmpdir=tmpdir, 746861094cfSGordon Ross debug=opts.debug, 747861094cfSGordon Ross extra_debug=opts.extra_debug, 748861094cfSGordon Ross force=opts.force, 749861094cfSGordon Ross )) 750861094cfSGordon Ross 751861094cfSGordon Ross # The ThreadPoolExecutor runs up to opts.j worker threads concurrently. 752861094cfSGordon Ross # executor.submit() queues each job; the pool calls run_job(job) in a 753181d1aa3SGordon Ross # worker thread. Exiting the "with" block waits for all thread pool 754181d1aa3SGordon Ross # executors to finish before proceeding. 755861094cfSGordon Ross with ThreadPoolExecutor(max_workers=opts.j) as executor: 756861094cfSGordon Ross for job in jobs: 757861094cfSGordon Ross executor.submit(run_job, job) 758861094cfSGordon Ross 759181d1aa3SGordon Ross # All thread pool executors have finished. If we were interrupted, 760181d1aa3SGordon Ross # there may be unfinished jobs and there may also be finished jobs 761181d1aa3SGordon Ross # scattered among those with pending output. Scan the remainder of 762181d1aa3SGordon Ross # the jobs list and flush (drain) any jobs with pending output. 763181d1aa3SGordon Ross with lock: 764181d1aa3SGordon Ross while next_to_print < len(jobs): 765181d1aa3SGordon Ross if jobs[next_to_print].done and jobs[next_to_print].output: 766181d1aa3SGordon Ross print(jobs[next_to_print].output, flush=True) 767181d1aa3SGordon Ross # Could free jobs[].output here but we're 768181d1aa3SGordon Ross # about to exit so just skip that work. 769181d1aa3SGordon Ross next_to_print += 1 770181d1aa3SGordon Ross 771861094cfSGordon Ross signal.signal(signal.SIGINT, orig_sigint) 772861094cfSGordon Ross signal.signal(signal.SIGTERM, orig_sigterm) 773861094cfSGordon Ross 774861094cfSGordon Ross passes = counters['pass'] 775861094cfSGordon Ross total = passes + counters['fail'] 776861094cfSGordon Ross if passes == total: 777861094cfSGordon Ross print(f'TEST SUMMARY: {passes} / {total} (ok)') 778861094cfSGordon Ross else: 779861094cfSGordon Ross print(f'TEST SUMMARY: {passes} / {total} ({total - passes} failing)') 780861094cfSGordon Ross 781861094cfSGordon Ross return counters['fail'] == 0 and not stop.is_set() 782861094cfSGordon Ross 783861094cfSGordon Ross 784861094cfSGordon Ross# --------------------------------------------------------------------------- 785861094cfSGordon Ross# Compiler detection 786861094cfSGordon Ross# --------------------------------------------------------------------------- 787861094cfSGordon Ross 788861094cfSGordon Ross# Exit codes emitted by the compiler-detection probe. 789861094cfSGordon Ross_COMP_STUDIO = 51 790861094cfSGordon Ross_COMP_CLANG = 52 791861094cfSGordon Ross_COMP_GCC = 53 792861094cfSGordon Ross_COMP_UNKNOWN = 99 793861094cfSGordon Ross 794861094cfSGordon Ross# clang defines both __GNUC__ and __clang__, therefore test for 795861094cfSGordon Ross# __clang__ ahead of __GNUC__. 796861094cfSGordon Ross 797861094cfSGordon Ross_C_PROBE_SRC = """\ 798861094cfSGordon Ross#include <stdlib.h> 799861094cfSGordon Rossint main(int argc, char **argv) { 800861094cfSGordon Ross#if defined(__SUNPRO_C) 801861094cfSGordon Rossexit(51); 802861094cfSGordon Ross#elif defined(__clang__) 803861094cfSGordon Rossexit(52); 804861094cfSGordon Ross#elif defined(__GNUC__) 805861094cfSGordon Rossexit(53); 806861094cfSGordon Ross#else 807861094cfSGordon Rossexit(99); 808861094cfSGordon Ross#endif 809861094cfSGordon Ross} 810861094cfSGordon Ross""" 811861094cfSGordon Ross 812861094cfSGordon Ross_CXX_PROBE_SRC = """\ 813861094cfSGordon Ross#include <cstdlib> 814861094cfSGordon Rossint main(int argc, char **argv) { 815861094cfSGordon Ross#if defined(__SUNPRO_CC) 816861094cfSGordon Rossexit(51); 817861094cfSGordon Ross#elif defined(__clang__) 818861094cfSGordon Rossexit(52); 819861094cfSGordon Ross#elif defined(__GNUC__) 820861094cfSGordon Rossexit(53); 821861094cfSGordon Ross#else 822861094cfSGordon Rossexit(99); 823861094cfSGordon Ross#endif 824861094cfSGordon Ross} 825861094cfSGordon Ross""" 826861094cfSGordon Ross 827f9db9ff7SGordon Rossdef sys_include_dir(root=None): 828f9db9ff7SGordon Ross """ 829f9db9ff7SGordon Ross Return the system include directory to use for -isystem/-nostdinc 830f9db9ff7SGordon Ross compiles: '<root>/usr/include' if root is given, else '/usr/include'. 831f9db9ff7SGordon Ross 832f9db9ff7SGordon Ross root is resolved by the caller from, in order of preference: the -R 833f9db9ff7SGordon Ross command-line option, the HEADER_TEST_ROOT environment variable, or 834f9db9ff7SGordon Ross None (meaning the true system root). 835f9db9ff7SGordon Ross """ 836f9db9ff7SGordon Ross if root: 837f9db9ff7SGordon Ross return os.path.join(root, 'usr/include') 838f9db9ff7SGordon Ross return '/usr/include' 839f9db9ff7SGordon Ross 840f9db9ff7SGordon Ross 841f9db9ff7SGordon Rossdef c_base_flags(root=None): 842f9db9ff7SGordon Ross """ 843f9db9ff7SGordon Ross Base flags used for all C compilations. We turn off -Wformat-security 844f9db9ff7SGordon Ross because the auto-generated tests don't pass string literals to printf 845f9db9ff7SGordon Ross family functions, which will trigger warnings in some compilers (e.g. 846f9db9ff7SGordon Ross clang-16). 847f9db9ff7SGordon Ross """ 848f9db9ff7SGordon Ross return [ 849861094cfSGordon Ross '-Wall', '-Werror', '-nostdinc', 850f9db9ff7SGordon Ross '-isystem', sys_include_dir(root), 851861094cfSGordon Ross '-Wno-format-security', 852861094cfSGordon Ross ] 853861094cfSGordon Ross 854861094cfSGordon Ross 855861094cfSGordon Rossdef _run_compiler_probe(compiler, src, ext, mflag, tmpdir): 856861094cfSGordon Ross """ 857861094cfSGordon Ross Write src to detect.ext, compile with compiler+mflag, run the result. 858861094cfSGordon Ross Returns the probe exit code, or None on compile/exec failure. 859861094cfSGordon Ross """ 860861094cfSGordon Ross srcfile = os.path.join(tmpdir, f'detect.{ext}') 861861094cfSGordon Ross exec_name = os.path.join(tmpdir, 'detect') 862861094cfSGordon Ross with open(srcfile, 'w') as f: 863861094cfSGordon Ross f.write(src) 864861094cfSGordon Ross try: 865861094cfSGordon Ross r = subprocess.run( 866861094cfSGordon Ross [compiler, mflag, srcfile, '-o', exec_name], 867861094cfSGordon Ross stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL) 868861094cfSGordon Ross if r.returncode != 0: 869861094cfSGordon Ross return None 870861094cfSGordon Ross r = subprocess.run( 871861094cfSGordon Ross [exec_name], 872861094cfSGordon Ross stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL) 873861094cfSGordon Ross return r.returncode 874861094cfSGordon Ross except OSError: 875861094cfSGordon Ross return None 876861094cfSGordon Ross 877861094cfSGordon Ross 878861094cfSGordon Rossdef _validate_c_compiler(cc, mflag, tmpdir): 879861094cfSGordon Ross """Return True if cc is a usable C compiler (gcc or clang); else False.""" 880861094cfSGordon Ross code = _run_compiler_probe(cc, _C_PROBE_SRC, 'c', mflag, tmpdir) 881861094cfSGordon Ross return code in (_COMP_GCC, _COMP_CLANG) 882861094cfSGordon Ross 883861094cfSGordon Ross 884861094cfSGordon Rossdef _validate_cxx_compiler(cc, mflag, tmpdir): 885861094cfSGordon Ross """Return True if cc is a usable C++ compiler (g++ or clang++); else False.""" 886861094cfSGordon Ross code = _run_compiler_probe(cc, _CXX_PROBE_SRC, 'cc', mflag, tmpdir) 887861094cfSGordon Ross return code in (_COMP_GCC, _COMP_CLANG) 888861094cfSGordon Ross 889861094cfSGordon Ross 890861094cfSGordon Rossdef _compiler_kind(compiler): 891861094cfSGordon Ross """ 892861094cfSGordon Ross Return 'clang' or 'gcc' by inspecting the compiler's --version output. 893861094cfSGordon Ross Used when an explicit compiler path is given and the kind cannot be 894861094cfSGordon Ross inferred from the name. 895861094cfSGordon Ross """ 896861094cfSGordon Ross result = subprocess.run( 897861094cfSGordon Ross [compiler, '--version'], 898861094cfSGordon Ross capture_output=True, text=True) 899861094cfSGordon Ross if 'clang' in result.stdout.lower(): 900861094cfSGordon Ross return 'clang' 901861094cfSGordon Ross return 'gcc' 902861094cfSGordon Ross 903861094cfSGordon Ross 904861094cfSGordon Rossdef find_c_compiler(mflag, tmpdir, explicit=None): 905861094cfSGordon Ross """ 906861094cfSGordon Ross Find a usable C compiler. Returns the compiler path. 907861094cfSGordon Ross Raises SystemExit if none is found. 908861094cfSGordon Ross """ 909861094cfSGordon Ross candidates = [explicit] if explicit else ['gcc', 'clang'] 910861094cfSGordon Ross for cc in candidates: 911861094cfSGordon Ross if _validate_c_compiler(cc, mflag, tmpdir): 912861094cfSGordon Ross return cc 913861094cfSGordon Ross if explicit: 914861094cfSGordon Ross sys.exit(f'error: C compiler {explicit!r} is not usable') 915861094cfSGordon Ross sys.exit('error: no usable C compiler found (tried gcc, clang)') 916861094cfSGordon Ross 917861094cfSGordon Ross 918861094cfSGordon Rossdef find_cxx_compiler(mflag, tmpdir, explicit=None): 919861094cfSGordon Ross """ 920861094cfSGordon Ross Find a usable C++ compiler. Returns (compiler, kind) where kind is 921861094cfSGordon Ross 'gcc' or 'clang'. Raises SystemExit if none is found. 922861094cfSGordon Ross """ 923861094cfSGordon Ross if explicit: 924861094cfSGordon Ross if not _validate_cxx_compiler(explicit, mflag, tmpdir): 925861094cfSGordon Ross sys.exit(f'error: C++ compiler {explicit!r} is not usable') 926861094cfSGordon Ross return explicit, _compiler_kind(explicit) 927861094cfSGordon Ross for cc, kind in [('g++', 'gcc'), ('clang++', 'clang')]: 928861094cfSGordon Ross if _validate_cxx_compiler(cc, mflag, tmpdir): 929861094cfSGordon Ross return cc, kind 930861094cfSGordon Ross sys.exit('error: no usable C++ compiler found (tried g++, clang++)') 931861094cfSGordon Ross 932861094cfSGordon Ross 933f9db9ff7SGordon Rossdef find_gcc_cxx_includes(compiler, root=None): 934861094cfSGordon Ross """ 935861094cfSGordon Ross Query a GCC C++ compiler for its internal include directory and return a 936861094cfSGordon Ross base_flags list with all necessary -isystem paths. 937861094cfSGordon Ross 938861094cfSGordon Ross The compiler reports its internal include directory as, e.g.: 939861094cfSGordon Ross /opt/gcc-14/lib/gcc/x86_64-pc-solaris2.11/14.2.0/include 940861094cfSGordon Ross 941861094cfSGordon Ross We parse out prefix, target triple, and version, then build: 942861094cfSGordon Ross -isystem prefix/include/c++/version 943861094cfSGordon Ross -isystem prefix/include/c++/version/target 944861094cfSGordon Ross -isystem prefix/lib/gcc/target/version/include 945f9db9ff7SGordon Ross -isystem <sys_include_dir> 946f9db9ff7SGordon Ross 947f9db9ff7SGordon Ross The compiler's own internal C++ headers always come from the real 948f9db9ff7SGordon Ross toolchain install; only the final system headers entry is redirected 949f9db9ff7SGordon Ross under root (see sys_include_dir()). 950861094cfSGordon Ross """ 951861094cfSGordon Ross result = subprocess.run( 952861094cfSGordon Ross [compiler, '-print-file-name=include'], 953861094cfSGordon Ross capture_output=True, text=True) 954861094cfSGordon Ross buf = result.stdout.strip() 955861094cfSGordon Ross 956861094cfSGordon Ross sep = '/lib/gcc/' 957861094cfSGordon Ross idx = buf.find(sep) 958861094cfSGordon Ross if idx < 0: 959861094cfSGordon Ross sys.exit(f'error: unexpected -print-file-name=include output: {buf!r}') 960861094cfSGordon Ross 961861094cfSGordon Ross prefix = buf[:idx] 962861094cfSGordon Ross rest = buf[idx + len(sep):] # "target/version/include" 963861094cfSGordon Ross parts = rest.split('/') 964861094cfSGordon Ross if len(parts) < 3: 965861094cfSGordon Ross sys.exit(f'error: cannot parse target/version from: {buf!r}') 966861094cfSGordon Ross target = parts[0] 967861094cfSGordon Ross version = parts[1] 968861094cfSGordon Ross 969861094cfSGordon Ross return [ 970861094cfSGordon Ross '-Wall', '-Werror', '-nostdinc', 971861094cfSGordon Ross '-isystem', f'{prefix}/include/c++/{version}', 972861094cfSGordon Ross '-isystem', f'{prefix}/include/c++/{version}/{target}', 973861094cfSGordon Ross '-isystem', f'{prefix}/lib/gcc/{target}/{version}/include', 974f9db9ff7SGordon Ross '-isystem', sys_include_dir(root), 975861094cfSGordon Ross '-Wno-format-security', 976861094cfSGordon Ross ] 977861094cfSGordon Ross 978861094cfSGordon Ross 979f9db9ff7SGordon Rossdef find_clang_cxx_includes(compiler, root=None): 980861094cfSGordon Ross """ 981861094cfSGordon Ross Query a clang++ compiler for its C++ include search paths by running 982861094cfSGordon Ross it in preprocessing mode with -v, then parse the include list from 983f9db9ff7SGordon Ross stderr. Returns a base_flags list with -isystem for each path found. 984f9db9ff7SGordon Ross 985f9db9ff7SGordon Ross clang always reports the real /usr/include in this list (it has no 986f9db9ff7SGordon Ross notion of an alternate root); if root is given, that entry is 987f9db9ff7SGordon Ross replaced with sys_include_dir(root) rather than appended alongside it, 988f9db9ff7SGordon Ross so proto headers take precedence instead of conflicting with the 989f9db9ff7SGordon Ross real ones. 990861094cfSGordon Ross """ 991861094cfSGordon Ross result = subprocess.run( 992861094cfSGordon Ross [compiler, '-xc++', '-E', '-v', '-'], 993861094cfSGordon Ross input='', capture_output=True, text=True) 994861094cfSGordon Ross 995861094cfSGordon Ross paths = [] 996861094cfSGordon Ross in_list = False 997861094cfSGordon Ross for line in result.stderr.splitlines(): 998861094cfSGordon Ross if line == '#include <...> search starts here:': 999861094cfSGordon Ross in_list = True 1000861094cfSGordon Ross elif line == 'End of search list.': 1001861094cfSGordon Ross break 1002861094cfSGordon Ross elif in_list: 1003861094cfSGordon Ross paths.append(line.strip()) 1004861094cfSGordon Ross 1005861094cfSGordon Ross if not paths: 1006861094cfSGordon Ross sys.exit(f'error: could not determine C++ include paths from {compiler}') 1007861094cfSGordon Ross 1008f9db9ff7SGordon Ross sys_dir = sys_include_dir(root) 1009f9db9ff7SGordon Ross if '/usr/include' in paths: 1010f9db9ff7SGordon Ross paths = [sys_dir if p == '/usr/include' else p for p in paths] 1011f9db9ff7SGordon Ross else: 1012f9db9ff7SGordon Ross paths.append(sys_dir) 1013f9db9ff7SGordon Ross 1014861094cfSGordon Ross flags = ['-Wall', '-Werror', '-nostdinc'] 1015861094cfSGordon Ross for p in paths: 1016861094cfSGordon Ross flags += ['-isystem', p] 1017861094cfSGordon Ross flags.append('-Wno-format-security') 1018861094cfSGordon Ross return flags 1019861094cfSGordon Ross 1020861094cfSGordon Ross 1021861094cfSGordon Ross# --------------------------------------------------------------------------- 1022861094cfSGordon Ross# Argument parsing and main 1023861094cfSGordon Ross# --------------------------------------------------------------------------- 1024861094cfSGordon Ross 1025861094cfSGordon Rossdef _parse_args(): 1026861094cfSGordon Ross p = argparse.ArgumentParser( 1027861094cfSGordon Ross description='Test C/C++ symbol visibility in system headers.') 1028861094cfSGordon Ross 1029861094cfSGordon Ross p.add_argument('--lang', required=True, choices=('c', 'c++'), 1030861094cfSGordon Ross help='Language to test') 1031861094cfSGordon Ross 1032861094cfSGordon Ross bits = p.add_mutually_exclusive_group(required=True) 1033861094cfSGordon Ross bits.add_argument('-m64', dest='mflag', action='store_const', const='-m64', 1034861094cfSGordon Ross help='Compile for 64-bit') 1035861094cfSGordon Ross bits.add_argument('-m32', dest='mflag', action='store_const', const='-m32', 1036861094cfSGordon Ross help='Compile for 32-bit') 1037861094cfSGordon Ross 1038861094cfSGordon Ross p.add_argument('-c', dest='compiler', metavar='COMPILER', default=None, 1039861094cfSGordon Ross help='Explicit compiler path') 1040861094cfSGordon Ross p.add_argument('-d', dest='debug', action='store_true', 1041861094cfSGordon Ross help='Show probe and compiler output on failure') 1042861094cfSGordon Ross p.add_argument('-D', dest='extra_debug', action='store_true', 1043f9db9ff7SGordon Ross help='Also show compiler command and probe program for ' 1044f9db9ff7SGordon Ross 'every test, not just failures (implies -d)') 1045861094cfSGordon Ross p.add_argument('-e', dest='env', metavar='ENV', default=None, 1046861094cfSGordon Ross help='Narrow to one environment name') 1047861094cfSGordon Ross p.add_argument('-f', dest='force', action='store_true', 1048861094cfSGordon Ross help='Continue after failures') 1049861094cfSGordon Ross p.add_argument('-j', dest='j', metavar='N', type=int, default=None, 1050861094cfSGordon Ross help='Number of parallel jobs (default: SYMBOL_TEST_JOBS or 4)') 1051*e1e6b944SGordon Ross p.add_argument('--positive-coverage', metavar='NAME', default=None, 1052*e1e6b944SGordon Ross help='Ensure an expected-success test compiles the primary ' 1053*e1e6b944SGordon Ross 'header in every environment named by NAME; by default ' 1054*e1e6b944SGordon Ross 'all declared environments require positive coverage') 1055861094cfSGordon Ross p.add_argument('-s', dest='sym', metavar='SYM', default=None, 1056861094cfSGordon Ross help='Narrow to one symbol name') 1057861094cfSGordon Ross 1058861094cfSGordon Ross p.add_argument('-C', dest='compiler_check', action='store_true', 1059861094cfSGordon Ross help='Check compiler only, do not run tests') 1060861094cfSGordon Ross 1061f9db9ff7SGordon Ross p.add_argument('-R', dest='root', metavar='ROOT', default=None, 1062f9db9ff7SGordon Ross help='Alternate root directory (e.g. a proto area) whose ' 1063f9db9ff7SGordon Ross 'ROOT/usr/include is tested instead of the default ' 1064f9db9ff7SGordon Ross '(default: $HEADER_TEST_ROOT/usr/include if that ' 1065f9db9ff7SGordon Ross 'environment variable is set, else /usr/include)') 1066f9db9ff7SGordon Ross 1067861094cfSGordon Ross p.add_argument('env_cfg', nargs='?', help='Environment config file') 1068861094cfSGordon Ross p.add_argument('sym_cfgs', nargs='*', metavar='sym_cfg', 1069861094cfSGordon Ross help='One or more symbols config files') 1070861094cfSGordon Ross 1071861094cfSGordon Ross args = p.parse_args() 1072861094cfSGordon Ross if args.extra_debug: 1073861094cfSGordon Ross args.debug = True 1074861094cfSGordon Ross jobs = 4 1075861094cfSGordon Ross env_jobs = os.environ.get('SYMBOL_TEST_JOBS') 1076861094cfSGordon Ross if env_jobs is not None: 1077861094cfSGordon Ross jobs = int(env_jobs) 1078861094cfSGordon Ross if args.j is not None: 1079861094cfSGordon Ross jobs = args.j 1080861094cfSGordon Ross args.j = jobs 1081f9db9ff7SGordon Ross if args.root is None: 1082f9db9ff7SGordon Ross args.root = os.environ.get('HEADER_TEST_ROOT') 1083861094cfSGordon Ross if not args.compiler_check and not args.env_cfg: 1084861094cfSGordon Ross p.error('env_cfg is required unless -C is specified') 1085861094cfSGordon Ross if not args.compiler_check and not args.sym_cfgs: 1086861094cfSGordon Ross p.error('at least one sym_cfg is required unless -C is specified') 1087861094cfSGordon Ross return args 1088861094cfSGordon Ross 1089861094cfSGordon Ross 1090861094cfSGordon Rossdef main(): 1091861094cfSGordon Ross args = _parse_args() 1092861094cfSGordon Ross mflag = args.mflag 1093861094cfSGordon Ross arch = '64-bit' if mflag == '-m64' else '32-bit' 1094861094cfSGordon Ross 1095861094cfSGordon Ross with tempfile.TemporaryDirectory() as tmpdir: 1096861094cfSGordon Ross if args.lang == 'c': 1097861094cfSGordon Ross compiler = find_c_compiler(mflag, tmpdir, args.compiler) 1098f9db9ff7SGordon Ross base_flags = c_base_flags(args.root) 1099861094cfSGordon Ross else: 1100861094cfSGordon Ross # kind is 'gcc' or 'clang', used to select the right 1101861094cfSGordon Ross # include path discovery method. 1102861094cfSGordon Ross compiler, kind = find_cxx_compiler(mflag, tmpdir, args.compiler) 1103861094cfSGordon Ross if kind == 'gcc': 1104f9db9ff7SGordon Ross base_flags = find_gcc_cxx_includes(compiler, args.root) 1105861094cfSGordon Ross else: 1106f9db9ff7SGordon Ross base_flags = find_clang_cxx_includes(compiler, args.root) 1107861094cfSGordon Ross 1108861094cfSGordon Ross if args.compiler_check: 1109861094cfSGordon Ross sys.exit(0) 1110861094cfSGordon Ross 1111861094cfSGordon Ross env_cfg = EnvConfig(args.lang) 1112861094cfSGordon Ross env_cfg.load(args.env_cfg) 1113861094cfSGordon Ross 1114861094cfSGordon Ross sym_cfg = SymConfig() 1115861094cfSGordon Ross for path in args.sym_cfgs: 1116861094cfSGordon Ross sym_cfg.load(path) 1117861094cfSGordon Ross 1118*e1e6b944SGordon Ross # Named groups may intentionally omit declared environments, so the 1119*e1e6b944SGordon Ross # default is the complete environment configuration. One could 1120*e1e6b944SGordon Ross # also make "ALL" the default but that would encode expectations 1121*e1e6b944SGordon Ross # on that being defined in the environment config file. 1122*e1e6b944SGordon Ross if args.positive_coverage is None: 1123*e1e6b944SGordon Ross positive_coverage = set(env_cfg.envs) 1124*e1e6b944SGordon Ross else: 1125*e1e6b944SGordon Ross positive_coverage = env_cfg.expand(args.positive_coverage) 1126*e1e6b944SGordon Ross 1127861094cfSGordon Ross ok = TestDriver().run( 1128*e1e6b944SGordon Ross sym_cfg, env_cfg, positive_coverage, compiler, 1129861094cfSGordon Ross mflag, arch, base_flags, tmpdir, args) 1130861094cfSGordon Ross 1131861094cfSGordon Ross sys.exit(0 if ok else 1) 1132861094cfSGordon Ross 1133861094cfSGordon Ross 1134861094cfSGordon Rossif __name__ == '__main__': 1135861094cfSGordon Ross main() 1136