1#!@PYTHON@ 2# 3# This file and its contents are supplied under the terms of the 4# Common Development and Distribution License ("CDDL"), version 1.0. 5# You may only use this file in accordance with the terms of version 6# 1.0 of the CDDL. 7# 8# A full copy of the text of the CDDL should have accompanied this 9# source. A copy of the CDDL is also available via the Internet at 10# http://www.illumos.org/license/CDDL. 11# 12 13# 14# Copyright 2026 Gordon W. Ross 15# 16 17""" 18symbol_test.py - C and C++ symbol visibility test driver. 19 20Reads an environment config file and one or more symbols config files, 21generates probe programs for each (symbol, environment) pair, compiles 22them, and reports pass/fail. Supports parallel compilation jobs. 23 24This program is run separately for 64-bit compiles and (where supported) 2532-bit compiles by the test driver scripts. See the setup.ksh scripts in 26the ../c-symbols/ and ../cxx-symbols/ directories for examples. 27 28See also tests/common/README.md. 29""" 30 31import argparse 32import io 33import os 34import signal 35import subprocess 36import sys 37import tempfile 38import threading 39from concurrent.futures import ThreadPoolExecutor 40 41 42# --------------------------------------------------------------------------- 43# parse_cfg - shared cfg file parser 44# --------------------------------------------------------------------------- 45 46def parse_cfg(fileobj, filename, handlers): 47 """ 48 Parse a cfg file-like object, dispatching each directive line to handlers. 49 50 Handles continuation lines (trailing backslash), blank lines, and comments 51 (lines starting with '#'). Each non-blank, non-comment line is split on 52 '|' with each field stripped, then dispatched as: 53 54 handlers[keyword](fields, filename, lineno) 55 56 Unknown keywords cause an error via sys.exit(). 57 """ 58 lineno = 0 59 accum = '' 60 for raw in fileobj: 61 lineno += 1 62 text = raw.rstrip('\n').rstrip('\r') 63 if text.endswith('\\'): 64 accum += text[:-1] 65 continue 66 accum += text 67 line = accum.strip() 68 accum = '' 69 if not line or line.startswith('#'): 70 continue 71 parts = [p.strip() for p in line.split('|')] 72 keyword, fields = parts[0], parts[1:] 73 if keyword not in handlers: 74 sys.exit( 75 f'error: {filename}:{lineno}: unknown keyword {keyword!r}') 76 handlers[keyword](fields, filename, lineno) 77 78 79# --------------------------------------------------------------------------- 80# CompileEnv - one compilation environment (name, lang standard, defines) 81# --------------------------------------------------------------------------- 82 83class CompileEnv: 84 """One compilation environment as defined by an 'env' line in the cfg.""" 85 86 def __init__(self, name, lang, defs): 87 self.name = name 88 self.lang = lang # e.g. 'c++98', 'c99' 89 self.defs = defs # preprocessor defines string, may be empty 90 91 92# --------------------------------------------------------------------------- 93# EnvConfig - loads and represents the env cfg file 94# See test_parse_env.py for examples of method calls and results. 95# --------------------------------------------------------------------------- 96 97class EnvConfig: 98 """ 99 Loads a compilation environment config file (c-symbols-env.cfg or 100 cxx-symbols-env.cfg) and provides environment and group lookup. 101 102 Public attributes: 103 envs: dict name -> CompileEnv 104 groups: dict name -> frozenset of env names 105 """ 106 107 def __init__(self, lang): 108 self.lang = lang 109 self.envs = {} 110 self.groups = {} 111 112 def load(self, path): 113 """Load env cfg from a file path.""" 114 # Locations searched for the file: 115 # 1. path as given 116 # 2. $STF_SUITE/cfg/<path> 117 # 3. cfg/<path> 118 candidates = [path] 119 if not os.path.isabs(path): 120 stf = os.environ.get('STF_SUITE', '../..') 121 candidates.append(os.path.join(stf, 'cfg', path)) 122 candidates.append(os.path.join('cfg', path)) 123 124 for candidate in candidates: 125 if os.path.exists(candidate): 126 with open(candidate) as f: 127 self._parse(f, filename=candidate) 128 return 129 130 sys.exit(f'error: env cfg file not found: {path}') 131 132 def _parse(self, fileobj, filename='<input>'): 133 """ 134 Parse a file-like object as an environment config file. 135 136 self.lang must be 'c' or 'c++'. Each env line's lang field is 137 validated against this value; a mismatch is an error. 138 """ 139 handlers = { 140 'env': lambda f, fn, ln: self._do_env(f, fn, ln), 141 'env_group': self._do_env_group, 142 } 143 parse_cfg(fileobj, filename, handlers) 144 145 def _do_env(self, fields, filename, lineno): 146 if len(fields) != 3: 147 sys.exit( 148 f'error: {filename}:{lineno}: env: expected 3 fields, ' 149 f'got {len(fields)}') 150 151 name, env_lang, defs = fields 152 153 # Validate lang consistency 154 is_cxx = env_lang.startswith('c++') 155 if self.lang == 'c++' and not is_cxx: 156 sys.exit( 157 f'error: --lang=c++ but {filename} line {lineno} ' 158 f'has lang {env_lang!r}') 159 if self.lang == 'c' and is_cxx: 160 sys.exit( 161 f'error: --lang=c but {filename} line {lineno} ' 162 f'has lang {env_lang!r}') 163 164 self.envs[name] = CompileEnv(name=name, lang=env_lang, defs=defs) 165 166 def _do_env_group(self, fields, filename, lineno): 167 if len(fields) != 2: 168 sys.exit( 169 f'error: {filename}:{lineno}: env_group: expected 2 fields, ' 170 f'got {len(fields)}') 171 172 name, members_str = fields 173 members = set() 174 for member in members_str.split(): 175 if member in self.envs: 176 members.add(member) 177 elif member in self.groups: 178 members |= self.groups[member] 179 else: 180 sys.exit( 181 f'error: {filename}:{lineno}: ' 182 f'reference to undefined env {member!r}') 183 184 self.groups[name] = frozenset(members) 185 186 def resolve(self, spec): 187 """ 188 Parse an env spec string like 'CXX98+ -CXX11 +CXX98'. 189 190 Returns (test_set, need_set) where both are sets of env names: 191 test_set - all envs to compile 192 need_set - envs where compilation must succeed (pass) 193 194 Bare name or +name -> added to both test_set and need_set. 195 -name -> added to test_set only (must fail). 196 """ 197 test_set = set() 198 need_set = set() 199 200 for token in spec.split(): 201 if token.startswith('+'): 202 act = True 203 token = token[1:] 204 elif token.startswith('-'): 205 act = False 206 token = token[1:] 207 else: 208 act = True 209 210 # Expand token - may be a single env or a group 211 expanded = self._expand(token) 212 test_set |= expanded 213 if act: 214 need_set |= expanded 215 else: 216 need_set -= expanded 217 218 return (test_set, need_set) 219 220 def _expand(self, name): 221 """Return frozenset of env names for a single env name or group name.""" 222 if name in self.envs: 223 return frozenset({name}) 224 if name in self.groups: 225 return self.groups[name] 226 sys.exit(f'error: reference to undefined env or group {name!r}') 227 228 229# --------------------------------------------------------------------------- 230# SymEntry - one symbol entry from a symbols config file 231# --------------------------------------------------------------------------- 232 233class SymEntry: 234 """One test entry as parsed from a symbols config file.""" 235 236 def __init__(self, directive, symbol, env_spec, headers, 237 rtype=None, atypes=None, defval=None): 238 self.directive = directive # 'func' | 'type' | 'value' | 'define' 239 self.symbol = symbol # name to test 240 self.env_spec = env_spec # raw env spec string e.g. 'CXX98+ -CXX11' 241 self.headers = headers # list of header filenames 242 self.rtype = rtype # return/declaration type (not for define) 243 self.atypes = atypes if atypes is not None else [] 244 self.defval = defval # expected value for 'define', else None 245 246 247# --------------------------------------------------------------------------- 248# SymConfig - loads symbols config files 249# See test_parse_sym.py for examples of method calls and results. 250# --------------------------------------------------------------------------- 251 252class SymConfig: 253 """ 254 Loads one or more symbols config files. 255 256 Public attributes: 257 entries: list of SymEntry, in file order 258 """ 259 260 def __init__(self): 261 self.entries = [] 262 263 def load(self, path): 264 """Load symbols config from a file path, with STF_SUITE search.""" 265 candidates = [path] 266 if not os.path.isabs(path): 267 stf = os.environ.get('STF_SUITE', '../..') 268 candidates.append(os.path.join(stf, 'cfg', path)) 269 candidates.append(os.path.join('cfg', path)) 270 271 for candidate in candidates: 272 if os.path.exists(candidate): 273 with open(candidate) as f: 274 self._parse(f, filename=candidate) 275 return 276 277 sys.exit(f'error: symbols config file not found: {path}') 278 279 def _parse(self, fileobj, filename='<input>'): 280 """Parse a file-like object as a symbols config file.""" 281 handlers = { 282 'type': self._do_type, 283 'value': self._do_value, 284 'define': self._do_define, 285 'func': self._do_func, 286 } 287 parse_cfg(fileobj, filename, handlers) 288 289 @staticmethod 290 def _split_list(s): 291 """Split a semicolon-separated field, stripping each item.""" 292 return [item.strip() for item in s.split(';') if item.strip()] 293 294 def _do_type(self, fields, filename, lineno): 295 # type | decl | headers | envs 296 if len(fields) != 3: 297 sys.exit( 298 f'error: {filename}:{lineno}: type: expected 3 fields, ' 299 f'got {len(fields)}') 300 decl, hdrs, envs = fields 301 self.entries.append(SymEntry( 302 directive='type', 303 symbol=decl, 304 rtype=decl, 305 headers=self._split_list(hdrs), 306 env_spec=envs, 307 )) 308 309 def _do_value(self, fields, filename, lineno): 310 # value | name | type | headers | envs 311 if len(fields) != 4: 312 sys.exit( 313 f'error: {filename}:{lineno}: value: expected 4 fields, ' 314 f'got {len(fields)}') 315 name, rtype, hdrs, envs = fields 316 self.entries.append(SymEntry( 317 directive='value', 318 symbol=name, 319 rtype=rtype, 320 headers=self._split_list(hdrs), 321 env_spec=envs, 322 )) 323 324 def _do_define(self, fields, filename, lineno): 325 # define | name | value | headers | envs (value may be empty) 326 if len(fields) != 4: 327 sys.exit( 328 f'error: {filename}:{lineno}: define: expected 4 fields, ' 329 f'got {len(fields)}') 330 name, defval, hdrs, envs = fields 331 self.entries.append(SymEntry( 332 directive='define', 333 symbol=name, 334 defval=defval if defval else None, 335 headers=self._split_list(hdrs), 336 env_spec=envs, 337 )) 338 339 def _do_func(self, fields, filename, lineno): 340 # func | name | rtype | atypes | headers | envs 341 if len(fields) != 5: 342 sys.exit( 343 f'error: {filename}:{lineno}: func: expected 5 fields, ' 344 f'got {len(fields)}') 345 name, rtype, atypes, hdrs, envs = fields 346 self.entries.append(SymEntry( 347 directive='func', 348 symbol=name, 349 rtype=rtype, 350 atypes=self._split_list(atypes), 351 headers=self._split_list(hdrs), 352 env_spec=envs, 353 )) 354 355 356# --------------------------------------------------------------------------- 357# ProbeGen - generates probe program source for a (SymEntry, lang) pair 358# See test_gen_probe.py for examples of method calls and results. 359# --------------------------------------------------------------------------- 360 361class ProbeGen: 362 363 RESULT_MACRO = ( 364 '#if __cplusplus >= 201103L\n' 365 '#define RESULT(v) result{v}\n' 366 '#else\n' 367 '#define RESULT(v) result = (v)\n' 368 '#endif\n' 369 ) 370 371 @staticmethod 372 def gen_probe(entry, lang): 373 """ 374 Generate probe program source text for a SymEntry and language. 375 376 """ 377 out = [] 378 379 for h in entry.headers: 380 out.append(f'#include <{h}>\n') 381 382 rtype = entry.rtype or '' 383 prefix, suffix = ProbeGen._split_rtype(rtype) 384 is_fnptr = suffix != '' 385 386 # Emit the RESULT() macro for C++ func probes with a non-void, 387 # non-fnptr return value (brace-init catches narrowing errors in 388 # C++11+; plain assignment used for C++98). 389 has_rtype = (entry.directive == 'func' and 390 rtype != '' and rtype != 'void') 391 use_result = has_rtype and not is_fnptr and lang == 'c++' 392 393 if use_result: 394 out.append(ProbeGen.RESULT_MACRO) 395 396 # Emit return type prefix + trailing space (matches C programs' 397 # unconditional addprogch(' ') after the rtype loop). 398 if rtype: 399 out.append(prefix + ' ') 400 401 if entry.directive == 'type': 402 out.append('test_type;\n') 403 404 elif entry.directive == 'value': 405 out.append(f'test_value{suffix};\n') 406 out.append('void\ntest_func(void)\n{\n') 407 out.append(f'\ttest_value = {entry.symbol};\n}}\n') 408 409 elif entry.directive == 'define': 410 out.append(f'#if !defined({entry.symbol})') 411 if entry.defval: 412 out.append(f' || {entry.symbol} != {entry.defval}') 413 out.append(f'\n#error {entry.symbol} is not defined or has the wrong value') 414 out.append('\n#endif\n') 415 out.append('\n') 416 417 elif entry.directive == 'func': 418 arglist = ProbeGen._build_arglist(entry.atypes) 419 out.append(f'\ntest_func({arglist}){suffix}\n{{\n\t') 420 421 call = f'{entry.symbol}({ProbeGen._build_callargs(entry.atypes)})' 422 423 if use_result: 424 out.append(f'{rtype} RESULT({call});\n\treturn result;\n}}') 425 elif has_rtype or is_fnptr: 426 out.append(f'return {call};\n}}') 427 else: 428 out.append(f'{call};\n}}') 429 out.append('\n') 430 431 return ''.join(out) 432 433 @staticmethod 434 def _split_rtype(rtype): 435 """ 436 Split rtype for function pointer support. 437 438 For 'void (*)(int)': returns ('void (*', ')(int)') 439 For 'double': returns ('double', '') 440 """ 441 idx = rtype.find('(*') 442 if idx >= 0: 443 return rtype[:idx + 2], rtype[idx + 2:] 444 return rtype, '' 445 446 @staticmethod 447 def _build_arglist(atypes): 448 """Build the formal parameter list string, matching C arg-name insertion.""" 449 if not atypes: 450 return 'void' 451 parts = [] 452 for i, atype in enumerate(atypes): 453 if atype == '': 454 parts.append('void') 455 elif atype == 'void': 456 parts.append('void') 457 elif '(*' in atype: 458 idx = atype.index('(*') 459 parts.append(f'{atype[:idx + 2]}a{i}{atype[idx + 2:]}') 460 elif '[' in atype: 461 idx = atype.index('[') 462 parts.append(f'{atype[:idx]}a{i}{atype[idx:]}') 463 else: 464 parts.append(f'{atype} a{i}') 465 return ', '.join(parts) 466 467 @staticmethod 468 def _build_callargs(atypes): 469 """Build actual argument names for the function call, skipping void/empty.""" 470 return ', '.join( 471 f'a{i}' for i, atype in enumerate(atypes) 472 if atype not in ('', 'void') 473 ) 474 475 476# --------------------------------------------------------------------------- 477# Job - inputs to a single (symbol, env) compilation 478# --------------------------------------------------------------------------- 479 480class Job: 481 """All inputs needed to run one compilation job in a worker thread.""" 482 483 __slots__ = ('index', 'entry', 'env', 'expect_pass', 'lang', 484 'compiler', 'mflag', 'arch', 'std_flag', 'base_flags', 485 'tmpdir', 'debug', 'extra_debug', 'force') 486 487 def __init__(self, index, entry, env, expect_pass, lang, 488 compiler, mflag, arch, std_flag, base_flags, 489 tmpdir, debug, extra_debug, force): 490 self.index = index 491 self.entry = entry 492 self.env = env 493 self.expect_pass = expect_pass 494 self.lang = lang 495 self.compiler = compiler 496 self.mflag = mflag 497 self.arch = arch # '64-bit' or '32-bit' 498 self.std_flag = std_flag # e.g. '-std=c99' 499 self.base_flags = base_flags # list of flags common to all jobs 500 self.tmpdir = tmpdir 501 self.debug = debug # -d: show probe + compiler output on failure 502 self.extra_debug = extra_debug # -D: also show compiler command on pass 503 self.force = force # -f: continue after failures 504 505 506# --------------------------------------------------------------------------- 507# TestDriver - builds job list, drives thread pool, reports results 508# --------------------------------------------------------------------------- 509 510class TestDriver: 511 """ 512 Expands symbol entries × environments into jobs, runs them in a thread 513 pool, and reports results. 514 """ 515 516 def run(self, entries, env_config, compiler, mflag, arch, 517 base_flags, tmpdir, opts): 518 """ 519 Run all (symbol, env) compilations. 520 521 Returns True if all tests passed (and not interrupted), else False. 522 """ 523 lock = threading.Lock() 524 stop = threading.Event() 525 counters = {'pass': 0, 'fail': 0} 526 527 orig_sigint = signal.getsignal(signal.SIGINT) 528 orig_sigterm = signal.getsignal(signal.SIGTERM) 529 530 def handle_signal(signum, frame): 531 stop.set() 532 533 signal.signal(signal.SIGINT, handle_signal) 534 signal.signal(signal.SIGTERM, handle_signal) 535 536 # run_job() is the worker function called by each thread in the pool. 537 # It captures all output for one job in a local buffer, then acquires 538 # the lock to write (flush) it contiguously to the output stream. 539 def run_job(job): 540 if stop.is_set(): 541 return 542 543 # Generate probe source and write to temp file. 544 src = ProbeGen.gen_probe(job.entry, job.lang) 545 ext = 'cc' if job.lang == 'c++' else 'c' 546 base = os.path.join(job.tmpdir, f'job-{job.index}') 547 srcfile = f'{base}.{ext}' 548 objfile = f'{base}.o' 549 logfile = f'{base}.log' 550 551 with open(srcfile, 'w') as f: 552 f.write(src) 553 554 # Build compiler command. 555 env_defs = job.env.defs.split() if job.env.defs else [] 556 cmd = ([job.compiler, job.mflag, job.std_flag] + 557 job.base_flags + env_defs + 558 ['-c', srcfile, '-o', objfile]) 559 560 with open(logfile, 'w') as lf: 561 proc = subprocess.run(cmd, stdout=lf, stderr=lf) 562 563 compile_ok = (proc.returncode == 0) 564 passed = (compile_ok == job.expect_pass) 565 566 # Output buffer (flushed at end of job) 567 sign = '+' if job.expect_pass else '-' 568 label = f'{job.entry.symbol} : {sign}{job.env.name} ({job.arch})' 569 out = [f'TEST STARTING {label}:'] 570 571 if job.extra_debug: 572 out.append(f'TEST DEBUG {label}: command: {" ".join(cmd)}') 573 574 if job.debug and not passed: 575 out.append(f'TEST DEBUG {label}: probe program:') 576 for line in src.splitlines(): 577 out.append(f'TEST DEBUG {label}: {line}') 578 with open(logfile) as lf: 579 cc_out = lf.read().strip() 580 if cc_out: 581 out.append(f'TEST DEBUG {label}: compiler output:') 582 for line in cc_out.splitlines(): 583 out.append(f'TEST DEBUG {label}: {line}') 584 585 if passed: 586 out.append(f'TEST PASS: {label}') 587 else: 588 verb = 'FAILING' if job.force else 'FAILED' 589 reason = (f'error compiling in {job.env.name}' 590 if job.expect_pass 591 else f'symbol visible in {job.env.name}') 592 out.append(f'TEST {verb} {label}: {reason}') 593 594 with lock: 595 print('\n'.join(out), flush=True) 596 if passed: 597 counters['pass'] += 1 598 else: 599 counters['fail'] += 1 600 if not job.force: 601 stop.set() 602 603 # Build the list of jobs to run. 604 jobs = [] 605 for entry in entries: 606 if opts.sym and entry.symbol != opts.sym: 607 continue 608 test_set, need_set = env_config.resolve(entry.env_spec) 609 if opts.env: 610 narrow, _ = env_config.resolve(opts.env) 611 test_set &= narrow 612 need_set &= narrow 613 for env_name in sorted(test_set): 614 env = env_config.envs[env_name] 615 jobs.append(Job( 616 index=len(jobs), 617 entry=entry, 618 env=env, 619 expect_pass=(env_name in need_set), 620 lang=opts.lang, 621 compiler=compiler, 622 mflag=mflag, 623 arch=arch, 624 std_flag=f'-std={env.lang}', 625 base_flags=base_flags, 626 tmpdir=tmpdir, 627 debug=opts.debug, 628 extra_debug=opts.extra_debug, 629 force=opts.force, 630 )) 631 632 # The ThreadPoolExecutor runs up to opts.j worker threads concurrently. 633 # executor.submit() queues each job; the pool calls run_job(job) in a 634 # worker thread. Exiting the "with" block waits for all submitted jobs 635 # to finish before proceeding. 636 with ThreadPoolExecutor(max_workers=opts.j) as executor: 637 for job in jobs: 638 executor.submit(run_job, job) 639 640 signal.signal(signal.SIGINT, orig_sigint) 641 signal.signal(signal.SIGTERM, orig_sigterm) 642 643 passes = counters['pass'] 644 total = passes + counters['fail'] 645 if passes == total: 646 print(f'TEST SUMMARY: {passes} / {total} (ok)') 647 else: 648 print(f'TEST SUMMARY: {passes} / {total} ({total - passes} failing)') 649 650 return counters['fail'] == 0 and not stop.is_set() 651 652 653# --------------------------------------------------------------------------- 654# Compiler detection 655# --------------------------------------------------------------------------- 656 657# Exit codes emitted by the compiler-detection probe. 658_COMP_STUDIO = 51 659_COMP_CLANG = 52 660_COMP_GCC = 53 661_COMP_UNKNOWN = 99 662 663# clang defines both __GNUC__ and __clang__, therefore test for 664# __clang__ ahead of __GNUC__. 665 666_C_PROBE_SRC = """\ 667#include <stdlib.h> 668int main(int argc, char **argv) { 669#if defined(__SUNPRO_C) 670exit(51); 671#elif defined(__clang__) 672exit(52); 673#elif defined(__GNUC__) 674exit(53); 675#else 676exit(99); 677#endif 678} 679""" 680 681_CXX_PROBE_SRC = """\ 682#include <cstdlib> 683int main(int argc, char **argv) { 684#if defined(__SUNPRO_CC) 685exit(51); 686#elif defined(__clang__) 687exit(52); 688#elif defined(__GNUC__) 689exit(53); 690#else 691exit(99); 692#endif 693} 694""" 695 696# Base flags used for all C compilations. 697# We turn off -Wformat-security because the auto-generated tests don't pass 698# string literals to printf family functions, which will trigger warnings in 699# some compilers (e.g. clang-16). 700_C_BASE_FLAGS = [ 701 '-Wall', '-Werror', '-nostdinc', 702 '-isystem', '/usr/include', 703 '-Wno-format-security', 704] 705 706 707def _run_compiler_probe(compiler, src, ext, mflag, tmpdir): 708 """ 709 Write src to detect.ext, compile with compiler+mflag, run the result. 710 Returns the probe exit code, or None on compile/exec failure. 711 """ 712 srcfile = os.path.join(tmpdir, f'detect.{ext}') 713 exec_name = os.path.join(tmpdir, 'detect') 714 with open(srcfile, 'w') as f: 715 f.write(src) 716 try: 717 r = subprocess.run( 718 [compiler, mflag, srcfile, '-o', exec_name], 719 stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL) 720 if r.returncode != 0: 721 return None 722 r = subprocess.run( 723 [exec_name], 724 stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL) 725 return r.returncode 726 except OSError: 727 return None 728 729 730def _validate_c_compiler(cc, mflag, tmpdir): 731 """Return True if cc is a usable C compiler (gcc or clang); else False.""" 732 code = _run_compiler_probe(cc, _C_PROBE_SRC, 'c', mflag, tmpdir) 733 return code in (_COMP_GCC, _COMP_CLANG) 734 735 736def _validate_cxx_compiler(cc, mflag, tmpdir): 737 """Return True if cc is a usable C++ compiler (g++ or clang++); else False.""" 738 code = _run_compiler_probe(cc, _CXX_PROBE_SRC, 'cc', mflag, tmpdir) 739 return code in (_COMP_GCC, _COMP_CLANG) 740 741 742def _compiler_kind(compiler): 743 """ 744 Return 'clang' or 'gcc' by inspecting the compiler's --version output. 745 Used when an explicit compiler path is given and the kind cannot be 746 inferred from the name. 747 """ 748 result = subprocess.run( 749 [compiler, '--version'], 750 capture_output=True, text=True) 751 if 'clang' in result.stdout.lower(): 752 return 'clang' 753 return 'gcc' 754 755 756def find_c_compiler(mflag, tmpdir, explicit=None): 757 """ 758 Find a usable C compiler. Returns the compiler path. 759 Raises SystemExit if none is found. 760 """ 761 candidates = [explicit] if explicit else ['gcc', 'clang'] 762 for cc in candidates: 763 if _validate_c_compiler(cc, mflag, tmpdir): 764 return cc 765 if explicit: 766 sys.exit(f'error: C compiler {explicit!r} is not usable') 767 sys.exit('error: no usable C compiler found (tried gcc, clang)') 768 769 770def find_cxx_compiler(mflag, tmpdir, explicit=None): 771 """ 772 Find a usable C++ compiler. Returns (compiler, kind) where kind is 773 'gcc' or 'clang'. Raises SystemExit if none is found. 774 """ 775 if explicit: 776 if not _validate_cxx_compiler(explicit, mflag, tmpdir): 777 sys.exit(f'error: C++ compiler {explicit!r} is not usable') 778 return explicit, _compiler_kind(explicit) 779 for cc, kind in [('g++', 'gcc'), ('clang++', 'clang')]: 780 if _validate_cxx_compiler(cc, mflag, tmpdir): 781 return cc, kind 782 sys.exit('error: no usable C++ compiler found (tried g++, clang++)') 783 784 785def find_gcc_cxx_includes(compiler): 786 """ 787 Query a GCC C++ compiler for its internal include directory and return a 788 base_flags list with all necessary -isystem paths. 789 790 The compiler reports its internal include directory as, e.g.: 791 /opt/gcc-14/lib/gcc/x86_64-pc-solaris2.11/14.2.0/include 792 793 We parse out prefix, target triple, and version, then build: 794 -isystem prefix/include/c++/version 795 -isystem prefix/include/c++/version/target 796 -isystem prefix/lib/gcc/target/version/include 797 -isystem /usr/include 798 """ 799 result = subprocess.run( 800 [compiler, '-print-file-name=include'], 801 capture_output=True, text=True) 802 buf = result.stdout.strip() 803 804 sep = '/lib/gcc/' 805 idx = buf.find(sep) 806 if idx < 0: 807 sys.exit(f'error: unexpected -print-file-name=include output: {buf!r}') 808 809 prefix = buf[:idx] 810 rest = buf[idx + len(sep):] # "target/version/include" 811 parts = rest.split('/') 812 if len(parts) < 3: 813 sys.exit(f'error: cannot parse target/version from: {buf!r}') 814 target = parts[0] 815 version = parts[1] 816 817 return [ 818 '-Wall', '-Werror', '-nostdinc', 819 '-isystem', f'{prefix}/include/c++/{version}', 820 '-isystem', f'{prefix}/include/c++/{version}/{target}', 821 '-isystem', f'{prefix}/lib/gcc/{target}/{version}/include', 822 '-isystem', '/usr/include', 823 '-Wno-format-security', 824 ] 825 826 827def find_clang_cxx_includes(compiler): 828 """ 829 Query a clang++ compiler for its C++ include search paths by running 830 it in preprocessing mode with -v, then parse the include list from 831 stderr. Returns a base_flags list with -isystem for each path found, 832 plus -isystem /usr/include. 833 """ 834 result = subprocess.run( 835 [compiler, '-xc++', '-E', '-v', '-'], 836 input='', capture_output=True, text=True) 837 838 paths = [] 839 in_list = False 840 for line in result.stderr.splitlines(): 841 if line == '#include <...> search starts here:': 842 in_list = True 843 elif line == 'End of search list.': 844 break 845 elif in_list: 846 paths.append(line.strip()) 847 848 if not paths: 849 sys.exit(f'error: could not determine C++ include paths from {compiler}') 850 851 flags = ['-Wall', '-Werror', '-nostdinc'] 852 for p in paths: 853 flags += ['-isystem', p] 854 if '/usr/include' not in paths: 855 flags += ['-isystem', '/usr/include'] 856 flags.append('-Wno-format-security') 857 return flags 858 859 860# --------------------------------------------------------------------------- 861# Argument parsing and main 862# --------------------------------------------------------------------------- 863 864def _parse_args(): 865 p = argparse.ArgumentParser( 866 description='Test C/C++ symbol visibility in system headers.') 867 868 p.add_argument('--lang', required=True, choices=('c', 'c++'), 869 help='Language to test') 870 871 bits = p.add_mutually_exclusive_group(required=True) 872 bits.add_argument('-m64', dest='mflag', action='store_const', const='-m64', 873 help='Compile for 64-bit') 874 bits.add_argument('-m32', dest='mflag', action='store_const', const='-m32', 875 help='Compile for 32-bit') 876 877 p.add_argument('-c', dest='compiler', metavar='COMPILER', default=None, 878 help='Explicit compiler path') 879 p.add_argument('-d', dest='debug', action='store_true', 880 help='Show probe and compiler output on failure') 881 p.add_argument('-D', dest='extra_debug', action='store_true', 882 help='Also show compiler command (implies -d)') 883 p.add_argument('-e', dest='env', metavar='ENV', default=None, 884 help='Narrow to one environment name') 885 p.add_argument('-f', dest='force', action='store_true', 886 help='Continue after failures') 887 p.add_argument('-j', dest='j', metavar='N', type=int, default=None, 888 help='Number of parallel jobs (default: SYMBOL_TEST_JOBS or 4)') 889 p.add_argument('-s', dest='sym', metavar='SYM', default=None, 890 help='Narrow to one symbol name') 891 892 p.add_argument('-C', dest='compiler_check', action='store_true', 893 help='Check compiler only, do not run tests') 894 895 p.add_argument('env_cfg', nargs='?', help='Environment config file') 896 p.add_argument('sym_cfgs', nargs='*', metavar='sym_cfg', 897 help='One or more symbols config files') 898 899 args = p.parse_args() 900 if args.extra_debug: 901 args.debug = True 902 jobs = 4 903 env_jobs = os.environ.get('SYMBOL_TEST_JOBS') 904 if env_jobs is not None: 905 jobs = int(env_jobs) 906 if args.j is not None: 907 jobs = args.j 908 args.j = jobs 909 if not args.compiler_check and not args.env_cfg: 910 p.error('env_cfg is required unless -C is specified') 911 if not args.compiler_check and not args.sym_cfgs: 912 p.error('at least one sym_cfg is required unless -C is specified') 913 return args 914 915 916def main(): 917 args = _parse_args() 918 mflag = args.mflag 919 arch = '64-bit' if mflag == '-m64' else '32-bit' 920 921 with tempfile.TemporaryDirectory() as tmpdir: 922 if args.lang == 'c': 923 compiler = find_c_compiler(mflag, tmpdir, args.compiler) 924 base_flags = _C_BASE_FLAGS 925 else: 926 # kind is 'gcc' or 'clang', used to select the right 927 # include path discovery method. 928 compiler, kind = find_cxx_compiler(mflag, tmpdir, args.compiler) 929 if kind == 'gcc': 930 base_flags = find_gcc_cxx_includes(compiler) 931 else: 932 base_flags = find_clang_cxx_includes(compiler) 933 934 if args.compiler_check: 935 sys.exit(0) 936 937 env_cfg = EnvConfig(args.lang) 938 env_cfg.load(args.env_cfg) 939 940 sym_cfg = SymConfig() 941 for path in args.sym_cfgs: 942 sym_cfg.load(path) 943 944 ok = TestDriver().run( 945 sym_cfg.entries, env_cfg, compiler, 946 mflag, arch, base_flags, tmpdir, args) 947 948 sys.exit(0 if ok else 1) 949 950 951if __name__ == '__main__': 952 main() 953