mirror of
				https://github.com/python/cpython.git
				synced 2025-11-04 07:31:38 +00:00 
			
		
		
		
	The c-analyzer doesn't support GCC localized messages, so just unset the LANG environment variable.
		
			
				
	
	
		
			184 lines
		
	
	
	
		
			4.9 KiB
		
	
	
	
		
			Python
		
	
	
	
	
	
			
		
		
	
	
			184 lines
		
	
	
	
		
			4.9 KiB
		
	
	
	
		
			Python
		
	
	
	
	
	
import contextlib
 | 
						|
import distutils.ccompiler
 | 
						|
import logging
 | 
						|
import os
 | 
						|
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)
 | 
						|
 | 
						|
    # 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)
 | 
						|
    return proc.stdout
 | 
						|
 | 
						|
 | 
						|
def preprocess(tool, filename, cwd=None, **kwargs):
 | 
						|
    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.
 | 
						|
        return run_cmd(argv, cwd=cwd)
 | 
						|
 | 
						|
 | 
						|
def _build_argv(
 | 
						|
    tool,
 | 
						|
    filename,
 | 
						|
    incldirs=None,
 | 
						|
    includes=None,
 | 
						|
    macros=None,
 | 
						|
    preargs=None,
 | 
						|
    postargs=None,
 | 
						|
    executable=None,
 | 
						|
    compiler=None,
 | 
						|
):
 | 
						|
    if includes:
 | 
						|
        includes = tuple(f'-include{i}' for i in includes)
 | 
						|
        postargs = (includes + postargs) if postargs else includes
 | 
						|
 | 
						|
    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)):
 | 
						|
        logger.info(stderr.strip())
 | 
						|
        raise OSMismatchError(filename, _expected, argv, error, tool)
 | 
						|
    elif (_missing := is_missing_dep(stderr)):
 | 
						|
        logger.info(stderr.strip())
 | 
						|
        raise MissingDependenciesError(filename, (_missing,), argv, error, tool)
 | 
						|
    elif '#error' in stderr:
 | 
						|
        # XXX Ignore incompatible files.
 | 
						|
        error = (stderr.splitlines()[1], rc)
 | 
						|
        logger.info(stderr.strip())
 | 
						|
        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
 |