diff --git a/setup.py b/setup.py index 20d088801044..772048cb57ea 100644 --- a/setup.py +++ b/setup.py @@ -10,6 +10,7 @@ use_setuptools() import sys +from collections import namedtuple # distutils is breaking our sdists for files in symlinked dirs. # distutils will copy if os.link is not available, so this is a hack @@ -55,52 +56,69 @@ # These are the packages in the order we want to display them. This # list may contain strings to create section headers for the display. + +Spacer = namedtuple('Spacer', ['text']) + +# these packages are dependencies for other packages +# give them names so that we can reference them later +numpy_pkg = setupext.Numpy() +cxx_pkg = setupext.CXX() +libagg_pkg = setupext.LibAgg() +freetype_pkg = setupext.FreeType() + mpl_packages = [ - 'Building Matplotlib', + Spacer('Building Matplotlib'), setupext.Matplotlib(), setupext.Python(), setupext.Platform(), - 'Required dependencies and extensions', - setupext.Numpy(), + + Spacer('Required dependencies and extensions'), + numpy_pkg, setupext.Dateutil(), setupext.Tornado(), setupext.Pyparsing(), - setupext.CXX(), - setupext.LibAgg(), - setupext.FreeType(), - setupext.FT2Font(), - setupext.Png(), - setupext.Image(), - setupext.TTConv(), - setupext.Path(), - setupext.Contour(), - setupext.Delaunay(), - setupext.Tri(), - 'Optional subpackages', + cxx_pkg, + libagg_pkg, + freetype_pkg, + setupext.FT2Font(dependencies=[freetype_pkg, numpy_pkg, cxx_pkg]), + setupext.Png(dependencies=[numpy_pkg, cxx_pkg]), + setupext.Image(dependencies=[numpy_pkg, libagg_pkg, cxx_pkg]), + setupext.TTConv(dependencies=[numpy_pkg, cxx_pkg]), + setupext.Path(dependencies=[numpy_pkg, libagg_pkg, cxx_pkg]), + setupext.Contour(dependencies=[numpy_pkg]), + setupext.Delaunay(dependencies=[numpy_pkg]), + setupext.Tri(dependencies=[numpy_pkg]), + + Spacer('Optional subpackages'), setupext.SampleData(), setupext.Toolkits(), setupext.Tests(), - 'Optional backend extensions', + + Spacer('Optional backend extensions'), # These backends are listed in order of preference, the first # being the most preferred. The first one that looks like it will # work will be selected as the default backend. - setupext.BackendMacOSX(), + setupext.BackendMacOSX(dependencies=[numpy_pkg, libagg_pkg, cxx_pkg]), setupext.BackendQt4(), setupext.BackendGtk3Agg(), setupext.BackendGtk3Cairo(), - setupext.BackendGtkAgg(), - setupext.BackendTkAgg(), + setupext.BackendGtkAgg(dependencies=[libagg_pkg, cxx_pkg, numpy_pkg]), + setupext.BackendTkAgg(dependencies=[numpy_pkg, libagg_pkg, cxx_pkg]), setupext.BackendWxAgg(), - setupext.BackendGtk(), - setupext.BackendAgg(), + setupext.BackendGtk(dependencies=[numpy_pkg]), + setupext.BackendAgg(dependencies=[numpy_pkg, + libagg_pkg, + freetype_pkg, + cxx_pkg]), setupext.BackendCairo(), setupext.Windowing(), - 'Optional LaTeX dependencies', + + Spacer('Optional LaTeX dependencies'), setupext.DviPng(), setupext.Ghostscript(), setupext.LaTeX(), - setupext.PdfToPs() - ] + setupext.PdfToPs(), +] classifiers = [ @@ -137,28 +155,29 @@ required_failed = [] good_packages = [] for package in mpl_packages: - if isinstance(package, str): + if isinstance(package, Spacer): print_raw('') - print_raw(package.upper()) - else: - try: - result = package.check() - if result is not None: - message = 'yes [%s]' % result - print_status(package.name, message) - except setupext.CheckFailed as e: - msg = str(e).strip() - if len(msg): - print_status(package.name, 'no [%s]' % msg) - else: - print_status(package.name, 'no') - if not package.optional: - required_failed.append(package) + print_raw(package.text.upper()) + continue + + try: + result = package.check() + if result is not None: + message = 'yes [%s]' % result + print_status(package.name, message) + except setupext.CheckFailed as e: + msg = str(e).strip() + if len(msg): + print_status(package.name, 'no [%s]' % msg) else: - good_packages.append(package) - if isinstance(package, setupext.OptionalBackendPackage): - if default_backend is None: - default_backend = package.name + print_status(package.name, 'no') + if not package.optional: + required_failed.append(package) + else: + good_packages.append(package) + if isinstance(package, setupext.OptionalBackendPackage): + if default_backend is None: + default_backend = package.name print_raw('') diff --git a/setupext.py b/setupext.py index 02ef9ee54c86..b10013fcfdcc 100644 --- a/setupext.py +++ b/setupext.py @@ -143,9 +143,9 @@ def get_base_dirs(): return options['basedirlist'] basedir_map = { - 'win32': ['win32_static',], + 'win32': ['win32_static'], 'darwin': ['/usr/local/', '/usr', '/usr/X11', '/opt/local'], - 'sunos5': [os.getenv('MPLIB_BASE') or '/usr/local',], + 'sunos5': [os.getenv('MPLIB_BASE') or '/usr/local'], 'gnu0': ['/usr'], 'aix5': ['/usr/local'], } @@ -240,98 +240,190 @@ def make_extension(name, files, *args, **kwargs): return ext -class PkgConfig(object): +class Configurator(object): """ This is a class for communicating with pkg-config. """ - def __init__(self): + def __init__(self, config_command): """ Determines whether pkg-config exists on this machine. """ + self.config_command = config_command + if sys.platform == 'win32': - self.has_pkgconfig = False + self.has_config = False else: - self.set_pkgconfig_path() - status, output = getstatusoutput("pkg-config --help") - self.has_pkgconfig = (status == 0) - - def set_pkgconfig_path(self): - pkgconfig_path = sysconfig.get_config_var('LIBDIR') - if pkgconfig_path is None: - return - - pkgconfig_path = os.path.join(pkgconfig_path, 'pkgconfig') - if not os.path.isdir(pkgconfig_path): - return + self.set_config_path() + status, output = getstatusoutput(self.config_command + ' --help') + self.has_config = (status == 0) - try: - os.environ['PKG_CONFIG_PATH'] += ':' + pkgconfig_path - except KeyError: - os.environ['PKG_CONFIG_PATH'] = pkgconfig_path + def set_config_path(self): + """Use for extra setup""" + pass - def setup_extension(self, ext, package, default_include_dirs=[], - default_library_dirs=[], default_libraries=[], - alt_exec=None): + def setup_extension(self, ext, package, + default_include_dirs=[], + default_library_dirs=[], default_libraries=[]): """ Add parameters to the given `ext` for the given `package`. """ flag_map = { '-I': 'include_dirs', '-L': 'library_dirs', '-l': 'libraries'} - executable = alt_exec - if self.has_pkgconfig: - executable = 'pkg-config {0}'.format(package) - - use_defaults = True - - if executable is not None: - command = "{0} --libs --cflags ".format(executable) + if self.has_config: + command = "{cfg} {pkg} --libs --cflags".format( + cfg=self.config_command, pkg=package) try: - output = check_output(command, shell=True, + output = check_output(command, + shell=True, stderr=subprocess.STDOUT) except subprocess.CalledProcessError: pass else: output = output.decode(sys.getfilesystemencoding()) - use_defaults = False for token in output.split(): attr = flag_map.get(token[:2]) if attr is not None: getattr(ext, attr).append(token[2:]) + return + + # couldn't find library, fallback to looking in default paths + basedirs = get_base_dirs() + for base in basedirs: + for include in default_include_dirs: + d = os.path.join(base, include) + if os.path.exists(d): + ext.include_dirs.append(d) + for lib in default_library_dirs: + d = os.path.join(base, lib) + if os.path.exists(d): + ext.library_dirs.append(d) + ext.libraries.extend(default_libraries) - if use_defaults: - basedirs = get_base_dirs() - for base in basedirs: - for include in default_include_dirs: - dir = os.path.join(base, include) - if os.path.exists(dir): - ext.include_dirs.append(dir) - for lib in default_library_dirs: - dir = os.path.join(base, lib) - if os.path.exists(dir): - ext.library_dirs.append(dir) - ext.libraries.extend(default_libraries) - return True + def get_version(self, package): + raise NotImplementedError() + + def check_for_config(self, package, include_file, ext, + min_version=None, version=None): + """ + A convenience function for writing checks for a + pkg_config-defined dependency. + + `package` is the pkg_config package name. + + `include_file` is a top-level include file we expect to find. + + `min_version` is the minimum version required. + + `version` will override the found version if this package + requires an alternate method for that. + """ + if version is None: + version = self.get_version(package) + + if version is None: + raise CheckFailed( + "%s information for '%s' could not be found." % + (self.config_command, package)) + + if min_version == 'PATCH': + raise CheckFailed( + "Requires patches that have not been merged upstream.") + + if min_version: + if (not is_min_version(version, min_version)): + raise CheckFailed( + "Requires %s %s or later. Found %s." % + (package, min_version, version)) + + if ext is None: + ext = make_extension('test', []) + self.setup_extension(ext, package) + + check_include_file(ext.include_dirs, include_file, package) - return False + return 'version %s' % version + + +class PkgConfig(Configurator): + + def set_config_path(self): + # ask python if it has a pkg-config libdir + config_path = sysconfig.get_config_var('LIBDIR') + if config_path is None: + return + + pkgconfig_path = os.path.join(config_path, 'pkgconfig') + if not os.path.isdir(pkgconfig_path): + return + + try: + os.environ['PKG_CONFIG_PATH'] += ':' + pkgconfig_path + except KeyError: + os.environ['PKG_CONFIG_PATH'] = pkgconfig_path def get_version(self, package): """ Get the version of the package from pkg-config. """ - if not self.has_pkgconfig: + if not self.has_config: return None status, output = getstatusoutput( - "pkg-config %s --modversion" % (package)) + "pkg-config %s --modversion" % package) + if status == 0: + return output + return None + + +class FreetypeConfig(Configurator): + """This is a class for communicating with freetype-config.""" + + def setup_extension(self, ext, package, **kw): + """Add parameters to the given `ext` for the given `package`.""" + if package != 'freetype2': + raise RuntimeError('package must be "freetype2"') + package = '' + return super(FreetypeConfig, self).setup_extension(ext, package, **kw) + + def get_version(self, package): + """Get the version from freetype-config.""" + if package != 'freetype2': + raise RuntimeError('package must be "freetype2"') + + if not self.has_config: + return None + + status, output = getstatusoutput("freetype-config --ftversion") + if status == 0: + return output + return None + + +class LibpngConfig(Configurator): + + def setup_extension(self, ext, package, **kw): + """Add parameters to the given `ext` for the given `package`.""" + if package != 'freetype2': + raise RuntimeError('package must be "freetype2"') + package = '' + return super(LibpngConfig, self).setup_extension(ext, package, **kw) + + def get_version(self, package): + if package != 'libpng': + raise RuntimeError('package must be libpng') + + status, output = getstatusoutput('libpng-config --version') if status == 0: return output return None -# The PkgConfig class should be used through this singleton -pkg_config = PkgConfig() +# The PkgConfig/FreetypeConfig class should be used through singletons +pkg_config = PkgConfig('pkg-config') +ft_config = FreetypeConfig('freetype-config') +png_config = LibpngConfig('libpng-config') class CheckFailed(Exception): @@ -344,6 +436,9 @@ class CheckFailed(Exception): class SetupPackage(object): optional = False + def __init__(self, dependencies=None): + self.dependencies = dependencies if dependencies is not None else [] + def check(self): """ Checks whether the dependencies are met. Should raise a @@ -369,7 +464,6 @@ def get_namespace_packages(self): """ return [] - def get_py_modules(self): """ Get a list of top-level modules to add to the configuration. @@ -402,47 +496,9 @@ def get_install_requires(self): """ return [] - def _check_for_pkg_config(self, package, include_file, min_version=None, - version=None): - """ - A convenience function for writing checks for a - pkg_config-defined dependency. - - `package` is the pkg_config package name. - - `include_file` is a top-level include file we expect to find. - - `min_version` is the minimum version required. - - `version` will override the found version if this package - requires an alternate method for that. - """ - if version is None: - version = pkg_config.get_version(package) - - if version is None: - raise CheckFailed( - "pkg-config information for '%s' could not be found." % - package) - - if min_version == 'PATCH': - raise CheckFailed( - "Requires patches that have not been merged upstream.") - - if min_version: - if (not is_min_version(version, min_version)): - raise CheckFailed( - "Requires %s %s or later. Found %s." % - (package, min_version, version)) - - ext = self.get_extension() - if ext is None: - ext = make_extension('test', []) - pkg_config.setup_extension(ext, package) - - check_include_file(ext.include_dirs, include_file, package) - - return 'version %s' % version + def add_dep_flags(self, ext): + for dep in self.dependencies: + dep.add_flags(ext) class OptionalPackage(SetupPackage): @@ -532,8 +588,7 @@ def get_py_modules(self): def get_package_data(self): return { - 'matplotlib': - [ + 'matplotlib': [ 'mpl-data/fonts/afm/*.afm', 'mpl-data/fonts/pdfcorefonts/*.afm', 'mpl-data/fonts/pdfcorefonts/*.txt', @@ -554,8 +609,9 @@ def get_package_data(self): 'backends/web_backend/jquery/css/themes/base/*.*', 'backends/web_backend/jquery/css/themes/base/images/*', 'backends/web_backend/css/*.*', - 'backends/Matplotlib.nib/*' - ]} + 'backends/Matplotlib.nib/*', + ], + } class SampleData(OptionalPackage): @@ -651,8 +707,8 @@ def check(self): ext = make_extension('test', []) ext.include_dirs.append(numpy.get_include()) - if not has_include_file( - ext.include_dirs, os.path.join("numpy", "arrayobject.h")): + if not has_include_file(ext.include_dirs, + os.path.join("numpy", "arrayobject.h")): raise CheckFailed( "The C headers for numpy could not be found. You" "may need to install the development package.") @@ -697,9 +753,11 @@ def check(self): sys.stdout = old_stdout try: - return self._check_for_pkg_config( - 'PyCXX', 'CXX/Extensions.hxx', min_version='6.2.4') - except CheckFailed as e: + return pkg_config.check_for_config('PyCXX', + 'CXX/Extensions.hxx', + self.get_extension(), + min_version='6.2.4') + except CheckFailed: # It's ok to just proceed here, since the `import CXX` # worked above, and PyCXX (at least upstream) ensures that # its header files are on the default distutils include @@ -710,12 +768,12 @@ def check(self): def add_flags(self, ext): if self.found_external and not 'sdist' in sys.argv: support_dir = os.path.normpath( - os.path.join( - sys.prefix, - 'share', - 'python%d.%d' % ( - sys.version_info[0], sys.version_info[1]), - 'CXX')) + os.path.join( + sys.prefix, + 'share', + 'python%d.%d' % (sys.version_info[0], sys.version_info[1]), + 'CXX') + ) if not os.path.exists(support_dir): # On Fedora 17, these files are installed in /usr/share/CXX support_dir = '/usr/src/CXX' @@ -742,8 +800,10 @@ class LibAgg(SetupPackage): def check(self): self.__class__.found_external = True try: - return self._check_for_pkg_config( - 'libagg', 'agg2/agg_basics.h', min_version='PATCH') + return pkg_config.check_for_config('libagg', + 'agg2/agg_basics.h', + self.get_extension(), + min_version='PATCH') except CheckFailed as e: self.__class__.found_external = False return str(e) + ' Using local copy.' @@ -774,26 +834,42 @@ def check(self): if sys.platform == 'win32': return "Unknown version" - status, output = getstatusoutput("freetype-config --version") + status, version = getstatusoutput("freetype-config --version") if status == 0: - version = output - else: - version = None + try: + return ft_config.check_for_config( + 'freetype2', 'ft2build.h', self.get_extension(), + min_version='2.4', version=version + ) + except CheckFailed: + pass - return self._check_for_pkg_config( - 'freetype2', 'ft2build.h', - min_version='2.4', version=version) + return pkg_config.check_for_config( + 'freetype2', 'ft2build.h', self.get_extension(), + min_version='2.4', version=None) def add_flags(self, ext): - pkg_config.setup_extension( - ext, 'freetype2', - default_include_dirs=[ - 'freetype2', 'lib/freetype2/include', - 'lib/freetype2/include/freetype2'], - default_library_dirs=[ - 'freetype2/lib'], - default_libraries=['freetype', 'z'], - alt_exec='freetype-config') + if ft_config.has_config: + ft_config.setup_extension( + ext, 'freetype2', + default_include_dirs=[ + 'freetype2', + 'lib/freetype2/include', + 'lib/freetype2/include/freetype2', + ], + default_library_dirs=['freetype2/lib'], + default_libraries=['freetype', 'z']) + + else: + pkg_config.setup_extension( + ext, 'freetype2', + default_include_dirs=[ + 'freetype2', + 'lib/freetype2/include', + 'lib/freetype2/include/freetype2', + ], + default_library_dirs=['freetype2/lib'], + default_libraries=['freetype', 'z']) class FT2Font(SetupPackage): @@ -805,9 +881,7 @@ def get_extension(self): 'src/mplutils.cpp' ] ext = make_extension('matplotlib.ft2font', sources) - FreeType().add_flags(ext) - Numpy().add_flags(ext) - CXX().add_flags(ext) + self.add_dep_flags(ext) return ext @@ -816,8 +890,8 @@ class Png(SetupPackage): def check(self): try: - return self._check_for_pkg_config( - 'libpng', 'png.h', + return pkg_config.check_for_config( + 'libpng', 'png.h', self.get_extension(), min_version='1.2') except CheckFailed as e: self.__class__.found_external = False @@ -830,8 +904,7 @@ def get_extension(self): ext = make_extension('matplotlib._png', sources) pkg_config.setup_extension( ext, 'libpng', default_libraries=['png', 'z']) - Numpy().add_flags(ext) - CXX().add_flags(ext) + self.add_dep_flags(ext) return ext @@ -846,8 +919,7 @@ def get_extension(self): 'ttconv/ttutil.cpp' ] ext = make_extension('matplotlib.ttconv', sources) - Numpy().add_flags(ext) - CXX().add_flags(ext) + self.add_dep_flags(ext) return ext @@ -862,9 +934,7 @@ def get_extension(self): ] ext = make_extension('matplotlib._path', sources) - Numpy().add_flags(ext) - LibAgg().add_flags(ext) - CXX().add_flags(ext) + self.add_dep_flags(ext) return ext @@ -876,9 +946,7 @@ def get_extension(self): 'src/_image.cpp', 'src/mplutils.cpp' ] ext = make_extension('matplotlib._image', sources) - Numpy().add_flags(ext) - LibAgg().add_flags(ext) - CXX().add_flags(ext) + self.add_dep_flags(ext) return ext @@ -890,7 +958,7 @@ def get_extension(self): "src/cntr.c" ] ext = make_extension('matplotlib._cntr', sources) - Numpy().add_flags(ext) + self.add_dep_flags(ext) return ext @@ -905,7 +973,7 @@ def get_extension(self): "delaunay_utils.cpp", "natneighbors.cpp"] sources = [os.path.join('lib/matplotlib/delaunay', s) for s in sources] ext = make_extension('matplotlib._delaunay', sources) - Numpy().add_flags(ext) + self.add_dep_flags(ext) return ext @@ -918,8 +986,7 @@ def get_extension(self): "src/mplutils.cpp" ] ext = make_extension('matplotlib._tri', sources) - Numpy().add_flags(ext) - CXX().add_flags(ext) + self.add_dep_flags(ext) return ext @@ -1012,18 +1079,16 @@ def get_extension(self): "src/_backend_agg.cpp" ] ext = make_extension('matplotlib.backends._backend_agg', sources) - Numpy().add_flags(ext) - LibAgg().add_flags(ext) - FreeType().add_flags(ext) - CXX().add_flags(ext) + self.add_dep_flags(ext) return ext class BackendTkAgg(OptionalBackendPackage): name = "tkagg" - def __init__(self): + def __init__(self, **kw): self.tcl_tk_cache = None + OptionalBackendPackage.__init__(self, **kw) def check(self): if self.get_config() is False: @@ -1063,9 +1128,7 @@ def get_extension(self): ext = make_extension('matplotlib.backends._tkagg', sources) self.add_flags(ext) - Numpy().add_flags(ext) - LibAgg().add_flags(ext) - CXX().add_flags(ext) + self.add_dep_flags(ext) return ext def query_tcltk(self): @@ -1135,7 +1198,7 @@ def parse_tcl_config(self, tcl_lib_dir, tk_lib_dir): "/usr/lib/tcl" + str(Tkinter.TclVersion), "/usr/lib"] tk_poss = [tk_lib_dir, - os.path.normpath(os.path.join(tk_lib_dir, '..')), + os.path.normpath(os.path.join(tk_lib_dir, '..')), "/usr/lib/tk" + str(Tkinter.TkVersion), "/usr/lib"] for ptcl, ptk in zip(tcl_poss, tk_poss): @@ -1203,9 +1266,9 @@ def guess_tcl_config(self, tcl_lib_dir, tk_lib_dir, tk_ver): if not os.path.exists(tcl_inc): # this is a hack for suse linux, which is broken - if (sys.platform.startswith('linux') and - os.path.exists('/usr/include/tcl.h') and - os.path.exists('/usr/include/tk.h')): + if all([sys.platform.startswith('linux'), + os.path.exists('/usr/include/tcl.h'), + os.path.exists('/usr/include/tk.h')]): tcl_inc = '/usr/include' tk_inc = '/usr/include' @@ -1353,7 +1416,7 @@ def get_extension(self): ] ext = make_extension('matplotlib.backends._backend_gdk', sources) self.add_flags(ext) - Numpy().add_flags(ext) + self.add_dep_flags(ext) return ext def add_flags(self, ext): @@ -1407,9 +1470,9 @@ def getoutput(s): (flag.startswith('-l') or flag.startswith('-L'))]) # visual studio doesn't need the math library - if (sys.platform == 'win32' and - win32_compiler == 'msvc' and - 'm' in ext.libraries): + if all([sys.platform == 'win32', + win32_compiler == 'msvc', + 'm' in ext.libraries]): ext.libraries.remove('m') elif sys.platform != 'win32': @@ -1439,9 +1502,7 @@ def get_extension(self): ] ext = make_extension('matplotlib.backends._gtkagg', sources) self.add_flags(ext) - LibAgg().add_flags(ext) - CXX().add_flags(ext) - Numpy().add_flags(ext) + self.add_dep_flags(ext) return ext @@ -1616,9 +1677,7 @@ def get_extension(self): ] ext = make_extension('matplotlib.backends._macosx', sources) - Numpy().add_flags(ext) - LibAgg().add_flags(ext) - CXX().add_flags(ext) + self.add_dep_flags(ext) ext.extra_link_args.extend(['-framework', 'Cocoa']) return ext