# SPDX-License-Identifier: BSD-2-Clause # Copyright (c) 2024 Phil Thompson import os import sys from .buildable import BuildableBindings from .configurable import Configurable, Option from .exceptions import UserException from .generator import parse, resolve from .generator.outputs import (output_api, output_code, output_extract, output_pyi) from .installable import Installable from .module import copy_nonshared_sources from .version import SIP_VERSION class Bindings(Configurable): """ The encapsulation of a module's bindings. """ # The configurable options. _options = ( # Any bindings level builder-specific settings. Option('builder_settings', option_type=list), # The list of #define names and values in the format "NAME" or # "NAME=VALUE". Option('define_macros', option_type=list), # Set if exception support is enabled. Option('exceptions', option_type=bool), # The list of extra compiler arguments. Option('extra_compile_args', option_type=list), # The list of extra linker arguments. Option('extra_link_args', option_type=list), # The list of extra compiled object files to link. Option('extra_objects', option_type=list), # The list of extracts to generate. Option('generate_extracts', option_type=list), # The list of additional .h files. Option('headers', option_type=list), # The list of additional C/C++ include directories to search. Option('include_dirs', option_type=list), # Set if the bindings are internal. Internal bindings don't have their # .sip, .pyi or .api files installed. Option('internal', option_type=bool), # The list of library names to link against. Option('libraries', option_type=list), # The list of C/C++ library directories to search. Option('library_dirs', option_type=list), # Set to always release the Python GIL. Option('release_gil', option_type=bool), # Set to compile the module as a static library. Option('static', option_type=bool), # The name of the .sip file that specifies the bindings. If it is # relative then it is relative to the project's 'sip_files_dir'. Option('sip_file'), # The filename extension to use for generated source files. Option('source_suffix'), # The list of additional C/C++ source files to compile and link. Option('sources', option_type=list), # The list of tags to enable. Option('tags', option_type=list), # The user-configurable options. Although the use of a corresponding # command line option will affect all sets of bindings, putting them # here (as opposed to in Project) means they can have individual # values specified in pyproject.toml. Option('concatenate', option_type=int, help="concatenate the generated bindings into N source files", metavar="N"), Option('debug', option_type=bool, help="build with debugging symbols"), Option('disabled_features', option_type=list, help="disable the TAG feature tag", metavar="TAG"), Option('docstrings', option_type=bool, inverted=True, help="disable the generation of docstrings"), Option('pep484_pyi', option_type=bool, help="enable the generation of PEP 484 .pyi files"), Option('protected_is_public', option_type=bool, help="enable the protected/public hack (default on non-Windows)"), Option('protected_is_public', option_type=bool, inverted=True, help="disable the protected/public hack (default on Windows)"), Option('tracing', option_type=bool, help="build with tracing support"), ) def __init__(self, project, name, **kwargs): """ Initialise the bindings. """ super().__init__() self.project = project self.name = name self.initialise_options(kwargs) def apply_nonuser_defaults(self, tool): """ Set default values for each non-user configurable option that hasn't been set yet. """ # Provide a default .sip file name if needed. if self.sip_file is None: self.sip_file = self.name + '.sip' super().apply_nonuser_defaults(tool) def apply_user_defaults(self, tool): """ Set default values for user options that haven't been set yet. """ if self.protected_is_public is None: self.protected_is_public = (self.project.py_platform != 'win32') super().apply_user_defaults(tool) def generate(self): """ Generate the bindings source code and optional additional extracts. and return a BuildableBindings instance containing the details of everything needed to build the bindings. """ project = self.project # The old parser had no concept of the encoding of a .sip file. For # the moment we say that files should be UTF-8. If that proves to be a # problem then a project-specific encoding should be able to be # specified in pyproject.toml which would apply to all .sip files that # make up the project. encoding = 'UTF-8' # Parse the input file. spec, modules, sip_files = parse(self.sip_file, SIP_VERSION, encoding, project.abi_version, self.tags, self.disabled_features, self.protected_is_public, self._sip_include_dirs, project.sip_module) # Resolve the types. resolve(spec, modules) module = spec.module uses_limited_api = module.use_limited_api or spec.is_composite # The details of things that will have been generated. Note that we # don't include anything for .api files or generic extracts as the # arguments include a file name. buildable = BuildableBindings(self, module.fq_py_name.name, uses_limited_api=uses_limited_api) buildable.builder_settings.extend(self.builder_settings) buildable.debug = self.debug buildable.exceptions = self.exceptions buildable.extra_compile_args = self.extra_compile_args buildable.extra_link_args = self.extra_link_args buildable.extra_objects = self.extra_objects buildable.static = self.static # Generate any API file. if project.api_dir and not self.internal: project.progress( "Generating the {0} .api file".format(buildable.target)) output_api(spec, os.path.join(project.build_dir, buildable.target + '.api')) # Generate any extracts. for extract_ref in self.generate_extracts: output_extract(spec, extract_ref) # Generate any type hints file. if self.pep484_pyi and not self.internal: project.progress( "Generating the {0} .pyi file".format(buildable.target)) pyi_path = os.path.join(buildable.build_dir, buildable.target + '.pyi') output_pyi(spec, project, pyi_path) installable = Installable('pyi', target_subdir=buildable.get_install_subdir()) installable.files.append(pyi_path) buildable.installables.append(installable) # Generate the bindings. output_code(spec, self, project, buildable) buildable.headers.extend(self.headers) # Add the sip module code if it is not shared. buildable.include_dirs.append(buildable.build_dir) if project.sip_module: # sip.h will already be in the build directory. buildable.include_dirs.append(project.build_dir) if not self.internal: # Add an installable for the .sip files. installable = buildable.get_bindings_installable('sip') sip_dir = os.path.dirname(self.sip_file) for fn in sip_files: sip_path = os.path.join(sip_dir, fn) # The code generator does not report the full pathname of a # .sip file (only names relative to the search directory in # which it was found). Therefore we need to check if it is # actually in the directory we are installing from and # ignore it if not. This isn't really the right thing to # do but is actually what we want when we have optional # license .sip files. if os.path.isfile(sip_path): installable.files.append(sip_path) buildable.installables.append(installable) else: buildable.sources.extend( copy_nonshared_sources(project.abi_version.split('.')[0], buildable.build_dir)) buildable.include_dirs.extend(self.include_dirs) buildable.sources.extend(self.sources) if self.protected_is_public: buildable.define_macros.append('SIP_PROTECTED_IS_PUBLIC') buildable.define_macros.append('protected=public') buildable.define_macros.extend(self.define_macros) buildable.libraries.extend(self.libraries) buildable.library_dirs.extend(self.library_dirs) return buildable def get_options(self): """ Return the list of configurable options. """ options = super().get_options() options.extend(self._options) return options def is_buildable(self): """ Return True if the bindings are buildable. This will not be called if the bindings have been explicitly enabled. """ return True def verify_configuration(self, tool): """ Verify that the configuration is complete and consistent. """ if tool not in Option.BUILD_TOOLS: return project = self.project super().verify_configuration(tool) # Make sure relevent paths are absolute and use native separators. self.extra_objects = [project.project_path(o) for o in self.extra_objects] self.headers = [project.project_path(h) for h in self.headers] self.include_dirs = [project.project_path(d) for d in self.include_dirs] self.library_dirs = [project.project_path(d) for d in self.library_dirs] self.sip_file = project.project_path(self.sip_file, relative_to=project.sip_files_dir) self.sources = [project.project_path(s) for s in self.sources] # Check the .sip file exists. if not os.path.isfile(self.sip_file): raise UserException( "the file '{0}' for the {1} bindings does not " "exist".format(self.sip_file, self.name)) # On Windows the interpreter must be a debug build if a debug version # is to be built and vice versa. if sys.platform == 'win32': if self.debug: if not project.py_debug: raise UserException( "A debug version of Python must be used when " "building a debug version of the {0} " "bindings".format(self.name)) elif project.py_debug: raise UserException( "A debug version of the {0} bindings must be built " "when a debug version of Python is used".format( self.name)) if not self.source_suffix: self.source_suffix = None