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