| """Generate build-details.json (see PEP 739).""" |
| |
| # Script initially imported from: |
| # https://github.com/FFY00/python-instrospection/blob/main/python_introspection/scripts/generate-build-details.py |
| |
| from __future__ import annotations |
| |
| import argparse |
| import collections |
| import importlib.machinery |
| import json |
| import os |
| import sys |
| import sysconfig |
| |
| TYPE_CHECKING = False |
| if TYPE_CHECKING: |
| from typing import Any |
| |
| |
| def version_info_to_dict(obj: sys._version_info) -> dict[str, Any]: |
| field_names = ('major', 'minor', 'micro', 'releaselevel', 'serial') |
| return {field: getattr(obj, field) for field in field_names} |
| |
| |
| def get_dict_key(container: dict[str, Any], key: str) -> dict[str, Any]: |
| for part in key.split('.'): |
| container = container[part] |
| return container |
| |
| |
| def generate_data(schema_version: str) -> collections.defaultdict[str, Any]: |
| """Generate the build-details.json data (PEP 739). |
| |
| :param schema_version: The schema version of the data we want to generate. |
| """ |
| |
| if schema_version != '1.0': |
| raise ValueError(f'Unsupported schema_version: {schema_version}') |
| |
| data: collections.defaultdict[str, Any] = collections.defaultdict( |
| lambda: collections.defaultdict(dict), |
| ) |
| |
| data['schema_version'] = schema_version |
| |
| data['base_prefix'] = sysconfig.get_config_var('installed_base') |
| #data['base_interpreter'] = sys._base_executable |
| data['base_interpreter'] = os.path.join( |
| sysconfig.get_path('scripts'), |
| "python" |
| + sysconfig.get_config_var('LDVERSION') |
| + sysconfig.get_config_var('EXE'), |
| ) |
| data['platform'] = sysconfig.get_platform() |
| |
| data['language']['version'] = sysconfig.get_python_version() |
| data['language']['version_info'] = version_info_to_dict(sys.version_info) |
| |
| data['implementation'] = vars(sys.implementation).copy() |
| data['implementation']['version'] = version_info_to_dict(sys.implementation.version) |
| # Fix cross-compilation |
| if '_multiarch' in data['implementation']: |
| data['implementation']['_multiarch'] = sysconfig.get_config_var('MULTIARCH') |
| |
| data['abi']['flags'] = list(sys.abiflags) |
| |
| data['suffixes']['source'] = importlib.machinery.SOURCE_SUFFIXES |
| data['suffixes']['bytecode'] = importlib.machinery.BYTECODE_SUFFIXES |
| #data['suffixes']['optimized_bytecode'] = importlib.machinery.OPTIMIZED_BYTECODE_SUFFIXES |
| #data['suffixes']['debug_bytecode'] = importlib.machinery.DEBUG_BYTECODE_SUFFIXES |
| data['suffixes']['extensions'] = importlib.machinery.EXTENSION_SUFFIXES |
| |
| LIBDIR = sysconfig.get_config_var('LIBDIR') |
| LDLIBRARY = sysconfig.get_config_var('LDLIBRARY') |
| LIBRARY = sysconfig.get_config_var('LIBRARY') |
| PY3LIBRARY = sysconfig.get_config_var('PY3LIBRARY') |
| LIBPYTHON = sysconfig.get_config_var('LIBPYTHON') |
| LIBPC = sysconfig.get_config_var('LIBPC') |
| INCLUDEPY = sysconfig.get_config_var('INCLUDEPY') |
| |
| if os.name == 'posix': |
| # On POSIX, LIBRARY is always the static library, while LDLIBRARY is the |
| # dynamic library if enabled, otherwise it's the static library. |
| # If LIBRARY != LDLIBRARY, support for the dynamic library is enabled. |
| has_dynamic_library = LDLIBRARY != LIBRARY |
| has_static_library = sysconfig.get_config_var('STATIC_LIBPYTHON') |
| elif os.name == 'nt': |
| # Windows can only use a dynamic library or a static library. |
| # If it's using a dynamic library, sys.dllhandle will be set. |
| # Static builds on Windows are not really well supported, though. |
| # More context: https://github.com/python/cpython/issues/110234 |
| has_dynamic_library = hasattr(sys, 'dllhandle') |
| has_static_library = not has_dynamic_library |
| else: |
| raise NotADirectoryError(f'Unknown platform: {os.name}') |
| |
| # On POSIX, EXT_SUFFIX is set regardless if extension modules are supported |
| # or not, and on Windows older versions of CPython only set EXT_SUFFIX when |
| # extension modules are supported, but newer versions of CPython set it |
| # regardless. |
| # |
| # We only want to set abi.extension_suffix and stable_abi_suffix if |
| # extension modules are supported. |
| if has_dynamic_library: |
| data['abi']['extension_suffix'] = sysconfig.get_config_var('EXT_SUFFIX') |
| |
| # EXTENSION_SUFFIXES has been constant for a long time, and currently we |
| # don't have a better information source to find the stable ABI suffix. |
| for suffix in importlib.machinery.EXTENSION_SUFFIXES: |
| if suffix.startswith('.abi'): |
| data['abi']['stable_abi_suffix'] = suffix |
| break |
| |
| data['libpython']['dynamic'] = os.path.join(LIBDIR, LDLIBRARY) |
| # FIXME: Not sure if windows has a different dll for the stable ABI, and |
| # even if it does, currently we don't have a way to get its name. |
| if PY3LIBRARY: |
| data['libpython']['dynamic_stableabi'] = os.path.join(LIBDIR, PY3LIBRARY) |
| |
| # Os POSIX, this is defined by the LIBPYTHON Makefile variable not being |
| # empty. On Windows, don't link extensions — LIBPYTHON won't be defined, |
| data['libpython']['link_extensions'] = bool(LIBPYTHON) |
| |
| if has_static_library: |
| data['libpython']['static'] = os.path.join(LIBDIR, LIBRARY) |
| |
| data['c_api']['headers'] = INCLUDEPY |
| if LIBPC: |
| data['c_api']['pkgconfig_path'] = LIBPC |
| |
| return data |
| |
| |
| def make_paths_relative(data: dict[str, Any], config_path: str | None = None) -> None: |
| # Make base_prefix relative to the config_path directory |
| if config_path: |
| data['base_prefix'] = relative_path(data['base_prefix'], |
| os.path.dirname(config_path)) |
| base_prefix = data['base_prefix'] |
| |
| # Update path values to make them relative to base_prefix |
| PATH_KEYS = ( |
| 'base_interpreter', |
| 'libpython.dynamic', |
| 'libpython.dynamic_stableabi', |
| 'libpython.static', |
| 'c_api.headers', |
| 'c_api.pkgconfig_path', |
| ) |
| for entry in PATH_KEYS: |
| *parents, child = entry.split('.') |
| # Get the key container object |
| try: |
| container = data |
| for part in parents: |
| container = container[part] |
| if child not in container: |
| raise KeyError(child) |
| current_path = container[child] |
| except KeyError: |
| continue |
| # Get the relative path |
| new_path = relative_path(current_path, base_prefix) |
| # Join '.' so that the path is formated as './path' instead of 'path' |
| new_path = os.path.join('.', new_path) |
| container[child] = new_path |
| |
| |
| def relative_path(path: str, base: str) -> str: |
| if os.name != 'nt': |
| return os.path.relpath(path, base) |
| |
| # There are no relative paths between drives on Windows. |
| path_drv, _ = os.path.splitdrive(path) |
| base_drv, _ = os.path.splitdrive(base) |
| if path_drv.lower() == base_drv.lower(): |
| return os.path.relpath(path, base) |
| |
| return path |
| |
| |
| def main() -> None: |
| parser = argparse.ArgumentParser(exit_on_error=False) |
| parser.add_argument('location') |
| parser.add_argument( |
| '--schema-version', |
| default='1.0', |
| help='Schema version of the build-details.json file to generate.', |
| ) |
| parser.add_argument( |
| '--relative-paths', |
| action='store_true', |
| help='Whether to specify paths as absolute, or as relative paths to ``base_prefix``.', |
| ) |
| parser.add_argument( |
| '--config-file-path', |
| default=None, |
| help='If specified, ``base_prefix`` will be set as a relative path to the given config file path.', |
| ) |
| |
| args = parser.parse_args() |
| |
| data = generate_data(args.schema_version) |
| if args.relative_paths: |
| make_paths_relative(data, args.config_file_path) |
| |
| json_output = json.dumps(data, indent=2) |
| with open(args.location, 'w', encoding='utf-8') as f: |
| f.write(json_output) |
| f.write('\n') |
| |
| |
| if __name__ == '__main__': |
| main() |