# > pyroma .
# ------------------------------
# Checking .
# Found Pillow
# ------------------------------
# Final rating: 10/10
# Your cheese is so fresh most people think it's a cream: Mascarpone
# ------------------------------
from __future__ import annotations

import os
import re
import shutil
import struct
import subprocess
import sys
import warnings
from collections.abc import Iterator
from typing import Any

from setuptools import Extension, setup
from setuptools.command.build_ext import build_ext


def get_version() -> str:
    version_file = "src/PIL/_version.py"
    with open(version_file, encoding="utf-8") as f:
        return f.read().split('"')[1]


configuration: dict[str, list[str]] = {}


PILLOW_VERSION = get_version()
FREETYPE_ROOT = None
HARFBUZZ_ROOT = None
FRIBIDI_ROOT = None
IMAGEQUANT_ROOT = None
JPEG2K_ROOT = None
JPEG_ROOT = None
LCMS_ROOT = None
RAQM_ROOT = None
TIFF_ROOT = None
WEBP_ROOT = None
ZLIB_ROOT = None
FUZZING_BUILD = "LIB_FUZZING_ENGINE" in os.environ

if sys.platform == "win32" and sys.version_info >= (3, 14):
    import atexit

    atexit.register(
        lambda: warnings.warn(
            f"Pillow {PILLOW_VERSION} does not support Python "
            f"{sys.version_info.major}.{sys.version_info.minor} and does not provide "
            "prebuilt Windows binaries. We do not recommend building from source on "
            "Windows.",
            RuntimeWarning,
        )
    )


_IMAGING = ("decode", "encode", "map", "display", "outline", "path")

_LIB_IMAGING = (
    "Access",
    "AlphaComposite",
    "Resample",
    "Reduce",
    "Bands",
    "BcnDecode",
    "BitDecode",
    "Blend",
    "Chops",
    "ColorLUT",
    "Convert",
    "ConvertYCbCr",
    "Copy",
    "Crop",
    "Dib",
    "Draw",
    "Effects",
    "EpsEncode",
    "File",
    "Fill",
    "Filter",
    "FliDecode",
    "Geometry",
    "GetBBox",
    "GifDecode",
    "GifEncode",
    "HexDecode",
    "Histo",
    "JpegDecode",
    "JpegEncode",
    "Matrix",
    "ModeFilter",
    "Negative",
    "Offset",
    "Pack",
    "PackDecode",
    "Palette",
    "Paste",
    "Quant",
    "QuantOctree",
    "QuantHash",
    "QuantHeap",
    "PcdDecode",
    "PcxDecode",
    "PcxEncode",
    "Point",
    "RankFilter",
    "RawDecode",
    "RawEncode",
    "Storage",
    "SgiRleDecode",
    "SunRleDecode",
    "TgaRleDecode",
    "TgaRleEncode",
    "Unpack",
    "UnpackYCC",
    "UnsharpMask",
    "XbmDecode",
    "XbmEncode",
    "ZipDecode",
    "ZipEncode",
    "TiffDecode",
    "Jpeg2KDecode",
    "Jpeg2KEncode",
    "BoxBlur",
    "QuantPngQuant",
    "codec_fd",
)

DEBUG = False


class DependencyException(Exception):
    pass


class RequiredDependencyException(Exception):
    pass


PLATFORM_MINGW = os.name == "nt" and "GCC" in sys.version


def _dbg(s: str, tp: Any = None) -> None:
    if DEBUG:
        if tp:
            print(s % tp)
            return
        print(s)


def _find_library_dirs_ldconfig() -> list[str]:
    # Based on ctypes.util from Python 2

    ldconfig = "ldconfig" if shutil.which("ldconfig") else "/sbin/ldconfig"
    args: list[str]
    env: dict[str, str]
    expr: str
    if sys.platform.startswith("linux") or sys.platform.startswith("gnu"):
        if struct.calcsize("l") == 4:
            machine = os.uname()[4] + "-32"
        else:
            machine = os.uname()[4] + "-64"
        mach_map = {
            "x86_64-64": "libc6,x86-64",
            "ppc64-64": "libc6,64bit",
            "sparc64-64": "libc6,64bit",
            "s390x-64": "libc6,64bit",
            "ia64-64": "libc6,IA-64",
        }
        abi_type = mach_map.get(machine, "libc6")

        # Assuming GLIBC's ldconfig (with option -p)
        # Alpine Linux uses musl that can't print cache
        args = [ldconfig, "-p"]
        expr = rf".*\({abi_type}.*\) => (.*)"
        env = dict(os.environ)
        env["LC_ALL"] = "C"
        env["LANG"] = "C"

    elif sys.platform.startswith("freebsd"):
        args = [ldconfig, "-r"]
        expr = r".* => (.*)"
        env = {}

    try:
        p = subprocess.Popen(
            args, stderr=subprocess.DEVNULL, stdout=subprocess.PIPE, env=env, text=True
        )
    except OSError:  # E.g. command not found
        return []
    data = p.communicate()[0]

    dirs = []
    for dll in re.findall(expr, data):
        dir = os.path.dirname(dll)
        if dir not in dirs:
            dirs.append(dir)
    return dirs


def _add_directory(
    path: list[str], subdir: str | None, where: int | None = None
) -> None:
    if subdir is None:
        return
    subdir = os.path.realpath(subdir)
    if os.path.isdir(subdir) and subdir not in path:
        if where is None:
            _dbg("Appending path %s", subdir)
            path.append(subdir)
        else:
            _dbg("Inserting path %s", subdir)
            path.insert(where, subdir)
    elif subdir in path and where is not None:
        path.remove(subdir)
        path.insert(where, subdir)


def _find_include_file(self: pil_build_ext, include: str) -> int:
    for directory in self.compiler.include_dirs:
        _dbg("Checking for include file %s in %s", (include, directory))
        if os.path.isfile(os.path.join(directory, include)):
            _dbg("Found %s", include)
            return 1
    return 0


def _find_library_file(self: pil_build_ext, library: str) -> str | None:
    ret = self.compiler.find_library_file(self.compiler.library_dirs, library)
    if ret:
        _dbg("Found library %s at %s", (library, ret))
    else:
        _dbg("Couldn't find library %s in %s", (library, self.compiler.library_dirs))
    return ret


def _find_include_dir(self: pil_build_ext, dirname: str, include: str) -> bool | str:
    for directory in self.compiler.include_dirs:
        _dbg("Checking for include file %s in %s", (include, directory))
        if os.path.isfile(os.path.join(directory, include)):
            _dbg("Found %s in %s", (include, directory))
            return True
        subdir = os.path.join(directory, dirname)
        _dbg("Checking for include file %s in %s", (include, subdir))
        if os.path.isfile(os.path.join(subdir, include)):
            _dbg("Found %s in %s", (include, subdir))
            return subdir
    return False


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: str) -> tuple[list[str], list[str]] | None:
    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:
            pass
    return None


class pil_build_ext(build_ext):
    class ext_feature:
        features = [
            "zlib",
            "jpeg",
            "tiff",
            "freetype",
            "raqm",
            "lcms",
            "webp",
            "jpeg2000",
            "imagequant",
            "xcb",
        ]

        required = {"jpeg", "zlib"}
        vendor: set[str] = set()

        def __init__(self) -> None:
            self._settings: dict[str, str | bool | None] = {}
            for f in self.features:
                self.set(f, None)

        def require(self, feat: str) -> bool:
            return feat in self.required

        def get(self, feat: str) -> str | bool | None:
            return self._settings[feat]

        def set(self, feat: str, value: str | bool | None) -> None:
            self._settings[feat] = value

        def want(self, feat: str) -> bool:
            return self._settings[feat] is None

        def want_vendor(self, feat: str) -> bool:
            return feat in self.vendor

        def __iter__(self) -> Iterator[str]:
            yield from self.features

    feature = ext_feature()

    user_options = (
        build_ext.user_options
        + [(f"disable-{x}", None, f"Disable support for {x}") for x in feature]
        + [(f"enable-{x}", None, f"Enable support for {x}") for x in feature]
        + [
            (f"vendor-{x}", None, f"Use vendored version of {x}")
            for x in ("raqm", "fribidi")
        ]
        + [
            ("disable-platform-guessing", None, "Disable platform guessing"),
            ("debug", None, "Debug logging"),
        ]
        + [("add-imaging-libs=", None, "Add libs to _imaging build")]
    )

    @staticmethod
    def check_configuration(option: str, value: str) -> bool | None:
        return True if value in configuration.get(option, []) else None

    def initialize_options(self) -> None:
        self.disable_platform_guessing = self.check_configuration(
            "platform-guessing", "disable"
        )
        self.add_imaging_libs = ""
        build_ext.initialize_options(self)
        for x in self.feature:
            setattr(self, f"disable_{x}", self.check_configuration(x, "disable"))
            setattr(self, f"enable_{x}", self.check_configuration(x, "enable"))
        for x in ("raqm", "fribidi"):
            setattr(self, f"vendor_{x}", self.check_configuration(x, "vendor"))
        if self.check_configuration("debug", "true"):
            self.debug = True
        self.parallel = configuration.get("parallel", [None])[-1]

    def finalize_options(self) -> None:
        build_ext.finalize_options(self)
        if self.debug:
            global DEBUG
            DEBUG = True
        if not self.parallel:
            # If --parallel (or -j) wasn't specified, we want to reproduce the same
            # behavior as before, that is, auto-detect the number of jobs.
            self.parallel = None

            cpu_count = os.cpu_count()
            if cpu_count is not None:
                try:
                    self.parallel = int(
                        os.environ.get("MAX_CONCURRENCY", min(4, cpu_count))
                    )
                except TypeError:
                    pass
        for x in self.feature:
            if getattr(self, f"disable_{x}"):
                self.feature.set(x, False)
                self.feature.required.discard(x)
                _dbg("Disabling %s", x)
                if getattr(self, f"enable_{x}"):
                    msg = f"Conflicting options: '-C {x}=enable' and '-C {x}=disable'"
                    raise ValueError(msg)
                if x == "freetype":
                    _dbg("'-C freetype=disable' implies '-C raqm=disable'")
                    if getattr(self, "enable_raqm"):
                        msg = (
                            "Conflicting options: "
                            "'-C raqm=enable' and '-C freetype=disable'"
                        )
                        raise ValueError(msg)
                    setattr(self, "disable_raqm", True)
            if getattr(self, f"enable_{x}"):
                _dbg("Requiring %s", x)
                self.feature.required.add(x)
                if x == "raqm":
                    _dbg("'-C raqm=enable' implies '-C freetype=enable'")
                    self.feature.required.add("freetype")
        for x in ("raqm", "fribidi"):
            if getattr(self, f"vendor_{x}"):
                if getattr(self, "disable_raqm"):
                    msg = f"Conflicting options: '-C {x}=vendor' and '-C raqm=disable'"
                    raise ValueError(msg)
                if x == "fribidi" and not getattr(self, "vendor_raqm"):
                    msg = (
                        f"Conflicting options: '-C {x}=vendor' and not '-C raqm=vendor'"
                    )
                    raise ValueError(msg)
                _dbg("Using vendored version of %s", x)
                self.feature.vendor.add(x)

    def _update_extension(
        self,
        name: str,
        libraries: list[str] | list[str | bool | None],
        define_macros: list[tuple[str, str | None]] | None = None,
        sources: list[str] | None = None,
    ) -> None:
        for extension in self.extensions:
            if extension.name == name:
                extension.libraries += libraries
                if define_macros is not None:
                    extension.define_macros += define_macros
                if sources is not None:
                    extension.sources += sources
                if FUZZING_BUILD:
                    extension.language = "c++"
                    extension.extra_link_args = ["--stdlib=libc++"]
                break

    def _remove_extension(self, name: str) -> None:
        for extension in self.extensions:
            if extension.name == name:
                self.extensions.remove(extension)
                break

    def get_macos_sdk_path(self) -> str | None:
        try:
            sdk_path = (
                subprocess.check_output(["xcrun", "--show-sdk-path", "--sdk", "macosx"])
                .strip()
                .decode("latin1")
            )
        except Exception:
            sdk_path = None
        if (
            not sdk_path
            or sdk_path == "/Applications/Xcode.app/Contents/Developer"
            "/Platforms/MacOSX.platform/Developer/SDKs/MacOSX.sdk"
        ):
            commandlinetools_sdk_path = (
                "/Library/Developer/CommandLineTools/SDKs/MacOSX.sdk"
            )
            if os.path.exists(commandlinetools_sdk_path):
                sdk_path = commandlinetools_sdk_path
        return sdk_path

    def build_extensions(self) -> None:
        library_dirs: list[str] = []
        include_dirs: list[str] = []

        pkg_config = None
        if _cmd_exists(os.environ.get("PKG_CONFIG", "pkg-config")):
            pkg_config = _pkg_config

        #
        # add configured kits
        for root_name, lib_name in {
            "JPEG_ROOT": "libjpeg",
            "JPEG2K_ROOT": "libopenjp2",
            "TIFF_ROOT": ("libtiff-5", "libtiff-4"),
            "ZLIB_ROOT": "zlib",
            "FREETYPE_ROOT": "freetype2",
            "HARFBUZZ_ROOT": "harfbuzz",
            "FRIBIDI_ROOT": "fribidi",
            "RAQM_ROOT": "raqm",
            "WEBP_ROOT": "libwebp",
            "LCMS_ROOT": "lcms2",
            "IMAGEQUANT_ROOT": "libimagequant",
        }.items():
            root = globals()[root_name]

            if root is None and root_name in os.environ:
                root_prefix = os.environ[root_name]
                root = (
                    os.path.join(root_prefix, "lib"),
                    os.path.join(root_prefix, "include"),
                )

            if root is None and pkg_config:
                if isinstance(lib_name, str):
                    _dbg(f"Looking for `{lib_name}` using pkg-config.")
                    root = pkg_config(lib_name)
                else:
                    for lib_name2 in lib_name:
                        _dbg(f"Looking for `{lib_name2}` using pkg-config.")
                        root = pkg_config(lib_name2)
                        if root:
                            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:
                    _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:
                    _add_directory(include_dirs, include_dir)

        # respect CFLAGS/CPPFLAGS/LDFLAGS
        for k in ("CFLAGS", "CPPFLAGS", "LDFLAGS"):
            if k in os.environ:
                for match in re.finditer(r"-I([^\s]+)", os.environ[k]):
                    _add_directory(include_dirs, match.group(1))
                for match in re.finditer(r"-L([^\s]+)", os.environ[k]):
                    _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 os.environ:
                for d in os.environ[k].split(os.path.pathsep):
                    _add_directory(include_dirs, d)

        for k in ("LD_RUN_PATH", "LIBRARY_PATH", "LIB"):
            if k in os.environ:
                for d in os.environ[k].split(os.path.pathsep):
                    _add_directory(library_dirs, d)

        _add_directory(library_dirs, os.path.join(sys.prefix, "lib"))
        _add_directory(include_dirs, os.path.join(sys.prefix, "include"))

        #
        # add platform directories

        if self.disable_platform_guessing:
            pass

        elif sys.platform == "cygwin":
            # pythonX.Y.dll.a is in the /usr/lib/pythonX.Y/config directory
            self.compiler.shared_lib_extension = ".dll.a"
            _add_directory(
                library_dirs,
                os.path.join(
                    "/usr/lib", "python{}.{}".format(*sys.version_info), "config"
                ),
            )

        elif sys.platform == "darwin":
            # attempt to make sure we pick freetype2 over other versions
            _add_directory(include_dirs, "/sw/include/freetype2")
            _add_directory(include_dirs, "/sw/lib/freetype2/include")
            # fink installation directories
            _add_directory(library_dirs, "/sw/lib")
            _add_directory(include_dirs, "/sw/include")
            # darwin ports installation directories
            _add_directory(library_dirs, "/opt/local/lib")
            _add_directory(include_dirs, "/opt/local/include")

            # if Homebrew is installed, use its lib and include directories
            try:
                prefix = (
                    subprocess.check_output(["brew", "--prefix"])
                    .strip()
                    .decode("latin1")
                )
            except Exception:
                # Homebrew not installed
                prefix = None

            ft_prefix = None

            if prefix:
                # add Homebrew's include and lib directories
                _add_directory(library_dirs, os.path.join(prefix, "lib"))
                _add_directory(include_dirs, os.path.join(prefix, "include"))
                _add_directory(
                    include_dirs, os.path.join(prefix, "opt", "zlib", "include")
                )
                ft_prefix = os.path.join(prefix, "opt", "freetype")

            if ft_prefix and os.path.isdir(ft_prefix):
                # freetype might not be linked into Homebrew's prefix
                _add_directory(library_dirs, os.path.join(ft_prefix, "lib"))
                _add_directory(include_dirs, os.path.join(ft_prefix, "include"))
            else:
                # fall back to freetype from XQuartz if
                # Homebrew's freetype is missing
                _add_directory(library_dirs, "/usr/X11/lib")
                _add_directory(include_dirs, "/usr/X11/include")

            # Add the macOS SDK path.
            sdk_path = self.get_macos_sdk_path()
            if sdk_path:
                _add_directory(library_dirs, os.path.join(sdk_path, "usr", "lib"))
                _add_directory(include_dirs, os.path.join(sdk_path, "usr", "include"))

                for extension in self.extensions:
                    extension.extra_compile_args = ["-Wno-nullability-completeness"]
        elif (
            sys.platform.startswith("linux")
            or sys.platform.startswith("gnu")
            or sys.platform.startswith("freebsd")
        ):
            for dirname in _find_library_dirs_ldconfig():
                _add_directory(library_dirs, dirname)
            if sys.platform.startswith("linux") and os.environ.get("ANDROID_ROOT"):
                # termux support for android.
                # system libraries (zlib) are installed in /system/lib
                # headers are at $PREFIX/include
                # user libs are at $PREFIX/lib
                _add_directory(
                    library_dirs,
                    os.path.join(
                        os.environ["ANDROID_ROOT"],
                        "lib" if struct.calcsize("l") == 4 else "lib64",
                    ),
                )

        elif sys.platform.startswith("netbsd"):
            _add_directory(library_dirs, "/usr/pkg/lib")
            _add_directory(include_dirs, "/usr/pkg/include")

        elif sys.platform.startswith("sunos5"):
            _add_directory(library_dirs, "/opt/local/lib")
            _add_directory(include_dirs, "/opt/local/include")

        # FIXME: check /opt/stuff directories here?

        # standard locations
        if not self.disable_platform_guessing:
            _add_directory(library_dirs, "/usr/local/lib")
            _add_directory(include_dirs, "/usr/local/include")

            _add_directory(library_dirs, "/usr/lib")
            _add_directory(include_dirs, "/usr/include")
            # alpine, at least
            _add_directory(library_dirs, "/lib")

        if sys.platform == "win32":
            # on Windows, look for the OpenJPEG libraries in the location that
            # the official installer puts them
            program_files = os.environ.get("ProgramFiles", "")
            best_version = (0, 0)
            best_path = None
            for name in os.listdir(program_files):
                if name.startswith("OpenJPEG "):
                    version = tuple(int(x) for x in name[9:].strip().split("."))
                    if version > best_version:
                        best_version = version
                        best_path = os.path.join(program_files, name)

            if best_path:
                _dbg("Adding %s to search list", best_path)
                _add_directory(library_dirs, os.path.join(best_path, "lib"))
                _add_directory(include_dirs, os.path.join(best_path, "include"))

        #
        # insert new dirs *before* default libs, to avoid conflicts
        # between Python PYD stub libs and real libraries

        self.compiler.library_dirs = library_dirs + self.compiler.library_dirs
        self.compiler.include_dirs = include_dirs + self.compiler.include_dirs

        #
        # look for available libraries

        feature = self.feature

        if feature.want("zlib"):
            _dbg("Looking for zlib")
            if _find_include_file(self, "zlib.h"):
                if _find_library_file(self, "z"):
                    feature.set("zlib", "z")
                elif sys.platform == "win32" and _find_library_file(self, "zlib"):
                    feature.set("zlib", "zlib")  # alternative name
                elif sys.platform == "win32" and _find_library_file(self, "zdll"):
                    feature.set("zlib", "zdll")  # dll import library

        if feature.want("jpeg"):
            _dbg("Looking for jpeg")
            if _find_include_file(self, "jpeglib.h"):
                if _find_library_file(self, "jpeg"):
                    feature.set("jpeg", "jpeg")
                elif sys.platform == "win32" and _find_library_file(self, "libjpeg"):
                    feature.set("jpeg", "libjpeg")  # alternative name

        feature.set("openjpeg_version", None)
        if feature.want("jpeg2000"):
            _dbg("Looking for jpeg2000")
            best_version: tuple[int, ...] | None = None
            best_path = None

            # Find the best version
            for directory in self.compiler.include_dirs:
                _dbg("Checking for openjpeg-#.# in %s", directory)
                try:
                    listdir = os.listdir(directory)
                except Exception:
                    # OSError, FileNotFoundError
                    continue
                for name in listdir:
                    if name.startswith("openjpeg-") and os.path.isfile(
                        os.path.join(directory, name, "openjpeg.h")
                    ):
                        _dbg("Found openjpeg.h in %s/%s", (directory, name))
                        version = tuple(int(x) for x in name[9:].split("."))
                        if best_version is None or version > best_version:
                            best_version = version
                            best_path = os.path.join(directory, name)
                            _dbg(
                                "Best openjpeg version %s so far in %s",
                                (best_version, best_path),
                            )

            if best_version and _find_library_file(self, "openjp2"):
                # Add the directory to the include path so we can include
                # <openjpeg.h> rather than having to cope with the versioned
                # include path
                _add_directory(self.compiler.include_dirs, best_path, 0)
                feature.set("jpeg2000", "openjp2")
                feature.set("openjpeg_version", ".".join(str(x) for x in best_version))

        if feature.want("imagequant"):
            _dbg("Looking for imagequant")
            if _find_include_file(self, "libimagequant.h"):
                if _find_library_file(self, "imagequant"):
                    feature.set("imagequant", "imagequant")
                elif _find_library_file(self, "libimagequant"):
                    feature.set("imagequant", "libimagequant")

        if feature.want("tiff"):
            _dbg("Looking for tiff")
            if _find_include_file(self, "tiff.h"):
                if _find_library_file(self, "tiff"):
                    feature.set("tiff", "tiff")
                if sys.platform in ["win32", "darwin"] and _find_library_file(
                    self, "libtiff"
                ):
                    feature.set("tiff", "libtiff")

        if feature.want("freetype"):
            _dbg("Looking for freetype")
            if _find_library_file(self, "freetype"):
                # look for freetype2 include files
                freetype_version = 0
                for subdir in self.compiler.include_dirs:
                    _dbg("Checking for include file %s in %s", ("ft2build.h", subdir))
                    if os.path.isfile(os.path.join(subdir, "ft2build.h")):
                        _dbg("Found %s in %s", ("ft2build.h", subdir))
                        freetype_version = 21
                        subdir = os.path.join(subdir, "freetype2")
                        break
                    subdir = os.path.join(subdir, "freetype2")
                    _dbg("Checking for include file %s in %s", ("ft2build.h", subdir))
                    if os.path.isfile(os.path.join(subdir, "ft2build.h")):
                        _dbg("Found %s in %s", ("ft2build.h", subdir))
                        freetype_version = 21
                        break
                if freetype_version:
                    feature.set("freetype", "freetype")
                    if subdir:
                        _add_directory(self.compiler.include_dirs, subdir, 0)

        if feature.get("freetype") and feature.want("raqm"):
            if not feature.want_vendor("raqm"):  # want system Raqm
                _dbg("Looking for Raqm")
                if _find_include_file(self, "raqm.h"):
                    if _find_library_file(self, "raqm"):
                        feature.set("raqm", "raqm")
                    elif _find_library_file(self, "libraqm"):
                        feature.set("raqm", "libraqm")
            else:  # want to build Raqm from src/thirdparty
                _dbg("Looking for HarfBuzz")
                feature.set("harfbuzz", None)
                hb_dir = _find_include_dir(self, "harfbuzz", "hb.h")
                if hb_dir:
                    if isinstance(hb_dir, str):
                        _add_directory(self.compiler.include_dirs, hb_dir, 0)
                    if _find_library_file(self, "harfbuzz"):
                        feature.set("harfbuzz", "harfbuzz")
                if feature.get("harfbuzz"):
                    if not feature.want_vendor("fribidi"):  # want system FriBiDi
                        _dbg("Looking for FriBiDi")
                        feature.set("fribidi", None)
                        fribidi_dir = _find_include_dir(self, "fribidi", "fribidi.h")
                        if fribidi_dir:
                            if isinstance(fribidi_dir, str):
                                _add_directory(
                                    self.compiler.include_dirs, fribidi_dir, 0
                                )
                            if _find_library_file(self, "fribidi"):
                                feature.set("fribidi", "fribidi")
                                feature.set("raqm", True)
                    else:  # want to build FriBiDi shim from src/thirdparty
                        feature.set("raqm", True)

        if feature.want("lcms"):
            _dbg("Looking for lcms")
            if _find_include_file(self, "lcms2.h"):
                if _find_library_file(self, "lcms2"):
                    feature.set("lcms", "lcms2")
                elif _find_library_file(self, "lcms2_static"):
                    # alternate Windows name.
                    feature.set("lcms", "lcms2_static")

        if feature.want("webp"):
            _dbg("Looking for webp")
            if all(
                _find_include_file(self, "webp/" + include)
                for include in ("encode.h", "decode.h", "mux.h", "demux.h")
            ):
                # In Google's precompiled zip it is called "libwebp"
                for prefix in ("", "lib"):
                    if all(
                        _find_library_file(self, prefix + library)
                        for library in ("webp", "webpmux", "webpdemux")
                    ):
                        feature.set("webp", prefix + "webp")
                        break

        if feature.want("xcb"):
            _dbg("Looking for xcb")
            if _find_include_file(self, "xcb/xcb.h"):
                if _find_library_file(self, "xcb"):
                    feature.set("xcb", "xcb")

        for f in feature:
            if not feature.get(f) and feature.require(f):
                if f in ("jpeg", "zlib"):
                    raise RequiredDependencyException(f)
                raise DependencyException(f)

        #
        # core library

        libs: list[str | bool | None] = []
        libs.extend(self.add_imaging_libs.split())
        defs: list[tuple[str, str | None]] = []
        if feature.get("tiff"):
            libs.append(feature.get("tiff"))
            defs.append(("HAVE_LIBTIFF", None))
            if sys.platform == "win32":
                # This define needs to be defined if-and-only-if it was defined
                # when compiling LibTIFF. LibTIFF doesn't expose it in `tiffconf.h`,
                # so we have to guess; by default it is defined in all Windows builds.
                # See #4237, #5243, #5359 for more information.
                defs.append(("USE_WIN32_FILEIO", None))
        if feature.get("jpeg"):
            libs.append(feature.get("jpeg"))
            defs.append(("HAVE_LIBJPEG", None))
        if feature.get("jpeg2000"):
            libs.append(feature.get("jpeg2000"))
            defs.append(("HAVE_OPENJPEG", None))
            if sys.platform == "win32" and not PLATFORM_MINGW:
                defs.append(("OPJ_STATIC", None))
        if feature.get("zlib"):
            libs.append(feature.get("zlib"))
            defs.append(("HAVE_LIBZ", None))
        if feature.get("imagequant"):
            libs.append(feature.get("imagequant"))
            defs.append(("HAVE_LIBIMAGEQUANT", None))
        if feature.get("xcb"):
            libs.append(feature.get("xcb"))
            defs.append(("HAVE_XCB", None))
        if sys.platform == "win32":
            libs.extend(["kernel32", "user32", "gdi32"])
        if struct.unpack("h", b"\0\1")[0] == 1:
            defs.append(("WORDS_BIGENDIAN", None))

        defs.append(("PILLOW_VERSION", f'"{PILLOW_VERSION}"'))

        self._update_extension("PIL._imaging", libs, defs)

        #
        # additional libraries

        if feature.get("freetype"):
            srcs = []
            libs = ["freetype"]
            defs = []
            if feature.get("raqm"):
                if not feature.want_vendor("raqm"):  # using system Raqm
                    defs.append(("HAVE_RAQM", None))
                    defs.append(("HAVE_RAQM_SYSTEM", None))
                    libs.append(feature.get("raqm"))
                else:  # building Raqm from src/thirdparty
                    defs.append(("HAVE_RAQM", None))
                    srcs.append("src/thirdparty/raqm/raqm.c")
                    libs.append(feature.get("harfbuzz"))
                    if not feature.want_vendor("fribidi"):  # using system FriBiDi
                        defs.append(("HAVE_FRIBIDI_SYSTEM", None))
                        libs.append(feature.get("fribidi"))
                    else:  # building FriBiDi shim from src/thirdparty
                        srcs.append("src/thirdparty/fribidi-shim/fribidi.c")
            self._update_extension("PIL._imagingft", libs, defs, srcs)

        else:
            self._remove_extension("PIL._imagingft")

        if feature.get("lcms"):
            libs = [feature.get("lcms")]
            if sys.platform == "win32":
                libs.extend(["user32", "gdi32"])
            self._update_extension("PIL._imagingcms", libs)
        else:
            self._remove_extension("PIL._imagingcms")

        webp = feature.get("webp")
        if isinstance(webp, str):
            libs = [webp, webp + "mux", webp + "demux"]
            self._update_extension("PIL._webp", libs)
        else:
            self._remove_extension("PIL._webp")

        tk_libs = ["psapi"] if sys.platform in ("win32", "cygwin") else []
        self._update_extension("PIL._imagingtk", tk_libs)

        build_ext.build_extensions(self)

        #
        # sanity checks

        self.summary_report(feature)

    def summary_report(self, feature: ext_feature) -> None:
        print("-" * 68)
        print("PIL SETUP SUMMARY")
        print("-" * 68)
        print(f"version      Pillow {PILLOW_VERSION}")
        version = sys.version.split("[")
        print(f"platform     {sys.platform} {version[0].strip()}")
        for v in version[1:]:
            print(f"             [{v.strip()}")
        print("-" * 68)

        raqm_extra_info = ""
        if feature.want_vendor("raqm"):
            raqm_extra_info += "bundled"
            if feature.want_vendor("fribidi"):
                raqm_extra_info += ", FriBiDi shim"

        options = [
            (feature.get("jpeg"), "JPEG"),
            (
                feature.get("jpeg2000"),
                "OPENJPEG (JPEG2000)",
                feature.get("openjpeg_version"),
            ),
            (feature.get("zlib"), "ZLIB (PNG/ZIP)"),
            (feature.get("imagequant"), "LIBIMAGEQUANT"),
            (feature.get("tiff"), "LIBTIFF"),
            (feature.get("freetype"), "FREETYPE2"),
            (feature.get("raqm"), "RAQM (Text shaping)", raqm_extra_info),
            (feature.get("lcms"), "LITTLECMS2"),
            (feature.get("webp"), "WEBP"),
            (feature.get("xcb"), "XCB (X protocol)"),
        ]

        all = 1
        for option in options:
            if option[0]:
                extra_info = ""
                if len(option) >= 3 and option[2]:
                    extra_info = f" ({option[2]})"
                print(f"--- {option[1]} support available{extra_info}")
            else:
                print(f"*** {option[1]} support not available")
                all = 0

        print("-" * 68)

        if not all:
            print("To add a missing option, make sure you have the required")
            print("library and headers.")
            print(
                "See https://pillow.readthedocs.io/en/latest/installation."
                "html#building-from-source"
            )
            print("")

        print("To check the build, run the selftest.py script.")
        print("")


def debug_build() -> bool:
    return hasattr(sys, "gettotalrefcount") or FUZZING_BUILD


files: list[str | os.PathLike[str]] = ["src/_imaging.c"]
for src_file in _IMAGING:
    files.append("src/" + src_file + ".c")
for src_file in _LIB_IMAGING:
    files.append(os.path.join("src/libImaging", src_file + ".c"))
ext_modules = [
    Extension("PIL._imaging", files),
    Extension("PIL._imagingft", ["src/_imagingft.c"]),
    Extension("PIL._imagingcms", ["src/_imagingcms.c"]),
    Extension("PIL._webp", ["src/_webp.c"]),
    Extension("PIL._imagingtk", ["src/_imagingtk.c", "src/Tk/tkImaging.c"]),
    Extension("PIL._imagingmath", ["src/_imagingmath.c"]),
    Extension("PIL._imagingmorph", ["src/_imagingmorph.c"]),
]


# parse configuration from _custom_build/backend.py
while sys.argv[-1].startswith("--pillow-configuration="):
    _, key, value = sys.argv.pop().split("=", 2)
    configuration.setdefault(key, []).append(value)

try:
    setup(
        cmdclass={"build_ext": pil_build_ext},
        ext_modules=ext_modules,
        zip_safe=not (debug_build() or PLATFORM_MINGW),
    )
except RequiredDependencyException as err:
    msg = f"""

The headers or library files could not be found for {str(err)},
a required dependency when compiling Pillow from source.

Please see the install instructions at:
   https://pillow.readthedocs.io/en/latest/installation/basic-installation.html

"""
    sys.stderr.write(msg)
    raise RequiredDependencyException(msg)
except DependencyException as err:
    msg = f"""

The headers or library files could not be found for {str(err)},
which was requested by the option flag '-C {str(err)}=enable'

"""
    sys.stderr.write(msg)
    raise DependencyException(msg)
