1import contextlib 2import distutils.ccompiler 3import logging 4import shlex 5import subprocess 6import sys 7 8from ..info import FileInfo, SourceLine 9from .errors import ( 10 PreprocessorFailure, 11 ErrorDirectiveError, 12 MissingDependenciesError, 13 OSMismatchError, 14) 15 16 17logger = logging.getLogger(__name__) 18 19 20# XXX Add aggregate "source" class(es)? 21# * expose all lines as single text string 22# * expose all lines as sequence 23# * iterate all lines 24 25 26def run_cmd(argv, *, 27 #capture_output=True, 28 stdout=subprocess.PIPE, 29 #stderr=subprocess.STDOUT, 30 stderr=subprocess.PIPE, 31 text=True, 32 check=True, 33 **kwargs 34 ): 35 if isinstance(stderr, str) and stderr.lower() == 'stdout': 36 stderr = subprocess.STDOUT 37 38 kw = dict(locals()) 39 kw.pop('argv') 40 kw.pop('kwargs') 41 kwargs.update(kw) 42 43 proc = subprocess.run(argv, **kwargs) 44 return proc.stdout 45 46 47def preprocess(tool, filename, **kwargs): 48 argv = _build_argv(tool, filename, **kwargs) 49 logger.debug(' '.join(shlex.quote(v) for v in argv)) 50 51 # Make sure the OS is supported for this file. 52 if (_expected := is_os_mismatch(filename)): 53 error = None 54 raise OSMismatchError(filename, _expected, argv, error, TOOL) 55 56 # Run the command. 57 with converted_error(tool, argv, filename): 58 # We use subprocess directly here, instead of calling the 59 # distutil compiler object's preprocess() method, since that 60 # one writes to stdout/stderr and it's simpler to do it directly 61 # through subprocess. 62 return run_cmd(argv) 63 64 65def _build_argv( 66 tool, 67 filename, 68 incldirs=None, 69 macros=None, 70 preargs=None, 71 postargs=None, 72 executable=None, 73 compiler=None, 74): 75 compiler = distutils.ccompiler.new_compiler( 76 compiler=compiler or tool, 77 ) 78 if executable: 79 compiler.set_executable('preprocessor', executable) 80 81 argv = None 82 def _spawn(_argv): 83 nonlocal argv 84 argv = _argv 85 compiler.spawn = _spawn 86 compiler.preprocess( 87 filename, 88 macros=[tuple(v) for v in macros or ()], 89 include_dirs=incldirs or (), 90 extra_preargs=preargs or (), 91 extra_postargs=postargs or (), 92 ) 93 return argv 94 95 96@contextlib.contextmanager 97def converted_error(tool, argv, filename): 98 try: 99 yield 100 except subprocess.CalledProcessError as exc: 101 convert_error( 102 tool, 103 argv, 104 filename, 105 exc.stderr, 106 exc.returncode, 107 ) 108 109 110def convert_error(tool, argv, filename, stderr, rc): 111 error = (stderr.splitlines()[0], rc) 112 if (_expected := is_os_mismatch(filename, stderr)): 113 logger.debug(stderr.strip()) 114 raise OSMismatchError(filename, _expected, argv, error, tool) 115 elif (_missing := is_missing_dep(stderr)): 116 logger.debug(stderr.strip()) 117 raise MissingDependenciesError(filename, (_missing,), argv, error, tool) 118 elif '#error' in stderr: 119 # XXX Ignore incompatible files. 120 error = (stderr.splitlines()[1], rc) 121 logger.debug(stderr.strip()) 122 raise ErrorDirectiveError(filename, argv, error, tool) 123 else: 124 # Try one more time, with stderr written to the terminal. 125 try: 126 output = run_cmd(argv, stderr=None) 127 except subprocess.CalledProcessError: 128 raise PreprocessorFailure(filename, argv, error, tool) 129 130 131def is_os_mismatch(filename, errtext=None): 132 # See: https://docs.python.org/3/library/sys.html#sys.platform 133 actual = sys.platform 134 if actual == 'unknown': 135 raise NotImplementedError 136 137 if errtext is not None: 138 if (missing := is_missing_dep(errtext)): 139 matching = get_matching_oses(missing, filename) 140 if actual not in matching: 141 return matching 142 return False 143 144 145def get_matching_oses(missing, filename): 146 # OSX 147 if 'darwin' in filename or 'osx' in filename: 148 return ('darwin',) 149 elif missing == 'SystemConfiguration/SystemConfiguration.h': 150 return ('darwin',) 151 152 # Windows 153 elif missing in ('windows.h', 'winsock2.h'): 154 return ('win32',) 155 156 # other 157 elif missing == 'sys/ldr.h': 158 return ('aix',) 159 elif missing == 'dl.h': 160 # XXX The existence of Python/dynload_dl.c implies others... 161 # Note that hpux isn't actual supported any more. 162 return ('hpux', '???') 163 164 # unrecognized 165 else: 166 return () 167 168 169def is_missing_dep(errtext): 170 if 'No such file or directory' in errtext: 171 missing = errtext.split(': No such file or directory')[0].split()[-1] 172 return missing 173 return False 174