| 
									
										
										
										
											2020-10-22 18:42:51 -06:00
										 |  |  | import contextlib | 
					
						
							|  |  |  | import distutils.ccompiler | 
					
						
							|  |  |  | import logging | 
					
						
							| 
									
										
										
										
											2023-06-28 04:50:51 +02:00
										 |  |  | import os | 
					
						
							| 
									
										
										
										
											2020-10-22 18:42:51 -06:00
										 |  |  | import shlex | 
					
						
							|  |  |  | import subprocess | 
					
						
							|  |  |  | import sys | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | from ..info import FileInfo, SourceLine | 
					
						
							|  |  |  | from .errors import ( | 
					
						
							|  |  |  |     PreprocessorFailure, | 
					
						
							|  |  |  |     ErrorDirectiveError, | 
					
						
							|  |  |  |     MissingDependenciesError, | 
					
						
							|  |  |  |     OSMismatchError, | 
					
						
							|  |  |  | ) | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | logger = logging.getLogger(__name__) | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | # XXX Add aggregate "source" class(es)? | 
					
						
							|  |  |  | #  * expose all lines as single text string | 
					
						
							|  |  |  | #  * expose all lines as sequence | 
					
						
							|  |  |  | #  * iterate all lines | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | def run_cmd(argv, *, | 
					
						
							|  |  |  |             #capture_output=True, | 
					
						
							|  |  |  |             stdout=subprocess.PIPE, | 
					
						
							|  |  |  |             #stderr=subprocess.STDOUT, | 
					
						
							|  |  |  |             stderr=subprocess.PIPE, | 
					
						
							|  |  |  |             text=True, | 
					
						
							|  |  |  |             check=True, | 
					
						
							|  |  |  |             **kwargs | 
					
						
							|  |  |  |             ): | 
					
						
							|  |  |  |     if isinstance(stderr, str) and stderr.lower() == 'stdout': | 
					
						
							|  |  |  |         stderr = subprocess.STDOUT | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |     kw = dict(locals()) | 
					
						
							|  |  |  |     kw.pop('argv') | 
					
						
							|  |  |  |     kw.pop('kwargs') | 
					
						
							|  |  |  |     kwargs.update(kw) | 
					
						
							|  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2023-06-28 04:50:51 +02:00
										 |  |  |     # Remove LANG environment variable: the C parser doesn't support GCC | 
					
						
							|  |  |  |     # localized messages | 
					
						
							|  |  |  |     env = dict(os.environ) | 
					
						
							|  |  |  |     env.pop('LANG', None) | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |     proc = subprocess.run(argv, env=env, **kwargs) | 
					
						
							| 
									
										
										
										
											2020-10-22 18:42:51 -06:00
										 |  |  |     return proc.stdout | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2022-09-12 11:09:31 -06:00
										 |  |  | def preprocess(tool, filename, cwd=None, **kwargs): | 
					
						
							| 
									
										
										
										
											2020-10-22 18:42:51 -06:00
										 |  |  |     argv = _build_argv(tool, filename, **kwargs) | 
					
						
							|  |  |  |     logger.debug(' '.join(shlex.quote(v) for v in argv)) | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |     # Make sure the OS is supported for this file. | 
					
						
							|  |  |  |     if (_expected := is_os_mismatch(filename)): | 
					
						
							|  |  |  |         error = None | 
					
						
							|  |  |  |         raise OSMismatchError(filename, _expected, argv, error, TOOL) | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |     # Run the command. | 
					
						
							|  |  |  |     with converted_error(tool, argv, filename): | 
					
						
							|  |  |  |         # We use subprocess directly here, instead of calling the | 
					
						
							|  |  |  |         # distutil compiler object's preprocess() method, since that | 
					
						
							|  |  |  |         # one writes to stdout/stderr and it's simpler to do it directly | 
					
						
							|  |  |  |         # through subprocess. | 
					
						
							| 
									
										
										
										
											2022-09-12 11:09:31 -06:00
										 |  |  |         return run_cmd(argv, cwd=cwd) | 
					
						
							| 
									
										
										
										
											2020-10-22 18:42:51 -06:00
										 |  |  | 
 | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | def _build_argv( | 
					
						
							|  |  |  |     tool, | 
					
						
							|  |  |  |     filename, | 
					
						
							|  |  |  |     incldirs=None, | 
					
						
							| 
									
										
										
										
											2022-09-12 11:09:31 -06:00
										 |  |  |     includes=None, | 
					
						
							| 
									
										
										
										
											2020-10-22 18:42:51 -06:00
										 |  |  |     macros=None, | 
					
						
							|  |  |  |     preargs=None, | 
					
						
							|  |  |  |     postargs=None, | 
					
						
							|  |  |  |     executable=None, | 
					
						
							|  |  |  |     compiler=None, | 
					
						
							|  |  |  | ): | 
					
						
							| 
									
										
										
										
											2022-09-12 11:09:31 -06:00
										 |  |  |     if includes: | 
					
						
							|  |  |  |         includes = tuple(f'-include{i}' for i in includes) | 
					
						
							|  |  |  |         postargs = (includes + postargs) if postargs else includes | 
					
						
							|  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2020-10-22 18:42:51 -06:00
										 |  |  |     compiler = distutils.ccompiler.new_compiler( | 
					
						
							|  |  |  |         compiler=compiler or tool, | 
					
						
							|  |  |  |     ) | 
					
						
							|  |  |  |     if executable: | 
					
						
							|  |  |  |         compiler.set_executable('preprocessor', executable) | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |     argv = None | 
					
						
							|  |  |  |     def _spawn(_argv): | 
					
						
							|  |  |  |         nonlocal argv | 
					
						
							|  |  |  |         argv = _argv | 
					
						
							|  |  |  |     compiler.spawn = _spawn | 
					
						
							|  |  |  |     compiler.preprocess( | 
					
						
							|  |  |  |         filename, | 
					
						
							|  |  |  |         macros=[tuple(v) for v in macros or ()], | 
					
						
							|  |  |  |         include_dirs=incldirs or (), | 
					
						
							|  |  |  |         extra_preargs=preargs or (), | 
					
						
							|  |  |  |         extra_postargs=postargs or (), | 
					
						
							|  |  |  |     ) | 
					
						
							|  |  |  |     return argv | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | @contextlib.contextmanager | 
					
						
							|  |  |  | def converted_error(tool, argv, filename): | 
					
						
							|  |  |  |     try: | 
					
						
							|  |  |  |         yield | 
					
						
							|  |  |  |     except subprocess.CalledProcessError as exc: | 
					
						
							|  |  |  |         convert_error( | 
					
						
							|  |  |  |             tool, | 
					
						
							|  |  |  |             argv, | 
					
						
							|  |  |  |             filename, | 
					
						
							|  |  |  |             exc.stderr, | 
					
						
							|  |  |  |             exc.returncode, | 
					
						
							|  |  |  |         ) | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | def convert_error(tool, argv, filename, stderr, rc): | 
					
						
							|  |  |  |     error = (stderr.splitlines()[0], rc) | 
					
						
							|  |  |  |     if (_expected := is_os_mismatch(filename, stderr)): | 
					
						
							| 
									
										
										
										
											2023-03-14 10:05:54 -06:00
										 |  |  |         logger.info(stderr.strip()) | 
					
						
							| 
									
										
										
										
											2020-10-22 18:42:51 -06:00
										 |  |  |         raise OSMismatchError(filename, _expected, argv, error, tool) | 
					
						
							|  |  |  |     elif (_missing := is_missing_dep(stderr)): | 
					
						
							| 
									
										
										
										
											2023-03-14 10:05:54 -06:00
										 |  |  |         logger.info(stderr.strip()) | 
					
						
							| 
									
										
										
										
											2020-10-22 18:42:51 -06:00
										 |  |  |         raise MissingDependenciesError(filename, (_missing,), argv, error, tool) | 
					
						
							|  |  |  |     elif '#error' in stderr: | 
					
						
							|  |  |  |         # XXX Ignore incompatible files. | 
					
						
							|  |  |  |         error = (stderr.splitlines()[1], rc) | 
					
						
							| 
									
										
										
										
											2023-03-14 10:05:54 -06:00
										 |  |  |         logger.info(stderr.strip()) | 
					
						
							| 
									
										
										
										
											2020-10-22 18:42:51 -06:00
										 |  |  |         raise ErrorDirectiveError(filename, argv, error, tool) | 
					
						
							|  |  |  |     else: | 
					
						
							|  |  |  |         # Try one more time, with stderr written to the terminal. | 
					
						
							|  |  |  |         try: | 
					
						
							|  |  |  |             output = run_cmd(argv, stderr=None) | 
					
						
							|  |  |  |         except subprocess.CalledProcessError: | 
					
						
							|  |  |  |             raise PreprocessorFailure(filename, argv, error, tool) | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | def is_os_mismatch(filename, errtext=None): | 
					
						
							|  |  |  |     # See: https://docs.python.org/3/library/sys.html#sys.platform | 
					
						
							|  |  |  |     actual = sys.platform | 
					
						
							|  |  |  |     if actual == 'unknown': | 
					
						
							|  |  |  |         raise NotImplementedError | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |     if errtext is not None: | 
					
						
							|  |  |  |         if (missing := is_missing_dep(errtext)): | 
					
						
							|  |  |  |             matching = get_matching_oses(missing, filename) | 
					
						
							|  |  |  |             if actual not in matching: | 
					
						
							|  |  |  |                 return matching | 
					
						
							|  |  |  |     return False | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | def get_matching_oses(missing, filename): | 
					
						
							|  |  |  |     # OSX | 
					
						
							|  |  |  |     if 'darwin' in filename or 'osx' in filename: | 
					
						
							|  |  |  |         return ('darwin',) | 
					
						
							|  |  |  |     elif missing == 'SystemConfiguration/SystemConfiguration.h': | 
					
						
							|  |  |  |         return ('darwin',) | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |     # Windows | 
					
						
							|  |  |  |     elif missing in ('windows.h', 'winsock2.h'): | 
					
						
							|  |  |  |         return ('win32',) | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |     # other | 
					
						
							|  |  |  |     elif missing == 'sys/ldr.h': | 
					
						
							|  |  |  |         return ('aix',) | 
					
						
							|  |  |  |     elif missing == 'dl.h': | 
					
						
							|  |  |  |         # XXX The existence of Python/dynload_dl.c implies others... | 
					
						
							|  |  |  |         # Note that hpux isn't actual supported any more. | 
					
						
							|  |  |  |         return ('hpux', '???') | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |     # unrecognized | 
					
						
							|  |  |  |     else: | 
					
						
							|  |  |  |         return () | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | def is_missing_dep(errtext): | 
					
						
							|  |  |  |     if 'No such file or directory' in errtext: | 
					
						
							|  |  |  |         missing = errtext.split(': No such file or directory')[0].split()[-1] | 
					
						
							|  |  |  |         return missing | 
					
						
							|  |  |  |     return False |