Aluode/PerceptionLabPortable
0
1from __future__ import annotations2 3import itertools4import os5import sys6import textwrap7from collections.abc import Iterator8from importlib.machinery import EXTENSION_SUFFIXES9from importlib.util import cache_from_source as _compiled_file_name10from pathlib import Path11from typing import TYPE_CHECKING12 13from setuptools.dist import Distribution14from setuptools.errors import BaseError15from setuptools.extension import Extension, Library16 17from distutils import log18from distutils.ccompiler import new_compiler19from distutils.sysconfig import customize_compiler, get_config_var20 21if TYPE_CHECKING:22 # Cython not installed on CI tests, causing _build_ext to be `Any`23 from distutils.command.build_ext import build_ext as _build_ext24else:25 try:26 # Attempt to use Cython for building extensions, if available27 from Cython.Distutils.build_ext import build_ext as _build_ext28 29 # Additionally, assert that the compiler module will load30 # also. Ref #1229.31 __import__('Cython.Compiler.Main')32 except ImportError:33 from distutils.command.build_ext import build_ext as _build_ext34 35# make sure _config_vars is initialized36get_config_var("LDSHARED")37# Not publicly exposed in typeshed distutils stubs, but this is done on purpose38# See https://github.com/pypa/setuptools/pull/4228#issuecomment-195985640039from distutils.sysconfig import _config_vars as _CONFIG_VARS # noqa: E40240 41 42def _customize_compiler_for_shlib(compiler):43 if sys.platform == "darwin":44 # building .dylib requires additional compiler flags on OSX; here we45 # temporarily substitute the pyconfig.h variables so that distutils'46 # 'customize_compiler' uses them before we build the shared libraries.47 tmp = _CONFIG_VARS.copy()48 try:49 # XXX Help! I don't have any idea whether these are right...50 _CONFIG_VARS['LDSHARED'] = (51 "gcc -Wl,-x -dynamiclib -undefined dynamic_lookup"52 )53 _CONFIG_VARS['CCSHARED'] = " -dynamiclib"54 _CONFIG_VARS['SO'] = ".dylib"55 customize_compiler(compiler)56 finally:57 _CONFIG_VARS.clear()58 _CONFIG_VARS.update(tmp)59 else:60 customize_compiler(compiler)61 62 63have_rtld = False64use_stubs = False65libtype = 'shared'66 67if sys.platform == "darwin":68 use_stubs = True69elif os.name != 'nt':70 try:71 import dl # type: ignore[import-not-found] # https://github.com/python/mypy/issues/1300272 73 use_stubs = have_rtld = hasattr(dl, 'RTLD_NOW')74 except ImportError:75 pass76 77 78def get_abi3_suffix():79 """Return the file extension for an abi3-compliant Extension()"""80 for suffix in EXTENSION_SUFFIXES:81 if '.abi3' in suffix: # Unix82 return suffix83 elif suffix == '.pyd': # Windows84 return suffix85 return None86 87 88class build_ext(_build_ext):89 distribution: Distribution # override distutils.dist.Distribution with setuptools.dist.Distribution90 editable_mode = False91 inplace = False92 93 def run(self):94 """Build extensions in build directory, then copy if --inplace"""95 old_inplace, self.inplace = self.inplace, False96 _build_ext.run(self)97 self.inplace = old_inplace98 if old_inplace:99 self.copy_extensions_to_source()100 101 def _get_inplace_equivalent(self, build_py, ext: Extension) -> tuple[str, str]:102 fullname = self.get_ext_fullname(ext.name)103 filename = self.get_ext_filename(fullname)104 modpath = fullname.split('.')105 package = '.'.join(modpath[:-1])106 package_dir = build_py.get_package_dir(package)107 inplace_file = os.path.join(package_dir, os.path.basename(filename))108 regular_file = os.path.join(self.build_lib, filename)109 return (inplace_file, regular_file)110 111 def copy_extensions_to_source(self) -> None:112 build_py = self.get_finalized_command('build_py')113 for ext in self.extensions:114 inplace_file, regular_file = self._get_inplace_equivalent(build_py, ext)115 116 # Always copy, even if source is older than destination, to ensure117 # that the right extensions for the current Python/platform are118 # used.119 if os.path.exists(regular_file) or not ext.optional:120 self.copy_file(regular_file, inplace_file, level=self.verbose)121 122 if ext._needs_stub:123 inplace_stub = self._get_equivalent_stub(ext, inplace_file)124 self._write_stub_file(inplace_stub, ext, compile=True)125 # Always compile stub and remove the original (leave the cache behind)126 # (this behaviour was observed in previous iterations of the code)127 128 def _get_equivalent_stub(self, ext: Extension, output_file: str) -> str:129 dir_ = os.path.dirname(output_file)130 _, _, name = ext.name.rpartition(".")131 return f"{os.path.join(dir_, name)}.py"132 133 def _get_output_mapping(self) -> Iterator[tuple[str, str]]:134 if not self.inplace:135 return136 137 build_py = self.get_finalized_command('build_py')138 opt = self.get_finalized_command('install_lib').optimize or ""139 140 for ext in self.extensions:141 inplace_file, regular_file = self._get_inplace_equivalent(build_py, ext)142 yield (regular_file, inplace_file)143 144 if ext._needs_stub:145 # This version of `build_ext` always builds artifacts in another dir,146 # when "inplace=True" is given it just copies them back.147 # This is done in the `copy_extensions_to_source` function, which148 # always compile stub files via `_compile_and_remove_stub`.149 # At the end of the process, a `.pyc` stub file is created without the150 # corresponding `.py`.151 152 inplace_stub = self._get_equivalent_stub(ext, inplace_file)153 regular_stub = self._get_equivalent_stub(ext, regular_file)154 inplace_cache = _compiled_file_name(inplace_stub, optimization=opt)155 output_cache = _compiled_file_name(regular_stub, optimization=opt)156 yield (output_cache, inplace_cache)157 158 def get_ext_filename(self, fullname: str) -> str:159 so_ext = os.getenv('SETUPTOOLS_EXT_SUFFIX')160 if so_ext:161 filename = os.path.join(*fullname.split('.')) + so_ext162 else:163 filename = _build_ext.get_ext_filename(self, fullname)164 ext_suffix = get_config_var('EXT_SUFFIX')165 if not isinstance(ext_suffix, str):166 raise OSError(167 "Configuration variable EXT_SUFFIX not found for this platform "168 "and environment variable SETUPTOOLS_EXT_SUFFIX is missing"169 )170 so_ext = ext_suffix171 172 if fullname in self.ext_map:173 ext = self.ext_map[fullname]174 abi3_suffix = get_abi3_suffix()175 if ext.py_limited_api and abi3_suffix: # Use abi3176 filename = filename[: -len(so_ext)] + abi3_suffix177 if isinstance(ext, Library):178 fn, ext = os.path.splitext(filename)179 return self.shlib_compiler.library_filename(fn, libtype)180 elif use_stubs and ext._links_to_dynamic:181 d, fn = os.path.split(filename)182 return os.path.join(d, 'dl-' + fn)183 return filename184 185 def initialize_options(self):186 _build_ext.initialize_options(self)187 self.shlib_compiler = None188 self.shlibs = []189 self.ext_map = {}190 self.editable_mode = False191 192 def finalize_options(self) -> None:193 _build_ext.finalize_options(self)194 self.extensions = self.extensions or []195 self.check_extensions_list(self.extensions)196 self.shlibs = [ext for ext in self.extensions if isinstance(ext, Library)]197 if self.shlibs:198 self.setup_shlib_compiler()199 for ext in self.extensions:200 ext._full_name = self.get_ext_fullname(ext.name)201 for ext in self.extensions:202 fullname = ext._full_name203 self.ext_map[fullname] = ext204 205 # distutils 3.1 will also ask for module names206 # XXX what to do with conflicts?207 self.ext_map[fullname.split('.')[-1]] = ext208 209 ltd = self.shlibs and self.links_to_dynamic(ext) or False210 ns = ltd and use_stubs and not isinstance(ext, Library)211 ext._links_to_dynamic = ltd212 ext._needs_stub = ns213 filename = ext._file_name = self.get_ext_filename(fullname)214 libdir = os.path.dirname(os.path.join(self.build_lib, filename))215 if ltd and libdir not in ext.library_dirs:216 ext.library_dirs.append(libdir)217 if ltd and use_stubs and os.curdir not in ext.runtime_library_dirs:218 ext.runtime_library_dirs.append(os.curdir)219 220 if self.editable_mode:221 self.inplace = True222 223 def setup_shlib_compiler(self):224 compiler = self.shlib_compiler = new_compiler(225 compiler=self.compiler, dry_run=self.dry_run, force=self.force226 )227 _customize_compiler_for_shlib(compiler)228 229 if self.include_dirs is not None:230 compiler.set_include_dirs(self.include_dirs)231 if self.define is not None:232 # 'define' option is a list of (name,value) tuples233 for name, value in self.define:234 compiler.define_macro(name, value)235 if self.undef is not None:236 for macro in self.undef:237 compiler.undefine_macro(macro)238 if self.libraries is not None:239 compiler.set_libraries(self.libraries)240 if self.library_dirs is not None:241 compiler.set_library_dirs(self.library_dirs)242 if self.rpath is not None:243 compiler.set_runtime_library_dirs(self.rpath)244 if self.link_objects is not None:245 compiler.set_link_objects(self.link_objects)246 247 # hack so distutils' build_extension() builds a library instead248 compiler.link_shared_object = link_shared_object.__get__(compiler) # type: ignore[method-assign]249 250 def get_export_symbols(self, ext):251 if isinstance(ext, Library):252 return ext.export_symbols253 return _build_ext.get_export_symbols(self, ext)254 255 def build_extension(self, ext) -> None:256 ext._convert_pyx_sources_to_lang()257 _compiler = self.compiler258 try:259 if isinstance(ext, Library):260 self.compiler = self.shlib_compiler261 _build_ext.build_extension(self, ext)262 if ext._needs_stub:263 build_lib = self.get_finalized_command('build_py').build_lib264 self.write_stub(build_lib, ext)265 finally:266 self.compiler = _compiler267 268 def links_to_dynamic(self, ext):269 """Return true if 'ext' links to a dynamic lib in the same package"""270 # XXX this should check to ensure the lib is actually being built271 # XXX as dynamic, and not just using a locally-found version or a272 # XXX static-compiled version273 libnames = dict.fromkeys([lib._full_name for lib in self.shlibs])274 pkg = '.'.join(ext._full_name.split('.')[:-1] + [''])275 return any(pkg + libname in libnames for libname in ext.libraries)276 277 def get_source_files(self) -> list[str]:278 return [*_build_ext.get_source_files(self), *self._get_internal_depends()]279 280 def _get_internal_depends(self) -> Iterator[str]:281 """Yield ``ext.depends`` that are contained by the project directory"""282 project_root = Path(self.distribution.src_root or os.curdir).resolve()283 depends = (dep for ext in self.extensions for dep in ext.depends)284 285 def skip(orig_path: str, reason: str) -> None:286 log.info(287 "dependency %s won't be automatically "288 "included in the manifest: the path %s",289 orig_path,290 reason,291 )292 293 for dep in depends:294 path = Path(dep)295 296 if path.is_absolute():297 skip(dep, "must be relative")298 continue299 300 if ".." in path.parts:301 skip(dep, "can't have `..` segments")302 continue303 304 try:305 resolved = (project_root / path).resolve(strict=True)306 except OSError:307 skip(dep, "doesn't exist")308 continue309 310 try:311 resolved.relative_to(project_root)312 except ValueError:313 skip(dep, "must be inside the project root")314 continue315 316 yield path.as_posix()317 318 def get_outputs(self) -> list[str]:319 if self.inplace:320 return list(self.get_output_mapping().keys())321 return sorted(_build_ext.get_outputs(self) + self.__get_stubs_outputs())322 323 def get_output_mapping(self) -> dict[str, str]:324 """See :class:`setuptools.commands.build.SubCommand`"""325 mapping = self._get_output_mapping()326 return dict(sorted(mapping, key=lambda x: x[0]))327 328 def __get_stubs_outputs(self):329 # assemble the base name for each extension that needs a stub330 ns_ext_bases = (331 os.path.join(self.build_lib, *ext._full_name.split('.'))332 for ext in self.extensions333 if ext._needs_stub334 )335 # pair each base with the extension336 pairs = itertools.product(ns_ext_bases, self.__get_output_extensions())337 return list(base + fnext for base, fnext in pairs)338 339 def __get_output_extensions(self):340 yield '.py'341 yield '.pyc'342 if self.get_finalized_command('build_py').optimize:343 yield '.pyo'344 345 def write_stub(self, output_dir, ext, compile=False) -> None:346 stub_file = os.path.join(output_dir, *ext._full_name.split('.')) + '.py'347 self._write_stub_file(stub_file, ext, compile)348 349 def _write_stub_file(self, stub_file: str, ext: Extension, compile=False):350 log.info("writing stub loader for %s to %s", ext._full_name, stub_file)351 if compile and os.path.exists(stub_file):352 raise BaseError(stub_file + " already exists! Please delete.")353 if not self.dry_run:354 with open(stub_file, 'w', encoding="utf-8") as f:355 content = (356 textwrap.dedent(f"""357 def __bootstrap__():358 global __bootstrap__, __file__, __loader__359 import sys, os, importlib.resources as irs, importlib.util360 #rtld import dl361 with irs.files(__name__).joinpath(362 {os.path.basename(ext._file_name)!r}) as __file__:363 del __bootstrap__364 if '__loader__' in globals():365 del __loader__366 #rtld old_flags = sys.getdlopenflags()367 old_dir = os.getcwd()368 try:369 os.chdir(os.path.dirname(__file__))370 #rtld sys.setdlopenflags(dl.RTLD_NOW)371 spec = importlib.util.spec_from_file_location(372 __name__, __file__)373 mod = importlib.util.module_from_spec(spec)374 spec.loader.exec_module(mod)375 finally:376 #rtld sys.setdlopenflags(old_flags)377 os.chdir(old_dir)378 __bootstrap__()379 """)380 .lstrip()381 .replace('#rtld', '#rtld' * (not have_rtld))382 )383 f.write(content)384 if compile:385 self._compile_and_remove_stub(stub_file)386 387 def _compile_and_remove_stub(self, stub_file: str):388 from distutils.util import byte_compile389 390 byte_compile([stub_file], optimize=0, force=True, dry_run=self.dry_run)391 optimize = self.get_finalized_command('install_lib').optimize392 if optimize > 0:393 byte_compile(394 [stub_file],395 optimize=optimize,396 force=True,397 dry_run=self.dry_run,398 )399 if os.path.exists(stub_file) and not self.dry_run:400 os.unlink(stub_file)401 402 403if use_stubs or os.name == 'nt':404 # Build shared libraries405 #406 def link_shared_object(407 self,408 objects,409 output_libname,410 output_dir=None,411 libraries=None,412 library_dirs=None,413 runtime_library_dirs=None,414 export_symbols=None,415 debug: bool = False,416 extra_preargs=None,417 extra_postargs=None,418 build_temp=None,419 target_lang=None,420 ) -> None:421 self.link(422 self.SHARED_LIBRARY,423 objects,424 output_libname,425 output_dir,426 libraries,427 library_dirs,428 runtime_library_dirs,429 export_symbols,430 debug,431 extra_preargs,432 extra_postargs,433 build_temp,434 target_lang,435 )436 437else:438 # Build static libraries everywhere else439 libtype = 'static'440 441 def link_shared_object(442 self,443 objects,444 output_libname,445 output_dir=None,446 libraries=None,447 library_dirs=None,448 runtime_library_dirs=None,449 export_symbols=None,450 debug: bool = False,451 extra_preargs=None,452 extra_postargs=None,453 build_temp=None,454 target_lang=None,455 ) -> None:456 # XXX we need to either disallow these attrs on Library instances,457 # or warn/abort here if set, or something...458 # libraries=None, library_dirs=None, runtime_library_dirs=None,459 # export_symbols=None, extra_preargs=None, extra_postargs=None,460 # build_temp=None461 462 assert output_dir is None # distutils build_ext doesn't pass this463 output_dir, filename = os.path.split(output_libname)464 basename, _ext = os.path.splitext(filename)465 if self.library_filename("x").startswith('lib'):466 # strip 'lib' prefix; this is kludgy if some platform uses467 # a different prefix468 basename = basename[3:]469 470 self.create_static_lib(objects, basename, output_dir, debug, target_lang)471 