diff --git a/CHANGELOG.md b/CHANGELOG.md index c116f900..072cdfa2 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,11 @@ All notable changes to this project will be documented in this file. +## [0.1x.x - 2023-0x-xx] + +### Fixed + +- When building from source, the installer additionally searches for `libheif` using `pkg-config`. #128 + ## [0.13.0 - 2023-08-09] ### Added diff --git a/setup.py b/setup.py index 80fce7d3..64893e19 100644 --- a/setup.py +++ b/setup.py @@ -1,11 +1,12 @@ #!/usr/bin/env python """Script to build wheel""" +import os +import re +import subprocess import sys -from os import environ, getenv, path from pathlib import Path from re import finditer from shutil import copy -from subprocess import check_output from typing import List from warnings import warn @@ -22,75 +23,144 @@ def get_version(): return locals()["__version__"] +def _cmd_exists(cmd: str) -> bool: + if "PATH" not in os.environ: + return False + return any(os.access(os.path.join(path, cmd), os.X_OK) for path in os.environ["PATH"].split(os.pathsep)) + + +def _pkg_config(name): + command = os.environ.get("PKG_CONFIG", "pkg-config") + for keep_system in (True, False): + try: + command_libs = [command, "--libs-only-L", name] + command_cflags = [command, "--cflags-only-I", name] + stderr = None + if keep_system: + command_libs.append("--keep-system-libs") + command_cflags.append("--keep-system-cflags") + stderr = subprocess.DEVNULL + # if not DEBUG: + # command_libs.append("--silence-errors") + # command_cflags.append("--silence-errors") + libs = re.split( + r"(^|\s+)-L", + subprocess.check_output(command_libs, stderr=stderr).decode("utf8").strip(), + )[ + ::2 + ][1:] + cflags = re.split( + r"(^|\s+)-I", + subprocess.check_output(command_cflags).decode("utf8").strip(), + )[ + ::2 + ][1:] + return libs, cflags + except Exception: # noqa # pylint: disable=broad-exception-caught + pass + return None + + class PillowHeifBuildExt(build_ext): """This class is based on the Pillow setup method""" def build_extensions(self): # pylint: disable=too-many-branches disable=too-many-statements - if getenv("PRE_COMMIT"): + if os.getenv("PRE_COMMIT"): return include_dirs = [] library_dirs = [] + pkg_config = None + if _cmd_exists(os.environ.get("PKG_CONFIG", "pkg-config")): + pkg_config = _pkg_config + + root = None + libheif_found = False + if pkg_config: + for lib_name in ("heif", "libheif"): + print(f"Looking for `{lib_name}` using pkg-config.") + root = pkg_config(lib_name) + if root: + print(f"Found `{lib_name}` using pkg-config: {root}") + libheif_found = True + break + + if isinstance(root, tuple): + lib_root, include_root = root + else: + lib_root = include_root = root + + if lib_root is not None: + if not isinstance(lib_root, (tuple, list)): + lib_root = (lib_root,) + for lib_dir in lib_root: + self._add_directory(library_dirs, lib_dir) + if include_root is not None: + if not isinstance(include_root, (tuple, list)): + include_root = (include_root,) + for include_dir in include_root: + self._add_directory(include_dirs, include_dir) + # respect CFLAGS/CPPFLAGS/LDFLAGS for k in ("CFLAGS", "CPPFLAGS", "LDFLAGS"): - if k in environ: - for match in finditer(r"-I([^\s]+)", environ[k]): + if k in os.environ: + for match in finditer(r"-I([^\s]+)", os.environ[k]): self._add_directory(include_dirs, match.group(1)) - for match in finditer(r"-L([^\s]+)", environ[k]): + for match in finditer(r"-L([^\s]+)", os.environ[k]): self._add_directory(library_dirs, match.group(1)) # include, rpath, if set as environment variables for k in ("C_INCLUDE_PATH", "CPATH", "INCLUDE"): - if k in environ: - for d in environ[k].split(path.pathsep): + if k in os.environ: + for d in os.environ[k].split(os.path.pathsep): self._add_directory(include_dirs, d) for k in ("LD_RUN_PATH", "LIBRARY_PATH", "LIB"): - if k in environ: - for d in environ[k].split(path.pathsep): + if k in os.environ: + for d in os.environ[k].split(os.path.pathsep): self._add_directory(library_dirs, d) - self._add_directory(include_dirs, path.join(sys.prefix, "include")) - self._add_directory(library_dirs, path.join(sys.prefix, "lib")) + self._add_directory(include_dirs, os.path.join(sys.prefix, "include")) + self._add_directory(library_dirs, os.path.join(sys.prefix, "lib")) if sys.platform.lower() == "win32": # Currently only MSYS2 is supported for Windows systems. Do a PR if you need support for anything else. - include_path_prefix = getenv("MSYS2_PREFIX") + include_path_prefix = os.getenv("MSYS2_PREFIX") if include_path_prefix is None: include_path_prefix = "C:\\msys64\\mingw64" warn(f"MSYS2_PREFIX environment variable is not set. Assuming `MSYS2_PREFIX={include_path_prefix}`") - if not path.isdir(include_path_prefix): + if not os.path.isdir(include_path_prefix): raise ValueError("MSYS2 not found and `MSYS2_PREFIX` is not set or is invalid.") - library_dir = path.join(include_path_prefix, "lib") + library_dir = os.path.join(include_path_prefix, "lib") # See comment a few lines below. We can't include MSYS2 directory before compiler directories :( # self._add_directory(include_dirs, path.join(include_path_prefix, "include")) self._add_directory(library_dirs, library_dir) - lib_export_file = Path(path.join(library_dir, "libheif.dll.a")) + lib_export_file = Path(os.path.join(library_dir, "libheif.dll.a")) if lib_export_file.is_file(): - copy(lib_export_file, path.join(library_dir, "libheif.lib")) + copy(lib_export_file, os.path.join(library_dir, "libheif.lib")) else: warn("If you build this with MSYS2, you should not see this warning.") - # on Windows, we include root of project instead of MSYS2 directory. - # Including MSYS2 directory leads to compilation errors, theirs `stdio.h` and others files are different. - # ATTENTION: If someone know how without hacks include MSYS2 directory as last directory in list - help! - self.compiler.include_dirs.append(path.dirname(path.abspath(__file__))) + # on Windows, we include "root" of the project instead of MSYS2 directory. + # Including MSYS2 directory leads to compilation errors, theirs `stdio.h` and other files are different. + # ATTENTION: If someone knows how without hacks include MSYS2 directory as last directory in list - help! + self.compiler.include_dirs.append(os.path.dirname(os.path.abspath(__file__))) self._update_extension( "_pillow_heif", ["libheif"], extra_compile_args=["/d2FH4-", "/WX"], extra_link_args=["/WX"] ) elif sys.platform.lower() == "darwin": try: # if Homebrew is installed, use its lib and include directories - homebrew_prefix = check_output(["brew", "--prefix"]).strip().decode("latin1") + homebrew_prefix = subprocess.check_output(["brew", "--prefix"]).strip().decode("latin1") except Exception: # noqa # pylint: disable=broad-except - homebrew_prefix = None # Homebrew not installed + homebrew_prefix = None # Homebrew isn't installed if homebrew_prefix: - # add Homebrew's include and lib directories - self._add_directory(library_dirs, path.join(homebrew_prefix, "lib")) - self._add_directory(include_dirs, path.join(homebrew_prefix, "include")) + # add Homebrew's "include" and "lib" directories + self._add_directory(library_dirs, os.path.join(homebrew_prefix, "lib")) + self._add_directory(include_dirs, os.path.join(homebrew_prefix, "include")) # fink installation directories self._add_directory(library_dirs, "/sw/lib") @@ -109,9 +179,10 @@ def build_extensions(self): # pylint: disable=too-many-branches disable=too-man self._add_directory(library_dirs, "/usr/lib") self._add_directory(library_dirs, "/lib") - include_path_prefix = linux_build_libs.build_libs() # this need a rework in the future - self._add_directory(library_dirs, path.join(include_path_prefix, "lib")) - self._add_directory(include_dirs, path.join(include_path_prefix, "include")) + if not libheif_found: + include_path_prefix = linux_build_libs.build_libs() # this needs a rework in the future + self._add_directory(library_dirs, os.path.join(include_path_prefix, "lib")) + self._add_directory(include_dirs, os.path.join(include_path_prefix, "include")) self._update_extension("_pillow_heif", ["heif"], extra_compile_args=["-Ofast", "-Werror"]) @@ -132,12 +203,12 @@ def _update_extension(self, name, libraries, extra_compile_args=None, extra_link @staticmethod def _add_directory(paths: List, subdir): if subdir: - subdir = path.realpath(subdir) - if path.isdir(subdir) and subdir not in paths: + subdir = os.path.realpath(subdir) + if os.path.isdir(subdir) and subdir not in paths: paths.append(subdir) -if getenv("READTHEDOCS", "False") == "True": +if os.getenv("READTHEDOCS", "False") == "True": setup(version=get_version()) else: setup(