# Copyright (C) 2019-2022 Nexedi SA and Contributors. # Kirill Smelkov <kirr@nexedi.com> # # This program is free software: you can Use, Study, Modify and Redistribute # it under the terms of the GNU General Public License version 3, or (at your # option) any later version, as published by the Free Software Foundation. # # You can also Link and Combine this program with other software covered by # the terms of any of the Free Software licenses or any of the Open Source # Initiative approved licenses and Convey the resulting work. Corresponding # source of such a combination shall include the source code for all other # software used. # # This program is distributed WITHOUT ANY WARRANTY; without even the implied # warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. # # See COPYING file for full licensing terms. # See https://www.nexedi.com/licensing for rationale and options. """Package build provides infrastructure to build Cython Pygolang-based packages. Use `setup` and `Extension` to build packages. For example:: from golang.pyx.build import setup, Extension setup( name = 'mypkg', description = 'my Pygolang/Cython-based package', ... ext_modules = [Extension('mypkg.mymod', ['mypkg/mymod.pyx'])], ) """ from __future__ import print_function, absolute_import # pygolang uses setuptools_dso.DSO to build libgolang; all extensions link to it. import setuptools_dso import sys, pkgutil, platform, sysconfig from os.path import dirname, join, exists from distutils.errors import DistutilsError # Error represents a build error. class Error(DistutilsError): pass # _PyPkg provides information about 1 py package. class _PyPkg: # .name - full package name, e.g. "golang.time" # .path - filesystem path of the package # (file for module, directory for pkg/__init__.py) pass # _findpkg finds specified python package and returns information about it. # # e.g. _findpkg("golang") -> _PyPkg("/path/to/pygolang/golang") def _findpkg(pkgname): # -> _PyPkg pkg = pkgutil.get_loader(pkgname) if pkg is None: # package not found raise Error("package %r not found" % (pkgname,)) path = pkg.get_filename() if path.endswith("__init__.py"): path = dirname(path) # .../pygolang/golang/__init__.py -> .../pygolang/golang pypkg = _PyPkg() pypkg.name = pkgname pypkg.path = path return pypkg # build_ext amends setuptools_dso.build_ext to allow combining C and C++ # sources in one extension without hitting `error: invalid argument # '-std=c++11' not allowed with 'C'`. _dso_build_ext = setuptools_dso.build_ext class build_ext(_dso_build_ext): def build_extension(self, ext): # wrap _compiler <src> -> <obj> with our code _compile = self.compiler._compile def _(obj, src, ext, cc_args, extra_postargs, pp_opts): # filter_out removes arguments that start with argprefix cc_args = cc_args[:] extra_postargs = extra_postargs[:] pp_opts = pp_opts[:] def filter_out(argprefix): for l in (cc_args, extra_postargs, pp_opts): _ = [] for arg in l: if not arg.startswith(argprefix): _.append(arg) l[:] = _ # filter-out C++ only options from non-C++ sources # # reason: while gcc only warns about -std=c++ passed with C source, # clang considers that an error. Given that with distutils / # setuptools the _same_ compiler is used to compile C and C++ # sources, and that it is not possible to provide per-source flags, # without filtering, that leads to inability to use both C and C++ # sources in one extension. cxx = (self.compiler.language_map[ext] == 'c++') if not cxx: filter_out('-std=c++') filter_out('-std=gnu++') _compile(obj, src, ext, cc_args, extra_postargs, pp_opts) self.compiler._compile = _ try: _dso_build_ext.build_extension(self, ext) # super doesn't work for _dso_build_ext finally: self.compiler._compile = _compile # setup should be used instead of setuptools.setup def setup(**kw): # setuptools_dso.setup hardcodes setuptools_dso.build_ext to be used. # temporarily inject what user specified in cmdclass, or our code there. _ = setuptools_dso.build_ext try: setuptools_dso.build_ext = kw.get('cmdclass', {}).get('build_ext', build_ext) setuptools_dso.setup(**kw) finally: setuptools_dso.build_ext = _ # DSO should be used to build DSOs that use libgolang. # # For example: # # setup( # ... # x_dsos = [DSO('mypkg.mydso', ['mypkg/mydso.cpp'])], # ) def DSO(name, sources, **kw): _, kw = _with_build_defaults(kw) dso = setuptools_dso.DSO(name, sources, **kw) return dso # _with_build_defaults returns copy of kw amended with build options common for # both DSO and Extension. def _with_build_defaults(kw): # -> (pygo, kw') # find pygolang root gopkg = _findpkg("golang") pygo = dirname(gopkg.path) # .../pygolang/golang -> .../pygolang if pygo == '': pygo = '.' kw = kw.copy() # prepend -I<pygolang> so that e.g. golang/libgolang.h is found incv = kw.get('include_dirs', [])[:] incv.insert(0, pygo) kw['include_dirs'] = incv # link with libgolang.so dsov = kw.get('dsos', [])[:] dsov.insert(0, 'golang.runtime.libgolang') kw['dsos'] = dsov # default language to C++ (chan[T] & co are accessible only via C++) lang = kw.setdefault('language', 'c++') # default to C++11 (chan[T] & co require C++11 features) ccdefault = [] if lang == 'c++': ccdefault.append('-std=c++11') # default to no strict-aliasing ccdefault.append('-fno-strict-aliasing') _ = kw.get('extra_compile_args', [])[:] _[0:0] = ccdefault # if another e.g. -std=... was already there - kw['extra_compile_args'] = _ # - it will override us # some C-level depends to workaround a bit lack of proper dependency # tracking in setuptools/distutils. dependv = kw.get('depends', [])[:] dependv.extend(['%s/golang/%s' % (pygo, _) for _ in [ 'libgolang.h', 'runtime/internal.h', 'runtime/internal/syscall.h', 'context.h', 'cxx.h', 'errors.h', 'fmt.h', 'io.h', 'strings.h', 'sync.h', 'time.h', 'pyx/runtime.h', '_testing.h', ]]) kw['depends'] = dependv return pygo, kw # Extension should be used to build extensions that use pygolang. # # For example: # # setup( # ... # ext_modules = [Extension('mypkg.mymod', ['mypkg/mymod.pyx'])], # ) def Extension(name, sources, **kw): pygo, kw = _with_build_defaults(kw) # some pyx-level depends to workaround a bit lack of proper dependency # tracking in setuptools/distutils. dependv = kw.get('depends', [])[:] dependv.extend(['%s/golang/%s' % (pygo, _) for _ in [ '_golang.pxd', 'runtime/_libgolang.pxd', 'runtime/internal/__init__.pxd', 'runtime/internal/syscall.pxd', '__init__.pxd', 'context.pxd', '_context.pxd', 'cxx.pxd', 'errors.pxd', '_errors.pxd', 'fmt.pxd', '_fmt.pxd', 'io.pxd', '_io.pxd', 'strings.pxd', 'sync.pxd', '_sync.pxd', 'time.pxd', '_time.pxd', 'pyx/runtime.pxd', ]]) kw['depends'] = dependv # workaround pip bug that for virtualenv case headers are installed into # not-searched include path. https://github.com/pypa/pip/issues/4610 # (without this e.g. "greenlet/greenlet.h" is not found) venv_inc = join(sys.prefix, 'include', 'site', 'python' + sysconfig.get_python_version()) if exists(venv_inc): kw['include_dirs'].append(venv_inc) # provide POSIX/PYPY/... defines to Cython POSIX = ('posix' in sys.builtin_module_names) PYPY = (platform.python_implementation() == 'PyPy') pyxenv = kw.get('cython_compile_time_env', {}) pyxenv.setdefault('POSIX', POSIX) pyxenv.setdefault('PYPY', PYPY) gverhex = _gevent_version_hex() if gverhex is not None: pyxenv.setdefault('GEVENT_VERSION_HEX', gverhex) kw['cython_compile_time_env'] = pyxenv # XXX hack, because setuptools_dso.Extension is not Cython.Extension # del from kw to avoid "Unknown Extension options: 'cython_compile_time_env'" #ext = setuptools_dso.Extension(name, sources, **kw) pyxenv = kw.pop('cython_compile_time_env') ext = setuptools_dso.Extension(name, sources, **kw) ext.cython_compile_time_env = pyxenv return ext # _gevent_version_hex returns gevent version in the format of PY_VERSION_HEX. # None is returned if gevent is not available. def _gevent_version_hex(): try: import gevent except ImportError: return None v = gevent.version_info # https://docs.python.org/3/c-api/apiabiversion.html rel = {'dev': 0, 'alpha': 0xa, 'beta': 0xb, 'rc': 0xc, 'final': 0xf} vhex = \ (v.major << (3*8)) | \ (v.minor << (2*8)) | \ (v.micro << (1*8)) | \ (rel[v.releaselevel] << 4) | \ (v.serial << 0) return vhex