#!/usr/bin/env python3 # /*########################################################################## # # Copyright (c) 2015-2022 European Synchrotron Radiation Facility # # Permission is hereby granted, free of charge, to any person obtaining a copy # of this software and associated documentation files (the "Software"), to deal # in the Software without restriction, including without limitation the rights # to use, copy, modify, merge, publish, distribute, sublicense, and/or sell # copies of the Software, and to permit persons to whom the Software is # furnished to do so, subject to the following conditions: # # The above copyright notice and this permission notice shall be included in # all copies or substantial portions of the Software. # # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR # IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, # FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE # AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER # LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN # THE SOFTWARE. # # ###########################################################################*/ __authors__ = ["Jérôme Kieffer", "Thomas Vincent"] __date__ = "29/08/2022" __license__ = "MIT" import sys import os import platform import logging logging.basicConfig(level=logging.INFO) logger = logging.getLogger("silx.setup") try: # setuptools >=62.4.0 from setuptools.command.build import build as _build except ImportError: from distutils.command.build import build as _build from setuptools import Command, Extension, find_packages from setuptools.command.sdist import sdist from setuptools.command.build_ext import build_ext try: import numpy except ImportError: raise ImportError( "To install this package, you must install numpy first\n" "(See https://pypi.org/project/numpy)") PROJECT = "silx" if sys.version_info.major < 3: logger.error(PROJECT + " no longer supports Python2") if "LANG" not in os.environ and sys.platform == "darwin": print("""WARNING: the LANG environment variable is not defined, an utf-8 LANG is mandatory to use setup.py, you may face unexpected UnicodeError. export LANG=en_US.utf-8 export LC_ALL=en_US.utf-8 """) def get_version(debian=False): """Returns current version number from _version.py file""" dirname = os.path.join( os.path.dirname(os.path.abspath(__file__)), "src", PROJECT) sys.path.insert(0, dirname) import _version sys.path = sys.path[1:] return _version.debianversion if debian else _version.strictversion def get_readme(): """Returns content of README.rst file""" dirname = os.path.dirname(os.path.abspath(__file__)) filename = os.path.join(dirname, "README.rst") with open(filename, "r", encoding="utf-8") as fp: long_description = fp.read() return long_description classifiers = ["Development Status :: 5 - Production/Stable", "Environment :: Console", "Environment :: MacOS X", "Environment :: Win32 (MS Windows)", "Environment :: X11 Applications :: Qt", "Intended Audience :: Education", "Intended Audience :: Science/Research", "License :: OSI Approved :: MIT License", "Natural Language :: English", "Operating System :: MacOS", "Operating System :: Microsoft :: Windows", "Operating System :: POSIX", "Programming Language :: Cython", "Programming Language :: Python :: 3", "Programming Language :: Python :: Implementation :: CPython", "Topic :: Scientific/Engineering :: Physics", "Topic :: Software Development :: Libraries :: Python Modules", ] class BuildMan(Command): """Command to build man pages""" description = "Build man pages of the provided entry points" user_options = [] def initialize_options(self): pass def finalize_options(self): pass def entry_points_iterator(self): """Iterate other entry points available on the project.""" entry_points = self.distribution.entry_points console_scripts = entry_points.get('console_scripts', []) gui_scripts = entry_points.get('gui_scripts', []) scripts = [] scripts.extend(console_scripts) scripts.extend(gui_scripts) for script in scripts: # Remove ending extra dependencies script = script.split("[")[0] elements = script.split("=") target_name = elements[0].strip() elements = elements[1].split(":") module_name = elements[0].strip() function_name = elements[1].strip() yield target_name, module_name, function_name def run_targeted_script(self, target_name, script_name, env, log_output=False): """Execute targeted script using --help and --version to help checking errors. help2man is not very helpful to do it for us. :return: True is both return code are equal to 0 :rtype: bool """ import subprocess if log_output: extra_args = {} else: try: # Python 3 from subprocess import DEVNULL except ImportError: # Python 2 import os DEVNULL = open(os.devnull, 'wb') extra_args = {'stdout': DEVNULL, 'stderr': DEVNULL} succeeded = True command_line = [sys.executable, script_name, "--help"] if log_output: logger.info("See the following execution of: %s", " ".join(command_line)) p = subprocess.Popen(command_line, env=env, **extra_args) status = p.wait() if log_output: logger.info("Return code: %s", status) succeeded = succeeded and status == 0 command_line = [sys.executable, script_name, "--version"] if log_output: logger.info("See the following execution of: %s", " ".join(command_line)) p = subprocess.Popen(command_line, env=env, **extra_args) status = p.wait() if log_output: logger.info("Return code: %s", status) succeeded = succeeded and status == 0 return succeeded @staticmethod def _write_script(target_name, lst_lines=None): """Write a script to a temporary file and return its name :paran target_name: base of the script name :param lst_lines: list of lines to be written in the script :return: the actual filename of the script (for execution or removal) """ import tempfile import stat script_fid, script_name = tempfile.mkstemp(prefix="%s_" % target_name, text=True) with os.fdopen(script_fid, 'wt') as script: for line in lst_lines: if not line.endswith("\n"): line += "\n" script.write(line) # make it executable mode = os.stat(script_name).st_mode os.chmod(script_name, mode + stat.S_IEXEC) return script_name def get_synopsis(self, module_name, env, log_output=False): """Execute a script to retrieve the synopsis for help2man :return: synopsis :rtype: single line string """ import subprocess script_name = None synopsis = None script = ["#!%s\n" % sys.executable, "import logging", "logging.basicConfig(level=logging.ERROR)", "import %s as app" % module_name, "print(app.__doc__)"] try: script_name = self._write_script(module_name, script) command_line = [sys.executable, script_name] p = subprocess.Popen(command_line, env=env, stdout=subprocess.PIPE) status = p.wait() if status != 0: logger.warning("Error while getting synopsis for module '%s'.", module_name) synopsis = p.stdout.read().decode("utf-8").strip() if synopsis == 'None': synopsis = None finally: # clean up the script if script_name is not None: os.remove(script_name) return synopsis def run(self): build = self.get_finalized_command('build') path = sys.path path.insert(0, os.path.abspath(build.build_lib)) env = dict((str(k), str(v)) for k, v in os.environ.items()) # env["PYTHONPATH"] = os.pathsep.join(path) if not os.path.isdir("build/man"): os.makedirs("build/man") import subprocess import tempfile import stat script_name = None workdir = tempfile.mkdtemp() entry_points = self.entry_points_iterator() for target_name, module_name, function_name in entry_points: logger.info("Build man for entry-point target '%s'" % target_name) # help2man expect a single executable file to extract the help # we create it, execute it, and delete it at the end try: # create a launcher using the right python interpreter script_name = os.path.join(workdir, target_name) with open(script_name, "wt") as script: script.write("#!%s\n" % sys.executable) script.write("import %s as app\n" % module_name) script.write("app.%s()\n" % function_name) # make it executable mode = os.stat(script_name).st_mode os.chmod(script_name, mode + stat.S_IEXEC) # execute help2man man_file = "build/man/%s.1" % target_name command_line = ["help2man", "-N", script_name, "-o", man_file] synopsis = self.get_synopsis(module_name, env) if synopsis: command_line += ["-n", synopsis] p = subprocess.Popen(command_line, env=env) status = p.wait() if status != 0: logger.info("Error while generating man file for target '%s'.", target_name) self.run_targeted_script(target_name, script_name, env, True) raise RuntimeError("Fail to generate '%s' man documentation" % target_name) finally: # clean up the script if script_name is not None: os.remove(script_name) os.rmdir(workdir) # ############## # # Compiler flags # # ############## # class Build(_build): """Command to support more user options for the build.""" user_options = [ ('no-openmp', None, "DEPRECATED: Instead, set the environment variable SILX_WITH_OPENMP to False"), ('openmp', None, "DEPRECATED: Instead, set the environment variable SILX_WITH_OPENMP to True"), ('force-cython', None, "DEPRECATED: Instead, set the environment variable SILX_FORCE_CYTHON to True"), ] user_options.extend(_build.user_options) boolean_options = ['no-openmp', 'openmp', 'force-cython'] boolean_options.extend(_build.boolean_options) def initialize_options(self): _build.initialize_options(self) self.no_openmp = None self.openmp = None self.force_cython = None def finalize_options(self): _build.finalize_options(self) if self.no_openmp is not None: logger.warning("--no-openmp is deprecated: Instead, set the environment variable SILX_WITH_OPENMP to False") if self.openmp is not None: logger.warning("--openmp is deprecated: Instead, set the environment variable SILX_WITH_OPENMP to True") if self.force_cython is not None: logger.warning("--force-cython is deprecated: Instead, set the environment variable SILX_FORCE_CYTHON to True") if not self.force_cython: self.force_cython = self._parse_env_as_bool("SILX_FORCE_CYTHON") is True self.finalize_openmp_options() def _parse_env_as_bool(self, key): content = os.environ.get(key, "") value = content.lower() if value in ["1", "true", "yes", "y"]: return True if value in ["0", "false", "no", "n"]: return False if value in ["none", ""]: return None msg = "Env variable '%s' contains '%s'. But a boolean or an empty \ string was expected. Variable ignored." logger.warning(msg, key, content) return None def finalize_openmp_options(self): """Check if extensions must be compiled with OpenMP. The result is stored into the object. """ if self.openmp: use_openmp = True elif self.no_openmp: use_openmp = False else: env_with_openmp = self._parse_env_as_bool("SILX_WITH_OPENMP") if env_with_openmp is not None: use_openmp = env_with_openmp else: # Use it by default use_openmp = True if use_openmp and platform.system() == "Darwin": logger.warning("OpenMP support ignored. Your platform does not support it.") use_openmp = False # Remove attributes used by distutils parsing # use 'use_openmp' instead del self.no_openmp del self.openmp self.use_openmp = use_openmp class BuildExt(build_ext): """Handle extension compilation. Command-line argument and environment can custom: - The use of cython to cythonize files, else a default version is used - Build extension with support of OpenMP (by default it is enabled) - If building with MSVC, compiler flags are converted from gcc flags. """ COMPILE_ARGS_CONVERTER = {'-fopenmp': '/openmp'} LINK_ARGS_CONVERTER = {'-fopenmp': ''} description = 'Build extensions' def finalize_options(self): build_ext.finalize_options(self) build_obj = self.distribution.get_command_obj("build") self.use_openmp = build_obj.use_openmp self.force_cython = build_obj.force_cython def patch_extension(self, ext): """ Patch an extension according to requested Cython and OpenMP usage. :param Extension ext: An extension """ # Cytonize from Cython.Build import cythonize patched_exts = cythonize( [ext], compiler_directives={'embedsignature': True, 'language_level': 3}, force=self.force_cython ) ext.sources = patched_exts[0].sources # Remove OpenMP flags if OpenMP is disabled if not self.use_openmp: ext.extra_compile_args = [ f for f in ext.extra_compile_args if f != '-fopenmp'] ext.extra_link_args = [ f for f in ext.extra_link_args if f != '-fopenmp'] # Convert flags from gcc to MSVC if required if self.compiler.compiler_type == 'msvc': extra_compile_args = [self.COMPILE_ARGS_CONVERTER.get(f, f) for f in ext.extra_compile_args] # Avoid empty arg ext.extra_compile_args = [arg for arg in extra_compile_args if arg] extra_link_args = [self.LINK_ARGS_CONVERTER.get(f, f) for f in ext.extra_link_args] # Avoid empty arg ext.extra_link_args = [arg for arg in extra_link_args if arg] def build_extensions(self): for ext in self.extensions: self.patch_extension(ext) build_ext.build_extensions(self) ################################################################################ # Debian source tree ################################################################################ class sdist_debian(sdist): """ Tailor made sdist for debian * remove auto-generated doc * remove cython generated .c files * remove cython generated .cpp files * remove .bat files * include .l man files """ description = "Create a source distribution for Debian (tarball, zip file, etc.)" @staticmethod def get_debian_name(): name = "%s_%s" % (PROJECT, get_version(debian=True)) return name def prune_file_list(self): sdist.prune_file_list(self) to_remove = ["doc/build", "doc/pdf", "doc/html", "pylint", "epydoc"] print("Removing files for debian") for rm in to_remove: self.filelist.exclude_pattern(pattern="*", anchor=False, prefix=rm) # this is for Cython files specifically: remove C & html files search_root = os.path.dirname(os.path.abspath(__file__)) for root, _, files in os.walk(search_root): for afile in files: if os.path.splitext(afile)[1].lower() == ".pyx": base_file = os.path.join(root, afile)[len(search_root) + 1:-4] self.filelist.exclude_pattern(pattern=base_file + ".c") self.filelist.exclude_pattern(pattern=base_file + ".cpp") self.filelist.exclude_pattern(pattern=base_file + ".html") # do not include third_party/_local files self.filelist.exclude_pattern(pattern="*", prefix="silx/third_party/_local") def make_distribution(self): self.prune_file_list() sdist.make_distribution(self) dest = self.archive_files[0] dirname, basename = os.path.split(dest) base, ext = os.path.splitext(basename) while ext in [".zip", ".tar", ".bz2", ".gz", ".Z", ".lz", ".orig"]: base, ext = os.path.splitext(base) # if ext: # dest = "".join((base, ext)) # else: # dest = base # sp = dest.split("-") # base = sp[:-1] # nr = sp[-1] debian_arch = os.path.join(dirname, self.get_debian_name() + ".orig.tar.gz") os.rename(self.archive_files[0], debian_arch) self.archive_files = [debian_arch] print("Building debian .orig.tar.gz in %s" % self.archive_files[0]) # ##### # # setup # # ##### # def get_project_configuration(): """Returns project arguments for setup""" # Use installed numpy version as minimal required version # This is useful for wheels to advertise the numpy version they were built with numpy_requested_version = ">=%s" % numpy.version.version logger.info("Install requires: numpy %s", numpy_requested_version) install_requires = [ # for most of the computation "numpy%s" % numpy_requested_version, # for the script launcher and pkg_resources "setuptools", # for io support "h5py", "fabio>=0.9", ] # extras requirements: target 'full' to install all dependencies at once full_requires = [ # opencl 'pyopencl', 'Mako', # gui 'qtconsole', 'matplotlib>=1.2.0', 'PyOpenGL', 'python-dateutil', 'PyQt5', # extra 'scipy', 'Pillow'] test_requires = [ "pytest", "pytest-xvfb" ] extras_require = { 'full': full_requires, 'test': test_requires, } # Here for packaging purpose only # Setting the SILX_FULL_INSTALL_REQUIRES environment variable # put all dependencies as install_requires if os.environ.get('SILX_FULL_INSTALL_REQUIRES') is not None: install_requires += full_requires # Set the SILX_INSTALL_REQUIRES_STRIP env. var. to a comma-separated # list of package names to remove them from install_requires install_requires_strip = os.environ.get('SILX_INSTALL_REQUIRES_STRIP') if install_requires_strip is not None: for package_name in install_requires_strip.split(','): install_requires.remove(package_name) package_data = { # Resources files for silx 'silx.resources': [ 'gui/logo/*.png', 'gui/logo/*.svg', 'gui/icons/*.png', 'gui/icons/*.svg', 'gui/icons/*.mng', 'gui/icons/*.gif', 'gui/icons/*/*.png', 'opencl/*.cl', 'opencl/image/*.cl', 'opencl/sift/*.cl', 'opencl/codec/*.cl', 'gui/colormaps/*.npy'], 'silx.examples': ['*.png'], } entry_points = { 'console_scripts': ['silx = silx.__main__:main'], # 'gui_scripts': [], } cmdclass = dict( build=Build, build_ext=BuildExt, build_man=BuildMan, debian_src=sdist_debian) def silx_io_specfile_define_macros(): # Locale and platform management if sys.platform == "win32": return [('WIN32', None), ('SPECFILE_POSIX', None)] elif os.name.lower().startswith('posix'): # the best choice is to have _GNU_SOURCE defined # as a compilation flag because that allows the # use of strtod_l use_gnu_source = os.environ.get("SPECFILE_USE_GNU_SOURCE", "False") if use_gnu_source in ("True", "1"): # 1 was the initially supported value return [('_GNU_SOURCE', 1)] return [('SPECFILE_POSIX', None)] else: return [] ext_modules = [ # silx.image Extension( name='silx.image.bilinear', sources=["src/silx/image/bilinear.pyx"], language='c', ), Extension( name='silx.image.marchingsquares._mergeimpl', sources=['src/silx/image/marchingsquares/_mergeimpl.pyx'], include_dirs=[ numpy.get_include(), os.path.join(os.path.dirname(__file__), "src", "silx", "utils", "include") ], language='c++', extra_link_args=['-fopenmp'], extra_compile_args=['-fopenmp'], ), Extension( name='silx.image.shapes', sources=["src/silx/image/shapes.pyx"], language='c', ), # silx.io Extension( name='silx.io.specfile', sources=[ 'src/silx/io/specfile/src/sfheader.c', 'src/silx/io/specfile/src/sfinit.c', 'src/silx/io/specfile/src/sflists.c', 'src/silx/io/specfile/src/sfdata.c', 'src/silx/io/specfile/src/sfindex.c', 'src/silx/io/specfile/src/sflabel.c', 'src/silx/io/specfile/src/sfmca.c', 'src/silx/io/specfile/src/sftools.c', 'src/silx/io/specfile/src/locale_management.c', 'src/silx/io/specfile.pyx', ], define_macros=silx_io_specfile_define_macros(), include_dirs=['src/silx/io/specfile/include'], language='c', ), # silx.math Extension( name='silx.math._colormap', sources=["src/silx/math/_colormap.pyx"], language='c', include_dirs=[ 'src/silx/math/include', numpy.get_include(), ], extra_link_args=['-fopenmp'], extra_compile_args=['-fopenmp'], ), Extension( name='silx.math.chistogramnd', sources=[ 'src/silx/math/histogramnd/src/histogramnd_c.c', 'src/silx/math/chistogramnd.pyx', ], include_dirs=[ 'src/silx/math/histogramnd/include', numpy.get_include(), ], language='c', ), Extension( name='silx.math.chistogramnd_lut', sources=['src/silx/math/chistogramnd_lut.pyx'], include_dirs=[ 'src/silx/math/histogramnd/include', numpy.get_include(), ], language='c', ), Extension( name='silx.math.combo', sources=['src/silx/math/combo.pyx'], include_dirs=['src/silx/math/include'], language='c', ), Extension( name='silx.math.interpolate', sources=["src/silx/math/interpolate.pyx"], language='c', include_dirs=[ 'src/silx/math/include', numpy.get_include(), ], extra_link_args=['-fopenmp'], extra_compile_args=['-fopenmp'], ), Extension( name='silx.math.marchingcubes', sources=[ 'src/silx/math/marchingcubes/mc_lut.cpp', 'src/silx/math/marchingcubes.pyx', ], include_dirs=[ 'src/silx/math/marchingcubes', numpy.get_include(), ], language='c++', ), Extension( name='silx.math.medianfilter.medianfilter', sources=['src/silx/math/medianfilter/medianfilter.pyx'], include_dirs=[ 'src/silx/math/medianfilter/include', numpy.get_include(), ], language='c++', extra_link_args=['-fopenmp'], extra_compile_args=['-fopenmp'], ), # silx.math.fit Extension( name='silx.math.fit.filters', sources=[ 'src/silx/math/fit/filters/src/smoothnd.c', 'src/silx/math/fit/filters/src/snip1d.c', 'src/silx/math/fit/filters/src/snip2d.c', 'src/silx/math/fit/filters/src/snip3d.c', 'src/silx/math/fit/filters/src/strip.c', 'src/silx/math/fit/filters.pyx', ], include_dirs=['src/silx/math/fit/filters/include'], language='c', ), Extension( name='silx.math.fit.functions', sources=[ 'src/silx/math/fit/functions/src/funs.c', 'src/silx/math/fit/functions.pyx', ], include_dirs=['src/silx/math/fit/functions/include'], language='c', ), Extension( name='silx.math.fit.peaks', sources=[ 'src/silx/math/fit/peaks/src/peaks.c', 'src/silx/math/fit/peaks.pyx', ], include_dirs=['src/silx/math/fit/peaks/include'], language='c', ), ] # silx.third_party if os.path.exists(os.path.join( os.path.dirname(__file__), "src", "silx", "third_party", "_local") ): ext_modules.append( Extension( name='silx.third_party._local.scipy_spatial.qhull', sources=[ 'src/silx/third_party/_local/scipy_spatial/qhull/src/' + fname for fname in ( 'geom2_r.c', 'geom_r.c', 'global_r.c', 'io_r.c', 'libqhull_r.c', 'mem_r.c', 'merge_r.c', 'poly2_r.c', 'poly_r.c', 'qset_r.c', 'random_r.c', 'rboxlib_r.c', 'stat_r.c', 'usermem_r.c', 'userprintf_rbox_r.c', 'userprintf_r.c', 'user_r.c' )] + [ 'src/silx/third_party/_local/scipy_spatial/qhull.pyx', ], include_dirs=[numpy.get_include()], ) ) return dict( name=PROJECT, version=get_version(), license="MIT", url="http://www.silx.org/", author="data analysis unit", author_email="silx@esrf.fr", classifiers=classifiers, description="Software library for X-ray data analysis", long_description=get_readme(), install_requires=install_requires, extras_require=extras_require, python_requires='>=3.5', cmdclass=cmdclass, zip_safe=False, entry_points=entry_points, packages=find_packages(where='src', include=['silx*']) + ['silx.examples'], package_dir={ "": "src", "silx.examples": "examples", }, ext_modules=ext_modules, package_data=package_data, ) if __name__ == "__main__": from setuptools import setup setup(**get_project_configuration())