codekingpro/portable-devtools
114k
1"""Provide access to Python's configuration information. The specific2configuration variables available depend heavily on the platform and3configuration. The values may be retrieved using4get_config_var(name), and the list of variables is available via5get_config_vars().keys(). Additional convenience functions are also6available.7 8Written by: Fred L. Drake, Jr.9Email: <fdrake@acm.org>10"""11 12from __future__ import annotations13 14import functools15import os16import pathlib17import re18import sys19import sysconfig20from typing import TYPE_CHECKING, Literal, overload21 22from jaraco.functools import pass_none23 24from .ccompiler import CCompiler25from .compat import py3926from .errors import DistutilsPlatformError27from .util import is_mingw28 29if TYPE_CHECKING:30 from typing_extensions import deprecated31else:32 33 def deprecated(message):34 return lambda fn: fn35 36 37IS_PYPY = '__pypy__' in sys.builtin_module_names38 39# These are needed in a couple of spots, so just compute them once.40PREFIX = os.path.normpath(sys.prefix)41EXEC_PREFIX = os.path.normpath(sys.exec_prefix)42BASE_PREFIX = os.path.normpath(sys.base_prefix)43BASE_EXEC_PREFIX = os.path.normpath(sys.base_exec_prefix)44 45# Path to the base directory of the project. On Windows the binary may46# live in project/PCbuild/win32 or project/PCbuild/amd64.47# set for cross builds48if "_PYTHON_PROJECT_BASE" in os.environ:49 project_base = os.path.abspath(os.environ["_PYTHON_PROJECT_BASE"])50else:51 if sys.executable:52 project_base = os.path.dirname(os.path.abspath(sys.executable))53 else:54 # sys.executable can be empty if argv[0] has been changed and Python is55 # unable to retrieve the real program name56 project_base = os.getcwd()57 58 59def _is_python_source_dir(d):60 """61 Return True if the target directory appears to point to an62 un-installed Python.63 """64 modules = pathlib.Path(d).joinpath('Modules')65 return any(modules.joinpath(fn).is_file() for fn in ('Setup', 'Setup.local'))66 67 68_sys_home = getattr(sys, '_home', None)69 70 71def _is_parent(dir_a, dir_b):72 """73 Return True if a is a parent of b.74 """75 return os.path.normcase(dir_a).startswith(os.path.normcase(dir_b))76 77 78if os.name == 'nt':79 80 @pass_none81 def _fix_pcbuild(d):82 # In a venv, sys._home will be inside BASE_PREFIX rather than PREFIX.83 prefixes = PREFIX, BASE_PREFIX84 matched = (85 prefix86 for prefix in prefixes87 if _is_parent(d, os.path.join(prefix, "PCbuild"))88 )89 return next(matched, d)90 91 project_base = _fix_pcbuild(project_base)92 _sys_home = _fix_pcbuild(_sys_home)93 94 95def _python_build():96 if _sys_home:97 return _is_python_source_dir(_sys_home)98 return _is_python_source_dir(project_base)99 100 101python_build = _python_build()102 103 104# Calculate the build qualifier flags if they are defined. Adding the flags105# to the include and lib directories only makes sense for an installation, not106# an in-source build.107build_flags = ''108try:109 if not python_build:110 build_flags = sys.abiflags111except AttributeError:112 # It's not a configure-based build, so the sys module doesn't have113 # this attribute, which is fine.114 pass115 116 117def get_python_version():118 """Return a string containing the major and minor Python version,119 leaving off the patchlevel. Sample return values could be '1.5'120 or '2.2'.121 """122 return f'{sys.version_info.major}.{sys.version_info.minor}'123 124 125def get_python_inc(plat_specific: bool = False, prefix: str | None = None) -> str:126 """Return the directory containing installed Python header files.127 128 If 'plat_specific' is false (the default), this is the path to the129 non-platform-specific header files, i.e. Python.h and so on;130 otherwise, this is the path to platform-specific header files131 (namely pyconfig.h).132 133 If 'prefix' is supplied, use it instead of sys.base_prefix or134 sys.base_exec_prefix -- i.e., ignore 'plat_specific'.135 """136 default_prefix = BASE_EXEC_PREFIX if plat_specific else BASE_PREFIX137 resolved_prefix = prefix if prefix is not None else default_prefix138 # MinGW imitates posix like layout, but os.name != posix139 os_name = "posix" if is_mingw() else os.name140 try:141 getter = globals()[f'_get_python_inc_{os_name}']142 except KeyError:143 raise DistutilsPlatformError(144 "I don't know where Python installs its C header files "145 f"on platform '{os.name}'"146 )147 return getter(resolved_prefix, prefix, plat_specific)148 149 150@pass_none151def _extant(path):152 """153 Replace path with None if it doesn't exist.154 """155 return path if os.path.exists(path) else None156 157 158def _get_python_inc_posix(prefix, spec_prefix, plat_specific):159 return (160 _get_python_inc_posix_python(plat_specific)161 or _extant(_get_python_inc_from_config(plat_specific, spec_prefix))162 or _get_python_inc_posix_prefix(prefix)163 )164 165 166def _get_python_inc_posix_python(plat_specific):167 """168 Assume the executable is in the build directory. The169 pyconfig.h file should be in the same directory. Since170 the build directory may not be the source directory,171 use "srcdir" from the makefile to find the "Include"172 directory.173 """174 if not python_build:175 return176 if plat_specific:177 return _sys_home or project_base178 incdir = os.path.join(get_config_var('srcdir'), 'Include')179 return os.path.normpath(incdir)180 181 182def _get_python_inc_from_config(plat_specific, spec_prefix):183 """184 If no prefix was explicitly specified, provide the include185 directory from the config vars. Useful when186 cross-compiling, since the config vars may come from187 the host188 platform Python installation, while the current Python189 executable is from the build platform installation.190 191 >>> monkeypatch = getfixture('monkeypatch')192 >>> gpifc = _get_python_inc_from_config193 >>> monkeypatch.setitem(gpifc.__globals__, 'get_config_var', str.lower)194 >>> gpifc(False, '/usr/bin/')195 >>> gpifc(False, '')196 >>> gpifc(False, None)197 'includepy'198 >>> gpifc(True, None)199 'confincludepy'200 """201 if spec_prefix is None:202 return get_config_var('CONF' * plat_specific + 'INCLUDEPY')203 204 205def _get_python_inc_posix_prefix(prefix):206 implementation = 'pypy' if IS_PYPY else 'python'207 python_dir = implementation + get_python_version() + build_flags208 return os.path.join(prefix, "include", python_dir)209 210 211def _get_python_inc_nt(prefix, spec_prefix, plat_specific):212 if python_build:213 # Include both include dirs to ensure we can find pyconfig.h214 return (215 os.path.join(prefix, "include")216 + os.path.pathsep217 + os.path.dirname(sysconfig.get_config_h_filename())218 )219 return os.path.join(prefix, "include")220 221 222# allow this behavior to be monkey-patched. Ref pypa/distutils#2.223def _posix_lib(standard_lib, libpython, early_prefix, prefix):224 if standard_lib:225 return libpython226 else:227 return os.path.join(libpython, "site-packages")228 229 230def get_python_lib(231 plat_specific: bool = False, standard_lib: bool = False, prefix: str | None = None232) -> str:233 """Return the directory containing the Python library (standard or234 site additions).235 236 If 'plat_specific' is true, return the directory containing237 platform-specific modules, i.e. any module from a non-pure-Python238 module distribution; otherwise, return the platform-shared library239 directory. If 'standard_lib' is true, return the directory240 containing standard Python library modules; otherwise, return the241 directory for site-specific modules.242 243 If 'prefix' is supplied, use it instead of sys.base_prefix or244 sys.base_exec_prefix -- i.e., ignore 'plat_specific'.245 """246 247 early_prefix = prefix248 249 if prefix is None:250 if standard_lib:251 prefix = plat_specific and BASE_EXEC_PREFIX or BASE_PREFIX252 else:253 prefix = plat_specific and EXEC_PREFIX or PREFIX254 255 if os.name == "posix" or is_mingw():256 if plat_specific or standard_lib:257 # Platform-specific modules (any module from a non-pure-Python258 # module distribution) or standard Python library modules.259 libdir = getattr(sys, "platlibdir", "lib")260 else:261 # Pure Python262 libdir = "lib"263 implementation = 'pypy' if IS_PYPY else 'python'264 libpython = os.path.join(prefix, libdir, implementation + get_python_version())265 return _posix_lib(standard_lib, libpython, early_prefix, prefix)266 elif os.name == "nt":267 if standard_lib:268 return os.path.join(prefix, "Lib")269 else:270 return os.path.join(prefix, "Lib", "site-packages")271 else:272 raise DistutilsPlatformError(273 f"I don't know where Python installs its library on platform '{os.name}'"274 )275 276 277@functools.lru_cache278def _customize_macos():279 """280 Perform first-time customization of compiler-related281 config vars on macOS. Use after a compiler is known282 to be needed. This customization exists primarily to support Pythons283 from binary installers. The kind and paths to build tools on284 the user system may vary significantly from the system285 that Python itself was built on. Also the user OS286 version and build tools may not support the same set287 of CPU architectures for universal builds.288 """289 290 sys.platform == "darwin" and __import__('_osx_support').customize_compiler(291 get_config_vars()292 )293 294 295def customize_compiler(compiler: CCompiler) -> None:296 """Do any platform-specific customization of a CCompiler instance.297 298 Mainly needed on Unix, so we can plug in the information that299 varies across Unices and is stored in Python's Makefile.300 """301 if compiler.compiler_type in ["unix", "cygwin"] or (302 compiler.compiler_type == "mingw32" and is_mingw()303 ):304 _customize_macos()305 306 (307 cc,308 cxx,309 cflags,310 ccshared,311 ldshared,312 ldcxxshared,313 shlib_suffix,314 ar,315 ar_flags,316 ) = get_config_vars(317 'CC',318 'CXX',319 'CFLAGS',320 'CCSHARED',321 'LDSHARED',322 'LDCXXSHARED',323 'SHLIB_SUFFIX',324 'AR',325 'ARFLAGS',326 )327 328 cxxflags = cflags329 330 if 'CC' in os.environ:331 newcc = os.environ['CC']332 if 'LDSHARED' not in os.environ and ldshared.startswith(cc):333 # If CC is overridden, use that as the default334 # command for LDSHARED as well335 ldshared = newcc + ldshared[len(cc) :]336 cc = newcc337 cxx = os.environ.get('CXX', cxx)338 ldshared = os.environ.get('LDSHARED', ldshared)339 ldcxxshared = os.environ.get('LDCXXSHARED', ldcxxshared)340 cpp = os.environ.get(341 'CPP',342 cc + " -E", # not always343 )344 345 ldshared = _add_flags(ldshared, 'LD')346 ldcxxshared = _add_flags(ldcxxshared, 'LD')347 cflags = os.environ.get('CFLAGS', cflags)348 ldshared = _add_flags(ldshared, 'C')349 cxxflags = os.environ.get('CXXFLAGS', cxxflags)350 ldcxxshared = _add_flags(ldcxxshared, 'CXX')351 cpp = _add_flags(cpp, 'CPP')352 cflags = _add_flags(cflags, 'CPP')353 cxxflags = _add_flags(cxxflags, 'CPP')354 ldshared = _add_flags(ldshared, 'CPP')355 ldcxxshared = _add_flags(ldcxxshared, 'CPP')356 357 ar = os.environ.get('AR', ar)358 359 archiver = ar + ' ' + os.environ.get('ARFLAGS', ar_flags)360 cc_cmd = cc + ' ' + cflags361 cxx_cmd = cxx + ' ' + cxxflags362 363 compiler.set_executables(364 preprocessor=cpp,365 compiler=cc_cmd,366 compiler_so=cc_cmd + ' ' + ccshared,367 compiler_cxx=cxx_cmd,368 compiler_so_cxx=cxx_cmd + ' ' + ccshared,369 linker_so=ldshared,370 linker_so_cxx=ldcxxshared,371 linker_exe=cc,372 linker_exe_cxx=cxx,373 archiver=archiver,374 )375 376 if 'RANLIB' in os.environ and compiler.executables.get('ranlib', None):377 compiler.set_executables(ranlib=os.environ['RANLIB'])378 379 compiler.shared_lib_extension = shlib_suffix380 381 382def get_config_h_filename() -> str:383 """Return full pathname of installed pyconfig.h file."""384 return sysconfig.get_config_h_filename()385 386 387def get_makefile_filename() -> str:388 """Return full pathname of installed Makefile from the Python build."""389 return sysconfig.get_makefile_filename()390 391 392def parse_config_h(fp, g=None):393 """Parse a config.h-style file.394 395 A dictionary containing name/value pairs is returned. If an396 optional dictionary is passed in as the second argument, it is397 used instead of a new dictionary.398 """399 return sysconfig.parse_config_h(fp, vars=g)400 401 402# Regexes needed for parsing Makefile (and similar syntaxes,403# like old-style Setup files).404_variable_rx = re.compile(r"([a-zA-Z][a-zA-Z0-9_]+)\s*=\s*(.*)")405_findvar1_rx = re.compile(r"\$\(([A-Za-z][A-Za-z0-9_]*)\)")406_findvar2_rx = re.compile(r"\${([A-Za-z][A-Za-z0-9_]*)}")407 408 409def parse_makefile(fn, g=None): # noqa: C901410 """Parse a Makefile-style file.411 412 A dictionary containing name/value pairs is returned. If an413 optional dictionary is passed in as the second argument, it is414 used instead of a new dictionary.415 """416 from distutils.text_file import TextFile417 418 fp = TextFile(419 fn,420 strip_comments=True,421 skip_blanks=True,422 join_lines=True,423 errors="surrogateescape",424 )425 426 if g is None:427 g = {}428 done = {}429 notdone = {}430 431 while True:432 line = fp.readline()433 if line is None: # eof434 break435 m = _variable_rx.match(line)436 if m:437 n, v = m.group(1, 2)438 v = v.strip()439 # `$$' is a literal `$' in make440 tmpv = v.replace('$$', '')441 442 if "$" in tmpv:443 notdone[n] = v444 else:445 try:446 v = int(v)447 except ValueError:448 # insert literal `$'449 done[n] = v.replace('$$', '$')450 else:451 done[n] = v452 453 # Variables with a 'PY_' prefix in the makefile. These need to454 # be made available without that prefix through sysconfig.455 # Special care is needed to ensure that variable expansion works, even456 # if the expansion uses the name without a prefix.457 renamed_variables = ('CFLAGS', 'LDFLAGS', 'CPPFLAGS')458 459 # do variable interpolation here460 while notdone:461 for name in list(notdone):462 value = notdone[name]463 m = _findvar1_rx.search(value) or _findvar2_rx.search(value)464 if m:465 n = m.group(1)466 found = True467 if n in done:468 item = str(done[n])469 elif n in notdone:470 # get it on a subsequent round471 found = False472 elif n in os.environ:473 # do it like make: fall back to environment474 item = os.environ[n]475 476 elif n in renamed_variables:477 if name.startswith('PY_') and name[3:] in renamed_variables:478 item = ""479 480 elif 'PY_' + n in notdone:481 found = False482 483 else:484 item = str(done['PY_' + n])485 else:486 done[n] = item = ""487 if found:488 after = value[m.end() :]489 value = value[: m.start()] + item + after490 if "$" in after:491 notdone[name] = value492 else:493 try:494 value = int(value)495 except ValueError:496 done[name] = value.strip()497 else:498 done[name] = value499 del notdone[name]500 501 if name.startswith('PY_') and name[3:] in renamed_variables:502 name = name[3:]503 if name not in done:504 done[name] = value505 else:506 # bogus variable reference; just drop it since we can't deal507 del notdone[name]508 509 fp.close()510 511 # strip spurious spaces512 for k, v in done.items():513 if isinstance(v, str):514 done[k] = v.strip()515 516 # save the results in the global dictionary517 g.update(done)518 return g519 520 521def expand_makefile_vars(s, vars):522 """Expand Makefile-style variables -- "${foo}" or "$(foo)" -- in523 'string' according to 'vars' (a dictionary mapping variable names to524 values). Variables not present in 'vars' are silently expanded to the525 empty string. The variable values in 'vars' should not contain further526 variable expansions; if 'vars' is the output of 'parse_makefile()',527 you're fine. Returns a variable-expanded version of 's'.528 """529 530 # This algorithm does multiple expansion, so if vars['foo'] contains531 # "${bar}", it will expand ${foo} to ${bar}, and then expand532 # ${bar}... and so forth. This is fine as long as 'vars' comes from533 # 'parse_makefile()', which takes care of such expansions eagerly,534 # according to make's variable expansion semantics.535 536 while True:537 m = _findvar1_rx.search(s) or _findvar2_rx.search(s)538 if m:539 (beg, end) = m.span()540 s = s[0:beg] + vars.get(m.group(1)) + s[end:]541 else:542 break543 return s544 545 546_config_vars = None547 548 549@overload550def get_config_vars() -> dict[str, str | int]: ...551@overload552def get_config_vars(arg: str, /, *args: str) -> list[str | int]: ...553def get_config_vars(*args: str) -> list[str | int] | dict[str, str | int]:554 """With no arguments, return a dictionary of all configuration555 variables relevant for the current platform. Generally this includes556 everything needed to build extensions and install both pure modules and557 extensions. On Unix, this means every variable defined in Python's558 installed Makefile; on Windows it's a much smaller set.559 560 With arguments, return a list of values that result from looking up561 each argument in the configuration variable dictionary.562 """563 global _config_vars564 if _config_vars is None:565 _config_vars = sysconfig.get_config_vars().copy()566 py39.add_ext_suffix(_config_vars)567 568 return [_config_vars.get(name) for name in args] if args else _config_vars569 570 571@overload572@deprecated(573 "SO is deprecated, use EXT_SUFFIX. Support will be removed when this module is synchronized with stdlib Python 3.11"574)575def get_config_var(name: Literal["SO"]) -> int | str | None: ...576@overload577def get_config_var(name: str) -> int | str | None: ...578def get_config_var(name: str) -> int | str | None:579 """Return the value of a single variable using the dictionary580 returned by 'get_config_vars()'. Equivalent to581 get_config_vars().get(name)582 """583 if name == 'SO':584 import warnings585 586 warnings.warn('SO is deprecated, use EXT_SUFFIX', DeprecationWarning, 2)587 return get_config_vars().get(name)588 589 590@pass_none591def _add_flags(value: str, type: str) -> str:592 """593 Add any flags from the environment for the given type.594 595 type is the prefix to FLAGS in the environment key (e.g. "C" for "CFLAGS").596 """597 flags = os.environ.get(f'{type}FLAGS')598 return f'{value} {flags}' if flags else value599 