diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index a24b964b..2e335f7c 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -8,7 +8,7 @@ jobs: fail-fast: false matrix: os: [ubuntu-20.04, macos-11, windows-2019] - python-version: ["3.8", "3.9", "3.10", "3.11"] + python-version: ["3.8", "3.9", "3.10", "3.11", "3.12"] runs-on: ${{ matrix.os }} steps: - uses: actions/checkout@v3 @@ -21,7 +21,7 @@ jobs: set -x && export DISTUTILS_DEBUG=1 && - python -mpip install --upgrade pip setuptools && + python -mpip install --upgrade pip && case "$(python -c 'import sys; print(sys.platform)')" in linux) diff --git a/src/_feature_tests.cpp b/ext/_feature_tests.cpp similarity index 100% rename from src/_feature_tests.cpp rename to ext/_feature_tests.cpp diff --git a/src/_macros.h b/ext/_macros.h similarity index 100% rename from src/_macros.h rename to ext/_macros.h diff --git a/src/_mplcairo.cpp b/ext/_mplcairo.cpp similarity index 100% rename from src/_mplcairo.cpp rename to ext/_mplcairo.cpp diff --git a/src/_mplcairo.h b/ext/_mplcairo.h similarity index 100% rename from src/_mplcairo.h rename to ext/_mplcairo.h diff --git a/src/_os.cpp b/ext/_os.cpp similarity index 100% rename from src/_os.cpp rename to ext/_os.cpp diff --git a/src/_os.h b/ext/_os.h similarity index 100% rename from src/_os.h rename to ext/_os.h diff --git a/src/_pattern_cache.cpp b/ext/_pattern_cache.cpp similarity index 100% rename from src/_pattern_cache.cpp rename to ext/_pattern_cache.cpp diff --git a/src/_pattern_cache.h b/ext/_pattern_cache.h similarity index 100% rename from src/_pattern_cache.h rename to ext/_pattern_cache.h diff --git a/src/_raqm.cpp b/ext/_raqm.cpp similarity index 100% rename from src/_raqm.cpp rename to ext/_raqm.cpp diff --git a/src/_raqm.h b/ext/_raqm.h similarity index 100% rename from src/_raqm.h rename to ext/_raqm.h diff --git a/src/_unity_build.cpp b/ext/_unity_build.cpp similarity index 100% rename from src/_unity_build.cpp rename to ext/_unity_build.cpp diff --git a/src/_util.cpp b/ext/_util.cpp similarity index 100% rename from src/_util.cpp rename to ext/_util.cpp diff --git a/src/_util.h b/ext/_util.h similarity index 100% rename from src/_util.h rename to ext/_util.h diff --git a/lib/mplcairo/.gitignore b/lib/mplcairo/.gitignore deleted file mode 100644 index b39f7941..00000000 --- a/lib/mplcairo/.gitignore +++ /dev/null @@ -1,2 +0,0 @@ -*.dll -_version.py diff --git a/pyproject.toml b/pyproject.toml new file mode 100644 index 00000000..53e8dfbd --- /dev/null +++ b/pyproject.toml @@ -0,0 +1,44 @@ +[build-system] +requires = [ + "setuptools>=62", + "setuptools_scm[toml]>=6.2", + "pybind11>=2.8.0", + "pycairo>=1.16.0; os_name == 'posix'", # Removed for manylinux build. +] +build-backend = "setuptools.build_meta" + +[project] +name = "mplcairo" +description = "A (new) cairo backend for Matplotlib." +readme = "README.rst" +authors = [{name = "Antony Lee"}] +license = {text = "MIT"} +classifiers = [ + "Development Status :: 4 - Beta", + "Framework :: Matplotlib", + "License :: OSI Approved :: MIT License", + "Programming Language :: Python :: 3", +] +requires-python = ">=3.8" +dependencies = [ + "matplotlib>=2.2", + "pillow", # Already a dependency of mpl>=3.3. + "pycairo>=1.16.0; os_name == 'posix'", +] +dynamic = ["version"] + +[tool.setuptools_scm] +version_scheme = "post-release" +local_scheme = "node-and-date" +fallback_version = "0+unknown" + +[tool.coverage.run] +branch = true +source_pkgs = ["mplcairo"] + +[tool.pytest.ini_options] +filterwarnings = [ + "error", + "ignore::DeprecationWarning", + "error::DeprecationWarning:mplcairo", +] diff --git a/setup.py b/setup.py index 08c2572e..4bcac7f0 100644 --- a/setup.py +++ b/setup.py @@ -5,7 +5,7 @@ Environment variables: MPLCAIRO_MANYLINUX - If set, build a manylinux wheel: pycairo is not declared as setup_requires. + If set, build a manylinux wheel: pycairo is not a build requirement. MPLCAIRO_NO_UNITY_BUILD If set, compile the various cpp files separately, instead of as a single @@ -23,22 +23,21 @@ import subprocess from subprocess import CalledProcessError import sys +from tempfile import TemporaryDirectory +import tokenize import urllib.request -if sys.platform == "darwin": - os.environ.setdefault("CC", "clang") - # Funnily enough, distutils uses $CC to compile c++ extensions but - # $CXX to *link* such extensions... (Moreover, it does some funky - # changes to $CXX if either $CC or $CXX has multiple words -- see e.g. - # https://bugs.python.org/issue6863.) - os.environ.setdefault("CXX", "clang") - -from setupext import Extension, build_ext, find_packages, setup +import setuptools +from setuptools import Distribution +from pybind11.setup_helpers import Pybind11Extension +if os.environ.get("MPLCAIRO_MANYLINUX", ""): + cairo = None +else: + import cairo MIN_CAIRO_VERSION = "1.13.1" # Also in _feature_tests.cpp. MIN_RAQM_VERSION = "0.7.0" -MANYLINUX = bool(os.environ.get("MPLCAIRO_MANYLINUX", "")) UNITY_BUILD = not bool(os.environ.get("MPLCAIRO_NO_UNITY_BUILD")) @@ -47,6 +46,63 @@ def get_pkgconfig(*args): universal_newlines=True)) +def gen_extension(tmpdir): + ext = Pybind11Extension( + "mplcairo._mplcairo", + sources=( + ["ext/_unity_build.cpp"] if UNITY_BUILD else + sorted({*map(str, Path("ext").glob("*.cpp"))} + - {"ext/_unity_build.cpp"})), + depends=[ + "setup.py", + *map(str, Path("ext").glob("*.h")), + *map(str, Path("ext").glob("*.cpp")), + ], + cxx_std=17, + include_dirs=[cairo.get_include()] if cairo else [], + ) + + # NOTE: Versions <= 8.2 of Arch Linux's python-pillow package included + # *into a non-overridable distutils header directory* a ``raqm.h`` that + # is both invalid (https://bugs.archlinux.org/task/57492) and outdated + # (missing a declaration for `raqm_version_string`). It is thus not + # possible to build mplcairo with such an old distro package installed. + try: + get_pkgconfig(f"raqm >= {MIN_RAQM_VERSION}") + except (FileNotFoundError, CalledProcessError): + tmpdir.mkdir(parents=True, exist_ok=True) + (tmpdir / "raqm-version.h").write_text("") # Touch it. + with urllib.request.urlopen( + f"https://raw.githubusercontent.com/HOST-Oman/libraqm/" + f"v{MIN_RAQM_VERSION}/ext/raqm.h") as request, \ + (tmpdir / "raqm.h").open("wb") as file: + file.write(request.read()) + ext.include_dirs += [tmpdir] + else: + ext.extra_compile_args += get_pkgconfig("--cflags", "raqm") + + if os.name == "posix": + get_pkgconfig(f"cairo >= {MIN_CAIRO_VERSION}") + ext.extra_compile_args += [ + "-flto", "-Wall", "-Wextra", "-Wpedantic", + *get_pkgconfig("--cflags", "cairo"), + ] + ext.extra_link_args += ["-flto"] + + elif os.name == "nt": + # Windows conda path for FreeType. + ext.include_dirs += [Path(sys.prefix, "Library/include")] + ext.extra_compile_args += [ + "/experimental:preprocessor", + "/wd4244", "/wd4267", # cf. gcc -Wconversion. + ] + ext.libraries += ["psapi", "cairo", "freetype"] + # Windows conda path for FreeType -- needs to be str, not Path. + ext.library_dirs += [str(Path(sys.prefix, "Library/lib"))] + + return ext + + @functools.lru_cache(1) def paths_from_link_libpaths(): # "Easy" way to call CommandLineToArgvW... @@ -61,69 +117,7 @@ def paths_from_link_libpaths(): return paths -class build_ext(build_ext): - - def finalize_options(self): - import cairo - from pybind11.setup_helpers import Pybind11Extension - - self.distribution.ext_modules[:] = ext, = [Pybind11Extension( - "mplcairo._mplcairo", - sources=( - ["src/_unity_build.cpp"] if UNITY_BUILD else - sorted({*map(str, Path("src").glob("*.cpp"))} - - {"src/_unity_build.cpp"})), - depends=[ - "setup.py", - *map(str, Path("src").glob("*.h")), - *map(str, Path("src").glob("*.cpp")), - ], - cxx_std=17, - include_dirs=[cairo.get_include()], - )] - - # NOTE: Versions <= 8.2 of Arch Linux's python-pillow package included - # *into a non-overridable distutils header directory* a ``raqm.h`` that - # is both invalid (https://bugs.archlinux.org/task/57492) and outdated - # (missing a declaration for `raqm_version_string`). It is thus not - # possible to build mplcairo with such an old distro package installed. - try: - get_pkgconfig(f"raqm >= {MIN_RAQM_VERSION}") - except (FileNotFoundError, CalledProcessError): - tmp_include_dir = Path( - self.get_finalized_command("build").build_base, "include") - tmp_include_dir.mkdir(parents=True, exist_ok=True) - (tmp_include_dir / "raqm-version.h").write_text("") # Touch it. - with urllib.request.urlopen( - f"https://raw.githubusercontent.com/HOST-Oman/libraqm/" - f"v{MIN_RAQM_VERSION}/src/raqm.h") as request, \ - (tmp_include_dir / "raqm.h").open("wb") as file: - file.write(request.read()) - ext.include_dirs += [tmp_include_dir] - else: - ext.extra_compile_args += get_pkgconfig("--cflags", "raqm") - - if os.name == "posix": - get_pkgconfig(f"cairo >= {MIN_CAIRO_VERSION}") - ext.extra_compile_args += [ - "-flto", "-Wall", "-Wextra", "-Wpedantic", - *get_pkgconfig("--cflags", "cairo"), - ] - ext.extra_link_args += ["-flto"] - - elif os.name == "nt": - # Windows conda path for FreeType. - ext.include_dirs += [Path(sys.prefix, "Library/include")] - ext.extra_compile_args += [ - "/experimental:preprocessor", - "/wd4244", "/wd4267", # cf. gcc -Wconversion. - ] - ext.libraries += ["psapi", "cairo", "freetype"] - # Windows conda path for FreeType -- needs to be str, not Path. - ext.library_dirs += [str(Path(sys.prefix, "Library/lib"))] - - super().finalize_options() - +class build_ext(setuptools.command.build_ext.build_ext): def _copy_dlls_to(self, dest): if os.name == "nt": for dll in ["cairo.dll", "freetype.dll"]: @@ -142,41 +136,41 @@ def copy_extensions_to_source(self): self.get_finalized_command("build_py").get_package_dir("mplcairo")) -setup.register_pth_hook("setup_mplcairo_pth.py", "mplcairo.pth") - - -setup( - name="mplcairo", - description="A (new) cairo backend for Matplotlib.", - long_description=open("README.rst", encoding="utf-8").read(), - author="Antony Lee", - url="https://github.com/matplotlib/mplcairo", - license="MIT", - classifiers=[ - "Development Status :: 4 - Beta", - "Framework :: Matplotlib", - "License :: OSI Approved :: MIT License", - "Programming Language :: Python :: 3", - ], - cmdclass={"build_ext": build_ext}, - packages=find_packages("lib"), - package_dir={"": "lib"}, - ext_modules=[Extension("", [])], - python_requires=">=3.8", - setup_requires=[ - "setuptools>=36.7", # setup_requires early install. - "setuptools_scm", - "pybind11>=2.8.0", - *(["pycairo>=1.16.0; os_name == 'posix'"] if not MANYLINUX else []), - ], - use_scm_version={ # xref __init__.py - "version_scheme": "post-release", - "local_scheme": "node-and-date", - "write_to": "lib/mplcairo/_version.py", - }, - install_requires=[ - "matplotlib>=2.2", - "pillow", # Already a dependency of mpl>=3.3. - "pycairo>=1.16.0; os_name == 'posix'", - ], -) +def register_pth_hook(source_path, pth_name): + """ + :: + setup.register_pth_hook("hook_source.py", "hook_name.pth") # Add hook. + """ + with tokenize.open(source_path) as file: + source = file.read() + _pth_hook_mixin._pth_hooks.append((pth_name, source)) + + +class _pth_hook_mixin: + _pth_hooks = [] + + def run(self): + super().run() + for pth_name, source in self._pth_hooks: + with Path(self.install_dir, pth_name).open("w") as file: + file.write(f"import os; exec({source!r})") + + def get_outputs(self): + return (super().get_outputs() + + [str(Path(self.install_dir, pth_name)) + for pth_name, _ in self._pth_hooks]) + + +def setup(**kwargs): + cmdclass = kwargs.setdefault("cmdclass", {}) + get = Distribution({"cmdclass": cmdclass}).get_command_class + cmdclass["develop"] = type( + "develop_with_pth_hook", (_pth_hook_mixin, get("develop")), {}) + cmdclass["install_lib"] = type( + "install_lib_with_pth_hook", (_pth_hook_mixin, get("install_lib")), {}) + setuptools.setup(**kwargs) + + +register_pth_hook("setup_mplcairo_pth.py", "mplcairo.pth") +with TemporaryDirectory() as tmpdir: + setup(ext_modules=[gen_extension(tmpdir=Path(tmpdir))]) diff --git a/setupext.py b/setupext.py deleted file mode 100644 index 0650d441..00000000 --- a/setupext.py +++ /dev/null @@ -1,60 +0,0 @@ -from pathlib import Path -import tokenize - -import setuptools -from setuptools import Distribution, Extension, find_packages -from setuptools.command.build_ext import build_ext - - -__all__ = ["Extension", "build_ext", "find_packages", "setup"] - - -class build_ext(build_ext): - def build_extensions(self): - try: - self.compiler.compiler_so.remove("-Wstrict-prototypes") - except (AttributeError, ValueError): - pass - super().build_extensions() - - -def register_pth_hook(source_path, pth_name): - """ - :: - setup.register_pth_hook("hook_source.py", "hook_name.pth") # Add hook. - """ - with tokenize.open(source_path) as file: - source = file.read() - _pth_hook_mixin._pth_hooks.append((pth_name, source)) - - -class _pth_hook_mixin: - _pth_hooks = [] - - def run(self): - super().run() - for pth_name, source in self._pth_hooks: - with Path(self.install_dir, pth_name).open("w") as file: - file.write(f"import os; exec({source!r})") - - def get_outputs(self): - return (super().get_outputs() - + [str(Path(self.install_dir, pth_name)) - for pth_name, _ in self._pth_hooks]) - - -def _prepare_pth_hook(kwargs): - cmdclass = kwargs.setdefault("cmdclass", {}) - get = Distribution({"cmdclass": cmdclass}).get_command_class - cmdclass["develop"] = type( - "develop_with_pth_hook", (_pth_hook_mixin, get("develop")), {}) - cmdclass["install_lib"] = type( - "install_lib_with_pth_hook", (_pth_hook_mixin, get("install_lib")), {}) - - -def setup(**kwargs): - _prepare_pth_hook(kwargs) - setuptools.setup(**kwargs) - - -setup.register_pth_hook = register_pth_hook diff --git a/src/mplcairo/.gitignore b/src/mplcairo/.gitignore new file mode 100644 index 00000000..6a746131 --- /dev/null +++ b/src/mplcairo/.gitignore @@ -0,0 +1 @@ +*.dll diff --git a/lib/mplcairo/__init__.py b/src/mplcairo/__init__.py similarity index 100% rename from lib/mplcairo/__init__.py rename to src/mplcairo/__init__.py diff --git a/lib/mplcairo/_backports.py b/src/mplcairo/_backports.py similarity index 100% rename from lib/mplcairo/_backports.py rename to src/mplcairo/_backports.py diff --git a/lib/mplcairo/_util.py b/src/mplcairo/_util.py similarity index 100% rename from lib/mplcairo/_util.py rename to src/mplcairo/_util.py diff --git a/src/mplcairo/_version.py b/src/mplcairo/_version.py new file mode 100644 index 00000000..691ec840 --- /dev/null +++ b/src/mplcairo/_version.py @@ -0,0 +1,16 @@ +# file generated by setuptools_scm +# don't change, don't track in version control +TYPE_CHECKING = False +if TYPE_CHECKING: + from typing import Tuple, Union + VERSION_TUPLE = Tuple[Union[int, str], ...] +else: + VERSION_TUPLE = object + +version: str +__version__: str +__version_tuple__: VERSION_TUPLE +version_tuple: VERSION_TUPLE + +__version__ = version = '0.5.post32+ge771c74' +__version_tuple__ = version_tuple = (0, 5, 'ge771c74') diff --git a/lib/mplcairo/base.py b/src/mplcairo/base.py similarity index 100% rename from lib/mplcairo/base.py rename to src/mplcairo/base.py diff --git a/lib/mplcairo/gtk.py b/src/mplcairo/gtk.py similarity index 100% rename from lib/mplcairo/gtk.py rename to src/mplcairo/gtk.py diff --git a/lib/mplcairo/gtk_native.py b/src/mplcairo/gtk_native.py similarity index 100% rename from lib/mplcairo/gtk_native.py rename to src/mplcairo/gtk_native.py diff --git a/lib/mplcairo/macosx.py b/src/mplcairo/macosx.py similarity index 100% rename from lib/mplcairo/macosx.py rename to src/mplcairo/macosx.py diff --git a/lib/mplcairo/multipage.py b/src/mplcairo/multipage.py similarity index 100% rename from lib/mplcairo/multipage.py rename to src/mplcairo/multipage.py diff --git a/lib/mplcairo/qt.py b/src/mplcairo/qt.py similarity index 100% rename from lib/mplcairo/qt.py rename to src/mplcairo/qt.py diff --git a/lib/mplcairo/tk.py b/src/mplcairo/tk.py similarity index 100% rename from lib/mplcairo/tk.py rename to src/mplcairo/tk.py diff --git a/lib/mplcairo/wx.py b/src/mplcairo/wx.py similarity index 100% rename from lib/mplcairo/wx.py rename to src/mplcairo/wx.py diff --git a/tools/build-macos-wheel.sh b/tools/build-macos-wheel.sh index 8667f53c..ac8f9f22 100755 --- a/tools/build-macos-wheel.sh +++ b/tools/build-macos-wheel.sh @@ -18,9 +18,9 @@ python -mvenv "$tmpenv" ( source "$tmpenv/bin/activate" - python -mpip install --upgrade pip setuptools wheel delocate + python -mpip install --upgrade pip build delocate setuptools_scm - cd "$toplevel" - python setup.py bdist_wheel - delocate-wheel -v dist/* + cd "$tmpdir/mplcairo" + python -mbuild + python -mdelocate.cmd.delocate_wheel -v dist/* ) diff --git a/tools/build-manylinux-wheel.sh b/tools/build-manylinux-wheel.sh index 7fcc93ac..af6db211 100755 --- a/tools/build-manylinux-wheel.sh +++ b/tools/build-manylinux-wheel.sh @@ -14,17 +14,20 @@ if [[ "$MPLCAIRO_MANYLINUX" != 1 ]]; then tmpdir="$(mktemp -d)" trap 'rm -rf "$tmpdir"' EXIT INT TERM git clone "$toplevel" "$tmpdir/mplcairo" + python -mvenv "$tmpdir/tmpenv" + "$tmpdir/tmpenv/bin/python" -mpip install setuptools_scm + mplcairo_version="$("$tmpdir/tmpenv/bin/python" -msetuptools_scm)" + sed -i '/Removed for manylinux build/d' "$tmpdir/mplcairo/pyproject.toml" relpath="$( python -c 'import os, sys; print(os.path.relpath(*map(os.path.realpath, sys.argv[1:])))' \ "$0" "$toplevel")" ${DOCKER:-docker} run \ - -e MPLCAIRO_MANYLINUX=1 -e PY_VERS="${PY_VERS:-3.7 3.8 3.9 3.10 3.11}" \ + -e MPLCAIRO_MANYLINUX=1 -e PY_VERS="${PY_VERS:-3.8 3.9 3.10 3.11 3.12}" \ + -e mplcairo_version="$mplcairo_version" \ --volume "$tmpdir/mplcairo":/io/mplcairo:Z \ quay.io/pypa/manylinux2014_x86_64 \ "/io/mplcairo/$relpath" - user="${SUDO_USER:-$USER}" - chown "$user:$(id -gn "$user")" -R "$tmpdir/mplcairo/build" mkdir -p "$toplevel/dist" mv "$tmpdir/mplcairo/dist/"*-manylinux*.whl "$toplevel/dist" @@ -63,16 +66,14 @@ else py_prefix=("/opt/python/cp${py_ver/./}-"*) tags="$(basename "$py_prefix")" echo "Building the wheel for Python $py_ver." - # Shim access to pycairo's header. - echo 'def get_include(): return "/dev/null"' \ - >"$py_prefix/lib/python$py_ver/site-packages/cairo.py" ( cd /io/mplcairo + "$py_prefix/bin/python" -mpip install build # Force a rebuild of the extension. - CFLAGS="-static-libgcc -static-libstdc++ -I/usr/include/cairo -I/usr/include/freetype2" \ + SETUPTOOLS_SCM_PRETEND_VERSION_FOR_MPLCAIRO="$mplcairo_version" \ + CFLAGS="-static-libgcc -static-libstdc++ -I/usr/include/cairo -I/usr/include/freetype2" \ LDFLAGS="-static-libgcc -static-libstdc++" \ - "$py_prefix/bin/python" setup.py bdist_wheel - mplcairo_version="$("$py_prefix/bin/python" setup.py --version)" + "$py_prefix/bin/python" -mbuild for wheel in "dist/mplcairo-$mplcairo_version-$tags-"*".whl"; do auditwheel -v repair -wdist "$wheel" rm "$wheel"