xref: /aosp_15_r20/external/executorch/setup.py (revision 523fa7a60841cd1ecfb9cc4201f1ca8b03ed023a)
1# Copyright (c) Meta Platforms, Inc. and affiliates.
2# Copyright 2024 Arm Limited and/or its affiliates.
3# All rights reserved.
4#
5# This source code is licensed under the BSD-style license found in the
6# LICENSE file in the root directory of this source tree.
7
8# Part of this code is from pybind11 cmake_example:
9# https://github.com/pybind/cmake_example/blob/master/setup.py so attach the
10# license below.
11
12# Copyright (c) 2016 The Pybind Development Team, All rights reserved.
13#
14# Redistribution and use in source and binary forms, with or without
15# modification, are permitted provided that the following conditions are met:
16#
17# 1. Redistributions of source code must retain the above copyright notice, this
18#    list of conditions and the following disclaimer.
19#
20# 2. Redistributions in binary form must reproduce the above copyright notice,
21#    this list of conditions and the following disclaimer in the documentation
22#    and/or other materials provided with the distribution.
23#
24# 3. Neither the name of the copyright holder nor the names of its contributors
25#    may be used to endorse or promote products derived from this software
26#    without specific prior written permission.
27#
28# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND
29# ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
30# WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
31# DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
32# FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
33# DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
34# SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
35# CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
36# OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
37# OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
38#
39# You are under no obligation whatsoever to provide any bug fixes, patches, or
40# upgrades to the features, functionality or performance of the source code
41# ("Enhancements") to anyone; however, if you choose to make your Enhancements
42# available either publicly, or directly to the author of this software, without
43# imposing a separate written license agreement for such Enhancements, then you
44# hereby grant the following license: a non-exclusive, royalty-free perpetual
45# license to install, use, modify, prepare derivative works, incorporate into
46# other computer software, distribute, and sublicense such enhancements or
47# derivative works thereof, in binary and source code form.
48
49import contextlib
50import os
51import platform
52import re
53import sys
54
55# Import this before distutils so that setuptools can intercept the distuils
56# imports.
57import setuptools  # noqa: F401 # usort: skip
58
59from distutils import log
60from distutils.sysconfig import get_python_lib
61from pathlib import Path
62from typing import List, Optional
63
64from setuptools import Extension, setup
65from setuptools.command.build import build
66from setuptools.command.build_ext import build_ext
67from setuptools.command.build_py import build_py
68
69# For information on setuptools Command subclassing see
70# https://setuptools.pypa.io/en/latest/userguide/extension.html
71
72
73class ShouldBuild:
74    """Indicates whether to build various components."""
75
76    @staticmethod
77    def _is_env_enabled(env_var: str, default: bool = False) -> bool:
78        val = os.environ.get(env_var, None)
79        if val is None:
80            return default
81        if val in ("OFF", "0", ""):
82            return False
83        return True
84
85    @classmethod
86    def pybindings(cls) -> bool:
87        return cls._is_env_enabled("EXECUTORCH_BUILD_PYBIND", default=False)
88
89    @classmethod
90    def llama_custom_ops(cls) -> bool:
91        return cls._is_env_enabled("EXECUTORCH_BUILD_KERNELS_CUSTOM_AOT", default=True)
92
93    @classmethod
94    def flatc(cls) -> bool:
95        return cls._is_env_enabled("EXECUTORCH_BUILD_FLATC", default=True)
96
97
98class Version:
99    """Static strings that describe the version of the pip package."""
100
101    # Cached values returned by the properties.
102    __root_dir_attr: Optional[str] = None
103    __string_attr: Optional[str] = None
104    __git_hash_attr: Optional[str] = None
105
106    @classmethod
107    def _root_dir(cls) -> str:
108        """The path to the root of the git repo."""
109        if cls.__root_dir_attr is None:
110            # This setup.py file lives in the root of the repo.
111            cls.__root_dir_attr = str(Path(__file__).parent.resolve())
112        return str(cls.__root_dir_attr)
113
114    @classmethod
115    def git_hash(cls) -> Optional[str]:
116        """The current git hash, if known."""
117        if cls.__git_hash_attr is None:
118            import subprocess
119
120            try:
121                cls.__git_hash_attr = (
122                    subprocess.check_output(
123                        ["git", "rev-parse", "HEAD"], cwd=cls._root_dir()
124                    )
125                    .decode("ascii")
126                    .strip()
127                )
128            except subprocess.CalledProcessError:
129                cls.__git_hash_attr = ""  # Non-None but empty.
130        # A non-None but empty value indicates that we don't know it.
131        return cls.__git_hash_attr if cls.__git_hash_attr else None
132
133    @classmethod
134    def string(cls) -> str:
135        """The version string."""
136        if cls.__string_attr is None:
137            # If set, BUILD_VERSION should override any local version
138            # information. CI will use this to manage, e.g., release vs. nightly
139            # versions.
140            version = os.getenv("BUILD_VERSION", "").strip()
141            if not version:
142                # Otherwise, read the version from a local file and add the git
143                # commit if available.
144                version = (
145                    open(os.path.join(cls._root_dir(), "version.txt")).read().strip()
146                )
147                if cls.git_hash():
148                    version += "+" + cls.git_hash()[:7]
149            cls.__string_attr = version
150        return cls.__string_attr
151
152    @classmethod
153    def write_to_python_file(cls, path: str) -> None:
154        """Creates a file similar to PyTorch core's `torch/version.py`."""
155        lines = [
156            "from typing import Optional",
157            '__all__ = ["__version__", "git_version"]',
158            f'__version__ = "{cls.string()}"',
159            # A string or None.
160            f"git_version: Optional[str] = {repr(cls.git_hash())}",
161        ]
162        with open(path, "w") as fp:
163            fp.write("\n".join(lines) + "\n")
164
165
166# The build type is determined by the DEBUG environment variable. If DEBUG is
167# set to a non-empty value, the build type is Debug. Otherwise, the build type
168# is Release.
169def get_build_type(is_debug=None) -> str:
170    debug = int(os.environ.get("DEBUG", 0)) if is_debug is None else is_debug
171    cfg = "Debug" if debug else "Release"
172    return cfg
173
174
175def get_dynamic_lib_name(name: str) -> str:
176    if platform.system() == "Windows":
177        return name + ".dll"
178    elif platform.system() == "Darwin":
179        return "lib" + name + ".dylib"
180    else:
181        return "lib" + name + ".so"
182
183
184def get_executable_name(name: str) -> str:
185    if platform.system() == "Windows":
186        return name + ".exe"
187    else:
188        return name
189
190
191class _BaseExtension(Extension):
192    """A base class that maps an abstract source to an abstract destination."""
193
194    def __init__(self, src: str, dst: str, name: str):
195        # Source path; semantics defined by the subclass.
196        self.src: str = src
197
198        # Destination path relative to a namespace defined elsewhere. If this ends
199        # in "/", it is treated as a directory. If this is "", it is treated as the
200        # root of the namespace.
201        # Destination path; semantics defined by the subclass.
202        self.dst: str = dst
203
204        # Other parts of setuptools expects .name to exist. For actual extensions
205        # this can be the module path, but otherwise it should be somehing unique
206        # that doesn't look like a module path.
207        self.name: str = name
208
209        super().__init__(name=self.name, sources=[])
210
211    def src_path(self, installer: "InstallerBuildExt") -> Path:
212        """Returns the path to the source file, resolving globs.
213
214        Args:
215            installer: The InstallerBuildExt instance that is installing the
216                file.
217        """
218        # Share the cmake-out location with CustomBuild.
219        cmake_cache_dir = Path(installer.get_finalized_command("build").cmake_cache_dir)
220
221        cfg = get_build_type(installer.debug)
222
223        if os.name == "nt":
224            # Replace %BUILD_TYPE% with the current build type.
225            self.src = self.src.replace("%BUILD_TYPE%", cfg)
226        else:
227            # Remove %BUILD_TYPE% from the path.
228            self.src = self.src.replace("/%BUILD_TYPE%", "")
229
230        # Construct the full source path, resolving globs. If there are no glob
231        # pattern characters, this will just ensure that the source file exists.
232        srcs = tuple(cmake_cache_dir.glob(self.src))
233        if len(srcs) != 1:
234            raise ValueError(
235                f"Expected exactly one file matching '{self.src}'; found {repr(srcs)}"
236            )
237        return srcs[0]
238
239
240class BuiltFile(_BaseExtension):
241    """An extension that installs a single file that was built by cmake.
242
243    This isn't technically a `build_ext` style python extension, but there's no
244    dedicated command for installing arbitrary data. It's convenient to use
245    this, though, because it lets us manage the files to install as entries in
246    `ext_modules`.
247    """
248
249    def __init__(
250        self,
251        src_dir: str,
252        src_name: str,
253        dst: str,
254        is_executable: bool = False,
255        is_dynamic_lib: bool = False,
256    ):
257        """Initializes a BuiltFile.
258
259        Args:
260            src_dir: The directory of the file to install, relative to the cmake-out
261                directory. A placeholder %BUILD_TYPE% will be replaced with the build
262                type for multi-config generators (like Visual Studio) where the build
263                output is in a subdirectory named after the build type. For single-
264                config generators (like Makefile Generators or Ninja), this placeholder
265                will be removed.
266            src_name: The name of the file to install
267            dst: The path to install to, relative to the root of the pip
268                package. If dst ends in "/", it is treated as a directory.
269                Otherwise it is treated as a filename.
270            is_executable: If True, the file is an executable. This is used to
271                determine the destination filename for executable.
272            is_dynamic_lib: If True, the file is a dynamic library. This is used
273                to determine the destination filename for dynamic library.
274        """
275        if is_executable and is_dynamic_lib:
276            raise ValueError("is_executable and is_dynamic_lib cannot be both True.")
277        if is_executable:
278            src_name = get_executable_name(src_name)
279        elif is_dynamic_lib:
280            src_name = get_dynamic_lib_name(src_name)
281        src = os.path.join(src_dir, src_name)
282        # This is not a real extension, so use a unique name that doesn't look
283        # like a module path. Some of setuptools's autodiscovery will look for
284        # extension names with prefixes that match certain module paths.
285        super().__init__(src=src, dst=dst, name=f"@EXECUTORCH_BuiltFile_{src}:{dst}")
286
287    def dst_path(self, installer: "InstallerBuildExt") -> Path:
288        """Returns the path to the destination file.
289
290        Args:
291            installer: The InstallerBuildExt instance that is installing the
292                file.
293        """
294        dst_root = Path(installer.build_lib).resolve()
295
296        if self.dst.endswith("/"):
297            # Destination looks like a directory. Use the basename of the source
298            # file for its final component.
299            return dst_root / Path(self.dst) / self.src_path(installer).name
300        else:
301            # Destination looks like a file.
302            return dst_root / Path(self.dst)
303
304
305class BuiltExtension(_BaseExtension):
306    """An extension that installs a python extension that was built by cmake."""
307
308    def __init__(self, src: str, modpath: str):
309        """Initializes a BuiltExtension.
310
311        Args:
312            src: The path to the file to install (typically a shared library),
313                relative to the cmake-out directory. May be an fnmatch-style
314                glob that matches exactly one file. If the path ends in `.so`,
315                this class will also look for similarly-named `.dylib` files.
316            modpath: The dotted path of the python module that maps to the
317                extension.
318        """
319        assert (
320            "/" not in modpath
321        ), f"modpath must be a dotted python module path: saw '{modpath}'"
322        # This is a real extension, so use the modpath as the name.
323        super().__init__(src=src, dst=modpath, name=modpath)
324
325    def src_path(self, installer: "InstallerBuildExt") -> Path:
326        """Returns the path to the source file, resolving globs.
327
328        Args:
329            installer: The InstallerBuildExt instance that is installing the
330                file.
331        """
332        try:
333            return super().src_path(installer)
334        except ValueError:
335            # Probably couldn't find the file. If the path ends with .so, try
336            # looking for a .dylib file instead, in case we're running on macos.
337            if self.src.endswith(".so"):
338                dylib_src = re.sub(r"\.so$", ".dylib", self.src)
339                return BuiltExtension(src=dylib_src, modpath=self.dst).src_path(
340                    installer
341                )
342            else:
343                raise
344
345    def dst_path(self, installer: "InstallerBuildExt") -> Path:
346        """Returns the path to the destination file.
347
348        Args:
349            installer: The InstallerBuildExt instance that is installing the
350                file.
351        """
352        # Our destination is a dotted module path. get_ext_fullpath() returns
353        # the relative path to the .so/.dylib/etc. file that maps to the module
354        # path: that's the file we're creating.
355        return Path(installer.get_ext_fullpath(self.dst))
356
357
358class InstallerBuildExt(build_ext):
359    """Installs files that were built by cmake."""
360
361    # TODO(dbort): Depend on the "build" command to ensure it runs first
362
363    def build_extension(self, ext: _BaseExtension) -> None:
364        src_file: Path = ext.src_path(self)
365        dst_file: Path = ext.dst_path(self)
366
367        # Ensure that the destination directory exists.
368        self.mkpath(os.fspath(dst_file.parent))
369
370        # Copy the file.
371        self.copy_file(os.fspath(src_file), os.fspath(dst_file))
372
373        # Ensure that the destination file is writable, even if the source was
374        # not. build_py does this by passing preserve_mode=False to copy_file,
375        # but that would clobber the X bit on any executables. TODO(dbort): This
376        # probably won't work on Windows.
377        if not os.access(src_file, os.W_OK):
378            # Make the file writable. This should respect the umask.
379            os.chmod(src_file, os.stat(src_file).st_mode | 0o222)
380
381
382class CustomBuildPy(build_py):
383    """Copies platform-independent files from the source tree into the output
384    package directory.
385
386    Override it so we can copy some files to locations that don't match their
387    original relative locations.
388
389    Standard setuptools features like package_data and MANIFEST.in can only
390    include or exclude a file in the source tree; they don't have a way to map
391    a file to a different relative location under the output package directory.
392    """
393
394    def run(self):
395        # Copy python files to the output directory. This set of files is
396        # defined by the py_module list and package_data patterns.
397        build_py.run(self)
398
399        # dst_root is the root of the `executorch` module in the output package
400        # directory. build_lib is the platform-independent root of the output
401        # package, and will look like `pip-out/lib`. It can contain multiple
402        # python packages, so be sure to copy the files into the `executorch`
403        # package subdirectory.
404        dst_root = os.path.join(self.build_lib, self.get_package_dir("executorch"))
405
406        # Create the version file.
407        Version.write_to_python_file(os.path.join(dst_root, "version.py"))
408
409        # Manually copy files into the output package directory. These are
410        # typically python "resource" files that will live alongside the python
411        # code that uses them.
412        src_to_dst = [
413            # TODO(dbort): See if we can add a custom pyproject.toml section for
414            # these, instead of hard-coding them here. See
415            # https://setuptools.pypa.io/en/latest/userguide/extension.html
416            ("schema/scalar_type.fbs", "exir/_serialize/scalar_type.fbs"),
417            ("schema/program.fbs", "exir/_serialize/program.fbs"),
418            (
419                "devtools/bundled_program/schema/bundled_program_schema.fbs",
420                "devtools/bundled_program/serialize/bundled_program_schema.fbs",
421            ),
422            (
423                "devtools/bundled_program/schema/scalar_type.fbs",
424                "devtools/bundled_program/serialize/scalar_type.fbs",
425            ),
426            # Install executorch-wheel-config.cmake to pip package.
427            (
428                "build/executorch-wheel-config.cmake",
429                "share/cmake/executorch-config.cmake",
430            ),
431        ]
432        # Copy all the necessary headers into include/executorch/ so that they can
433        # be found in the pip package. This is the subset of headers that are
434        # essential for building custom ops extensions.
435        # TODO: Use cmake to gather the headers instead of hard-coding them here.
436        # For example: https://discourse.cmake.org/t/installing-headers-the-modern-
437        # way-regurgitated-and-revisited/3238/3
438        for include_dir in [
439            "runtime/core/",
440            "runtime/kernel/",
441            "runtime/platform/",
442            "extension/kernel_util/",
443            "extension/tensor/",
444            "extension/threadpool/",
445        ]:
446            src_list = Path(include_dir).rglob("*.h")
447            for src in src_list:
448                src_to_dst.append(
449                    (str(src), os.path.join("include/executorch", str(src)))
450                )
451        for src, dst in src_to_dst:
452            dst = os.path.join(dst_root, dst)
453
454            # When modifying the filesystem, use the self.* methods defined by
455            # Command to benefit from the same logging and dry_run logic as
456            # setuptools.
457
458            # Ensure that the destination directory exists.
459            self.mkpath(os.path.dirname(dst))
460            # Follow the example of the base build_py class by not preserving
461            # the mode. This ensures that the output file is read/write even if
462            # the input file is read-only.
463            self.copy_file(src, dst, preserve_mode=False)
464
465
466class Buck2EnvironmentFixer(contextlib.AbstractContextManager):
467    """Removes HOME from the environment when running as root.
468
469    This script is sometimes run as root in docker containers. buck2 doesn't
470    allow running as root unless $HOME is owned by root or is not set.
471
472    TODO(pytorch/test-infra#5091): Remove this once the CI jobs stop running as
473    root.
474    """
475
476    def __init__(self):
477        self.saved_env = {}
478
479    def __enter__(self):
480        if os.name != "nt" and os.geteuid() == 0 and "HOME" in os.environ:
481            log.info("temporarily unsetting HOME while running as root")
482            self.saved_env["HOME"] = os.environ.pop("HOME")
483        return self
484
485    def __exit__(self, *args, **kwargs):
486        if "HOME" in self.saved_env:
487            log.info("restored HOME")
488            os.environ["HOME"] = self.saved_env["HOME"]
489
490
491# TODO(dbort): For editable wheels, may need to update get_source_files(),
492# get_outputs(), and get_output_mapping() to satisfy
493# https://setuptools.pypa.io/en/latest/userguide/extension.html#setuptools.command.build.SubCommand.get_output_mapping
494
495
496class CustomBuild(build):
497    def initialize_options(self):
498        super().initialize_options()
499        # The default build_base directory is called "build", but we have a
500        # top-level directory with that name. Setting build_base in setup()
501        # doesn't affect this, so override the core build command.
502        #
503        # See build.initialize_options() in
504        # setuptools/_distutils/command/build.py for the default.
505        self.build_base = "pip-out"
506
507        # Default build parallelism based on number of cores, but allow
508        # overriding through the environment.
509        default_parallel = str(os.cpu_count() - 1)
510        self.parallel = os.environ.get("CMAKE_BUILD_PARALLEL_LEVEL", default_parallel)
511
512    def run(self):
513        self.dump_options()
514
515        cfg = get_build_type(self.debug)
516
517        # get_python_lib() typically returns the path to site-packages, where
518        # all pip packages in the environment are installed.
519        cmake_prefix_path = os.environ.get("CMAKE_PREFIX_PATH", get_python_lib())
520
521        # The root of the repo should be the current working directory. Get
522        # the absolute path.
523        repo_root = os.fspath(Path.cwd())
524
525        # If blank, the cmake build system will find an appropriate binary.
526        buck2 = os.environ.get(
527            "BUCK2_EXECUTABLE", os.environ.get("BUCK2", os.environ.get("BUCK", ""))
528        )
529
530        cmake_args = [
531            f"-DBUCK2={buck2}",
532            f"-DPYTHON_EXECUTABLE={sys.executable}",
533            # Let cmake calls like `find_package(Torch)` find cmake config files
534            # like `TorchConfig.cmake` that are provided by pip packages.
535            f"-DCMAKE_PREFIX_PATH={cmake_prefix_path}",
536            f"-DCMAKE_BUILD_TYPE={cfg}",
537            # Enable logging even when in release mode. We are building for
538            # desktop, where saving a few kB is less important than showing
539            # useful error information to users.
540            "-DEXECUTORCH_ENABLE_LOGGING=ON",
541            "-DEXECUTORCH_LOG_LEVEL=Info",
542            "-DCMAKE_OSX_DEPLOYMENT_TARGET=10.15",
543            # The separate host project is only required when cross-compiling,
544            # and it can cause build race conditions (libflatcc.a errors) when
545            # enabled. TODO(dbort): Remove this override once this option is
546            # managed by cmake itself.
547            "-DEXECUTORCH_SEPARATE_FLATCC_HOST_PROJECT=OFF",
548        ]
549
550        build_args = [f"-j{self.parallel}"]
551
552        # TODO(dbort): Try to manage these targets and the cmake args from the
553        # extension entries themselves instead of hard-coding them here.
554        build_args += ["--target", "flatc"]
555
556        if ShouldBuild.pybindings():
557            cmake_args += [
558                "-DEXECUTORCH_BUILD_PYBIND=ON",
559                "-DEXECUTORCH_BUILD_KERNELS_QUANTIZED=ON",  # add quantized ops to pybindings.
560                "-DEXECUTORCH_BUILD_KERNELS_QUANTIZED_AOT=ON",
561            ]
562            build_args += ["--target", "portable_lib"]
563            # To link backends into the portable_lib target, callers should
564            # add entries like `-DEXECUTORCH_BUILD_XNNPACK=ON` to the CMAKE_ARGS
565            # environment variable.
566
567        if ShouldBuild.llama_custom_ops():
568            cmake_args += [
569                "-DEXECUTORCH_BUILD_KERNELS_CUSTOM=ON",  # add llama sdpa ops to pybindings.
570                "-DEXECUTORCH_BUILD_KERNELS_CUSTOM_AOT=ON",
571                "-DEXECUTORCH_BUILD_KERNELS_QUANTIZED=ON",  # add quantized ops to pybindings.
572                "-DEXECUTORCH_BUILD_KERNELS_QUANTIZED_AOT=ON",
573            ]
574            build_args += ["--target", "custom_ops_aot_lib"]
575            build_args += ["--target", "quantized_ops_aot_lib"]
576        # Allow adding extra cmake args through the environment. Used by some
577        # tests and demos to expand the set of targets included in the pip
578        # package.
579        if "CMAKE_ARGS" in os.environ:
580            cmake_args += [item for item in os.environ["CMAKE_ARGS"].split(" ") if item]
581
582        # Allow adding extra build args through the environment. Used by some
583        # tests and demos to expand the set of targets included in the pip
584        # package.
585        if "CMAKE_BUILD_ARGS" in os.environ:
586            build_args += [
587                item for item in os.environ["CMAKE_BUILD_ARGS"].split(" ") if item
588            ]
589
590        # CMAKE_BUILD_TYPE variable specifies the build type (configuration) for
591        # single-configuration generators (e.g., Makefile Generators or Ninja).
592        # For multi-config generators (like Visual Studio), CMAKE_BUILD_TYPE
593        # isn’t directly applicable.
594        # During the build step, --config specifies the configuration to build
595        # for multi-config generators.
596        build_args += ["--config", cfg]
597
598        # Put the cmake cache under the temp directory, like
599        # "pip-out/temp.<plat>/cmake-out".
600        cmake_cache_dir = os.path.join(repo_root, self.build_temp, "cmake-out")
601        self.mkpath(cmake_cache_dir)
602
603        # Generate the cmake cache from scratch to ensure that the cache state
604        # is predictable.
605        cmake_cache_file = Path(cmake_cache_dir) / "CMakeCache.txt"
606        log.info(f"deleting {cmake_cache_file}")
607        if not self.dry_run:
608            # Dry run should log the command but not actually run it.
609            (Path(cmake_cache_dir) / "CMakeCache.txt").unlink(missing_ok=True)
610        with Buck2EnvironmentFixer():
611            # The context manager may patch the environment while running this
612            # cmake command, which happens to run buck2 to get some source
613            # lists.
614
615            # Generate the build system files.
616            self.spawn(["cmake", "-S", repo_root, "-B", cmake_cache_dir, *cmake_args])
617
618        # Build the system.
619        self.spawn(["cmake", "--build", cmake_cache_dir, *build_args])
620
621        # Non-python files should live under this data directory.
622        data_root = os.path.join(self.build_lib, "executorch", "data")
623
624        # Directories like bin/ and lib/ live under data/.
625        bin_dir = os.path.join(data_root, "bin")
626
627        # Copy the bin wrapper so that users can run any executables under
628        # data/bin, as long as they are listed in the [project.scripts] section
629        # of pyproject.toml.
630        self.mkpath(bin_dir)
631        self.copy_file(
632            "build/pip_data_bin_init.py.in",
633            os.path.join(bin_dir, "__init__.py"),
634        )
635        # Share the cmake-out location with _BaseExtension.
636        self.cmake_cache_dir = cmake_cache_dir
637
638        # Finally, run the underlying subcommands like build_py, build_ext.
639        build.run(self)
640
641
642def get_ext_modules() -> List[Extension]:
643    """Returns the set of extension modules to build."""
644    ext_modules = []
645    if ShouldBuild.flatc():
646        ext_modules.append(
647            BuiltFile(
648                src_dir="third-party/flatbuffers/%BUILD_TYPE%/",
649                src_name="flatc",
650                dst="executorch/data/bin/",
651                is_executable=True,
652            )
653        )
654
655    if ShouldBuild.pybindings():
656        ext_modules.append(
657            # Install the prebuilt pybindings extension wrapper for the runtime,
658            # portable kernels, and a selection of backends. This lets users
659            # load and execute .pte files from python.
660            BuiltExtension(
661                "_portable_lib.*", "executorch.extension.pybindings._portable_lib"
662            )
663        )
664    if ShouldBuild.llama_custom_ops():
665        ext_modules.append(
666            BuiltFile(
667                src_dir="extension/llm/custom_ops/%BUILD_TYPE%/",
668                src_name="custom_ops_aot_lib",
669                dst="executorch/extension/llm/custom_ops",
670                is_dynamic_lib=True,
671            )
672        )
673        ext_modules.append(
674            # Install the prebuilt library for quantized ops required by custom ops.
675            BuiltFile(
676                src_dir="kernels/quantized/%BUILD_TYPE%/",
677                src_name="quantized_ops_aot_lib",
678                dst="executorch/kernels/quantized/",
679                is_dynamic_lib=True,
680            )
681        )
682
683    # Note that setuptools uses the presence of ext_modules as the main signal
684    # that a wheel is platform-specific. If we install any platform-specific
685    # files, this list must be non-empty. Therefore, we should always install
686    # platform-specific files using InstallerBuildExt.
687    return ext_modules
688
689
690# Override extension suffix to be ".so", skipping package info such as
691# "cpython-311-darwin"
692os.environ["SETUPTOOLS_EXT_SUFFIX"] = ".so"
693
694setup(
695    version=Version.string(),
696    # TODO(dbort): Could use py_modules to restrict the set of modules we
697    # package, and package_data to restrict the set up non-python files we
698    # include. See also setuptools/discovery.py for custom finders.
699    package_dir={
700        "executorch/backends": "backends",
701        # TODO(mnachin T180504136): Do not put examples/models
702        # into core pip packages. Refactor out the necessary utils
703        # or core models files into a separate package.
704        "executorch/examples/models": "examples/models",
705        "executorch/exir": "exir",
706        "executorch/extension": "extension",
707        "executorch/kernels/quantized": "kernels/quantized",
708        "executorch/schema": "schema",
709        "executorch/devtools": "devtools",
710        "executorch/devtools/bundled_program": "devtools/bundled_program",
711        "executorch/runtime": "runtime",
712        "executorch/util": "util",
713        # Note: This will install a top-level module called "serializer",
714        # which seems too generic and might conflict with other pip packages.
715        "serializer": "backends/arm/third-party/serialization_lib/python/serializer",
716        "tosa": "backends/arm/third-party/serialization_lib/python/tosa",
717    },
718    cmdclass={
719        "build": CustomBuild,
720        "build_ext": InstallerBuildExt,
721        "build_py": CustomBuildPy,
722    },
723    ext_modules=get_ext_modules(),
724)
725