cmake_extension.py 10.0 KB
# cmake/cmake_extension.py
# Copyright (c)  2023  Xiaomi Corporation
#
# flake8: noqa

import os
import platform
import shutil
import sys
from pathlib import Path

import glob
import setuptools
from setuptools.command.build_ext import build_ext


def need_split_package():
    ans = os.environ.get("SHERPA_ONNX_SPLIT_PYTHON_PACKAGE", None)
    return ans is not None


def is_for_pypi():
    ans = os.environ.get("SHERPA_ONNX_IS_FOR_PYPI", None)
    return ans is not None


def is_macos():
    return platform.system() == "Darwin"


def is_windows():
    return platform.system() == "Windows"


def is_linux():
    return platform.system() == "Linux"


def is_arm64():
    return platform.machine() in ["arm64", "aarch64"]


def is_x86():
    return platform.machine() in ["i386", "i686", "x86_64"]


def enable_alsa():
    build_alsa = os.environ.get("SHERPA_ONNX_ENABLE_ALSA", None)
    return build_alsa and is_linux() and (is_arm64() or is_x86())


def get_binaries():
    binaries = [
        "sherpa-onnx",
        "sherpa-onnx-keyword-spotter",
        "sherpa-onnx-microphone",
        "sherpa-onnx-microphone-offline",
        "sherpa-onnx-microphone-offline-audio-tagging",
        "sherpa-onnx-microphone-offline-speaker-identification",
        "sherpa-onnx-offline",
        "sherpa-onnx-offline-audio-tagging",
        "sherpa-onnx-offline-denoiser",
        "sherpa-onnx-offline-language-identification",
        "sherpa-onnx-offline-punctuation",
        "sherpa-onnx-offline-source-separation",
        "sherpa-onnx-offline-speaker-diarization",
        "sherpa-onnx-offline-tts",
        "sherpa-onnx-offline-tts-play",
        "sherpa-onnx-offline-websocket-server",
        "sherpa-onnx-online-punctuation",
        "sherpa-onnx-online-websocket-client",
        "sherpa-onnx-online-websocket-server",
        "sherpa-onnx-vad",
        "sherpa-onnx-vad-microphone",
        "sherpa-onnx-vad-microphone-offline-asr",
        "sherpa-onnx-vad-with-offline-asr",
        "sherpa-onnx-vad-with-online-asr",
        "sherpa-onnx-version",
    ]

    if enable_alsa():
        binaries += [
            "sherpa-onnx-alsa",
            "sherpa-onnx-alsa-offline",
            "sherpa-onnx-alsa-offline-audio-tagging",
            "sherpa-onnx-alsa-offline-speaker-identification",
            "sherpa-onnx-offline-tts-play-alsa",
            "sherpa-onnx-vad-alsa",
            "sherpa-onnx-vad-alsa-offline-asr",
        ]

    if is_windows():
        binaries += [
            "onnxruntime.dll",
            "sherpa-onnx-c-api.dll",
            "sherpa-onnx-cxx-api.dll",
        ]

    return binaries


try:
    from wheel.bdist_wheel import bdist_wheel as _bdist_wheel

    class bdist_wheel(_bdist_wheel):
        def finalize_options(self):
            _bdist_wheel.finalize_options(self)
            # In this case, the generated wheel has a name in the form
            # sherpa-xxx-pyxx-none-any.whl
            if is_for_pypi() and not is_macos():
                self.root_is_pure = True
            else:
                # The generated wheel has a name ending with
                # -linux_x86_64.whl
                self.root_is_pure = False

except ImportError:
    bdist_wheel = None


def cmake_extension(name, *args, **kwargs) -> setuptools.Extension:
    kwargs["language"] = "c++"
    sources = []
    return setuptools.Extension(name, sources, *args, **kwargs)


class BuildExtension(build_ext):
    def build_extension(self, ext: setuptools.extension.Extension):
        # build/temp.linux-x86_64-3.8
        os.makedirs(self.build_temp, exist_ok=True)

        # build/lib.linux-x86_64-3.8
        os.makedirs(self.build_lib, exist_ok=True)

        out_bin_dir = Path(self.build_lib).parent / "sherpa_onnx" / "bin"
        install_dir = Path(self.build_lib).resolve() / "sherpa_onnx"

        sherpa_onnx_dir = Path(__file__).parent.parent.resolve()

        cmake_args = os.environ.get("SHERPA_ONNX_CMAKE_ARGS", "")
        make_args = os.environ.get("SHERPA_ONNX_MAKE_ARGS", "")
        system_make_args = os.environ.get("MAKEFLAGS", "")

        if cmake_args == "":
            cmake_args = "-DCMAKE_BUILD_TYPE=Release"

        extra_cmake_args = ""
        if not need_split_package():
            extra_cmake_args += f" -DCMAKE_INSTALL_PREFIX={install_dir} "
        extra_cmake_args += " -DBUILD_SHARED_LIBS=ON "
        extra_cmake_args += " -DBUILD_PIPER_PHONMIZE_EXE=OFF "
        extra_cmake_args += " -DBUILD_PIPER_PHONMIZE_TESTS=OFF "
        extra_cmake_args += " -DBUILD_ESPEAK_NG_EXE=OFF "
        extra_cmake_args += " -DBUILD_ESPEAK_NG_TESTS=OFF "

        if not need_split_package():
            extra_cmake_args += " -DSHERPA_ONNX_ENABLE_C_API=ON "

        extra_cmake_args += " -DSHERPA_ONNX_BUILD_C_API_EXAMPLES=OFF "
        extra_cmake_args += " -DSHERPA_ONNX_ENABLE_CHECK=OFF "
        extra_cmake_args += " -DSHERPA_ONNX_ENABLE_PYTHON=ON "
        extra_cmake_args += " -DSHERPA_ONNX_ENABLE_PORTAUDIO=ON "
        if not need_split_package():
            extra_cmake_args += " -DSHERPA_ONNX_ENABLE_WEBSOCKET=ON "

        if "PYTHON_EXECUTABLE" not in cmake_args:
            print(f"Setting PYTHON_EXECUTABLE to {sys.executable}")
            cmake_args += f" -DPYTHON_EXECUTABLE={sys.executable}"

        # putting `cmake_args` from env variable ${SHERPA_ONNX_CMAKE_ARGS} last,
        # so they can onverride the "defaults" stored in `extra_cmake_args`
        cmake_args = extra_cmake_args + cmake_args

        if is_windows():
            if not need_split_package():
                build_cmd = f"""
             cmake {cmake_args} -B {self.build_temp} -S {sherpa_onnx_dir}
             cmake --build {self.build_temp} --target install --config Release -- -m:2
                """
            else:
                build_cmd = f"""
             cmake {cmake_args} -B {self.build_temp} -S {sherpa_onnx_dir}
             cmake --build {self.build_temp} --target _sherpa_onnx --config Release -- -m:2
                """

            print(f"build command is:\n{build_cmd}")
            ret = os.system(
                f"cmake {cmake_args} -B {self.build_temp} -S {sherpa_onnx_dir}"
            )
            if ret != 0:
                raise Exception("Failed to configure sherpa")

            if not need_split_package():
                ret = os.system(
                    f"cmake --build {self.build_temp} --target install --config Release -- -m:2"  # noqa
                )
            else:
                ret = os.system(
                    f"cmake --build {self.build_temp} --target _sherpa_onnx --config Release -- -m:2"  # noqa
                )
            if ret != 0:
                raise Exception("Failed to build and install sherpa")
        else:
            if make_args == "" and system_make_args == "":
                print("for fast compilation, run:")
                print('export SHERPA_ONNX_MAKE_ARGS="-j"; python setup.py install')
                print('Setting make_args to "-j4"')
                make_args = "-j4"

            if "-G Ninja" in cmake_args:
                if not need_split_package():
                    build_cmd = f"""
                        cd {self.build_temp}
                        cmake {cmake_args} {sherpa_onnx_dir}
                        ninja {make_args} install
                    """
                else:
                    build_cmd = f"""
                        cd {self.build_temp}
                        cmake {cmake_args} {sherpa_onnx_dir}
                        ninja {make_args} _sherpa_onnx
                    """
            else:
                if not need_split_package():
                    build_cmd = f"""
                        cd {self.build_temp}

                        cmake {cmake_args} {sherpa_onnx_dir}

                        make {make_args} install/strip
                    """
                else:
                    build_cmd = f"""
                        cd {self.build_temp}

                        cmake {cmake_args} {sherpa_onnx_dir}

                        make {make_args} _sherpa_onnx
                    """
            print(f"build command is:\n{build_cmd}")

            ret = os.system(build_cmd)
            if ret != 0:
                raise Exception(
                    "\nBuild sherpa-onnx failed. Please check the error message.\n"
                    "You can ask for help by creating an issue on GitHub.\n"
                    "\nClick:\n\thttps://github.com/k2-fsa/sherpa-onnx/issues/new\n"  # noqa
                )

        if need_split_package():
            dst = os.path.join(f"{self.build_lib}", "sherpa_onnx", "lib")
            os.system(f"mkdir {dst}")
            os.system(f"dir {dst}")

            ext = "pyd" if sys.platform.startswith("win") else "so"
            pattern = os.path.join(self.build_temp, "**", f"_sherpa_onnx.*.{ext}")
            matches = glob.glob(pattern, recursive=True)
            print("matches", list(matches))

            for f in matches:
                print(f, os.path.join(f"{self.build_lib}", "sherpa_onnx", "lib"))
                shutil.copy(f"{f}", dst)
                os.system(f"dir {dst}")

            return

        suffix = ".exe" if is_windows() else ""
        # Remember to also change setup.py

        binaries = get_binaries()

        for f in binaries:
            suffix = "" if ".dll" in f else suffix
            src_file = install_dir / "bin" / (f + suffix)
            if not src_file.is_file():
                src_file = install_dir / "lib" / (f + suffix)
            if not src_file.is_file():
                src_file = install_dir / ".." / (f + suffix)

            if not src_file.is_file():
                continue

            print(f"Copying {src_file} to {out_bin_dir}/")
            shutil.copy(f"{src_file}", f"{out_bin_dir}/")

        if Path(f"{install_dir}/bin").is_dir():
            shutil.rmtree(f"{install_dir}/bin")
        if Path(f"{install_dir}/share").is_dir():
            shutil.rmtree(f"{install_dir}/share")
        if Path(f"{install_dir}/lib/pkgconfig").is_dir():
            shutil.rmtree(f"{install_dir}/lib/pkgconfig")

        if is_macos():
            os.remove(f"{install_dir}/lib/libonnxruntime.dylib")