diff --git a/.ci/scripts/test_cpp_sdk_wheel.sh b/.ci/scripts/test_cpp_sdk_wheel.sh new file mode 100755 index 00000000000..8e874dab5e9 --- /dev/null +++ b/.ci/scripts/test_cpp_sdk_wheel.sh @@ -0,0 +1,226 @@ +#!/bin/bash +# Copyright (c) Meta Platforms, Inc. and affiliates. +# All rights reserved. +# +# This source code is licensed under the BSD-style license found in the +# LICENSE file in the root directory of this source tree. + +# Builds the Linux wheel with the prebuilt C++ SDK, installs it into a clean +# environment, and builds a standalone C++ program against it via +# find_package(executorch). This is the signal that the shipped +# libexecutorch.so, headers, and CMake package config actually let a C++ +# consumer link the ExecuTorch runtime with no source checkout, and that a +# separately built "coreless" backend shared library can register into the one +# runtime registry (the mechanism coalesced multi-backend execution relies on). + +set -euxo pipefail + +PYTHON_EXECUTABLE="${PYTHON_EXECUTABLE:-python}" +REPO_ROOT="$(cd "$(dirname "${BASH_SOURCE[0]}")/../.." && pwd)" +BUILD_VENV="${REPO_ROOT}/.venv-sdk-build" +TEST_VENV="${REPO_ROOT}/.venv-sdk-test" +WORK_DIR="${REPO_ROOT}/cpp_sdk_consumer" + +rm -rf "${BUILD_VENV}" "${TEST_VENV}" "${WORK_DIR}" "${REPO_ROOT}/dist" \ + "${REPO_ROOT}/pip-out" + +# --------------------------------------------------------------------------- +# Build the wheel. +# --------------------------------------------------------------------------- +"${PYTHON_EXECUTABLE}" -m venv "${BUILD_VENV}" +# shellcheck source=/dev/null +source "${BUILD_VENV}/bin/activate" +python -m pip install --upgrade pip +python -m pip install \ + "cmake>=3.24,<4.0.0" \ + "numpy>=2.0.0" \ + packaging \ + pyyaml \ + setuptools \ + wheel \ + zstd \ + certifi \ + torch \ + torchvision \ + --index-url https://download.pytorch.org/whl/cpu \ + --extra-index-url https://pypi.org/simple + +( + cd "${REPO_ROOT}" + python setup.py bdist_wheel +) + +WHEEL_FILE="$(find "${REPO_ROOT}/dist" -maxdepth 1 -name 'executorch-*.whl' | head -1)" +test -n "${WHEEL_FILE}" + +# --------------------------------------------------------------------------- +# Verify the SDK payload is present in the wheel. +# --------------------------------------------------------------------------- +python - "${WHEEL_FILE}" <<'PY' +import sys +import zipfile + +wheel_file = sys.argv[1] +with zipfile.ZipFile(wheel_file) as wheel: + names = set(wheel.namelist()) + +required = [ + "executorch/lib/libexecutorch.so", + "executorch/share/cmake/executorch-config.cmake", + "executorch/utils/__init__.py", + "executorch/include/executorch/runtime/executor/program.h", + "executorch/include/executorch/extension/module/module.h", +] +missing = [name for name in required if name not in names] +if missing: + raise AssertionError(f"{wheel_file} is missing SDK files: {missing}") + +# Headers whose implementation is not shipped must not be advertised. +forbidden = [ + "executorch/include/executorch/extension/module/bundled_module.h", + "executorch/include/executorch/extension/flat_tensor/serialize/serialize.h", +] +present = [name for name in forbidden if name in names] +if present: + raise AssertionError(f"{wheel_file} advertises unshipped APIs: {present}") + +print("SDK payload OK") +PY + +deactivate + +# --------------------------------------------------------------------------- +# Install into a clean environment and link a C++ consumer against it. +# --------------------------------------------------------------------------- +"${PYTHON_EXECUTABLE}" -m venv "${TEST_VENV}" +# shellcheck source=/dev/null +source "${TEST_VENV}/bin/activate" +python -m pip install --upgrade pip +python -m pip install "cmake>=3.24,<4.0.0" +# --no-deps: the C++ SDK link test does not need torch, proving the runtime is +# linkable standalone. (A plain install pulls declared deps; not needed here.) +python -m pip install --no-deps "${WHEEL_FILE}" + +CMAKE_PREFIX_PATH="$(python -c 'import executorch.utils as u; print(u.cmake_prefix_path)')" +export CMAKE_PREFIX_PATH + +mkdir -p "${WORK_DIR}" +cd "${WORK_DIR}" + +cat > main.cpp <<'CPP' +#include +#include +#include + +using executorch::runtime::get_backend_class; +using executorch::runtime::get_num_registered_backends; +using executorch::runtime::runtime_init; + +int main() { + runtime_init(); + printf("registered backends: %zu\n", get_num_registered_backends()); + // The delegate-only SDK ships no backends, so this must be null. What matters + // is that the symbol links and resolves from libexecutorch.so. + printf("stock backend lookup resolves: %d\n", + get_backend_class("NonexistentBackend") == nullptr); + printf("PASS: linked executorch::runtime from the wheel\n"); + return 0; +} +CPP + +cat > CMakeLists.txt <<'CMAKE' +cmake_minimum_required(VERSION 3.24) +project(executorch_cpp_sdk_consumer LANGUAGES CXX) +set(CMAKE_CXX_STANDARD 17) +find_package(executorch CONFIG REQUIRED) +if(NOT EXECUTORCH_SDK_FOUND) + message(FATAL_ERROR "EXECUTORCH_SDK_FOUND is false; the wheel C++ SDK is missing") +endif() +add_executable(consumer main.cpp) +target_link_libraries(consumer PRIVATE executorch::runtime) +CMAKE + +cmake -S . -B build +cmake --build build +./build/consumer + +# The runtime and the consumer must not pull in libtorch. +if ldd ./build/consumer | grep -Eiq "libtorch|libc10"; then + echo "ERROR: consumer unexpectedly links libtorch/libc10" >&2 + exit 1 +fi + +# --------------------------------------------------------------------------- +# Prove cross-shared-object registration: a "coreless" backend .so (no bundled +# runtime, register_backend undefined) registers into the runtime inside +# libexecutorch.so when loaded. This is the mechanism coalesced multi-backend +# execution depends on. +# --------------------------------------------------------------------------- +cat > mybackend.cpp <<'CPP' +#include +using namespace executorch::runtime; +namespace { +struct MyBackend final : public BackendInterface { + bool is_available() const override { return true; } + Result init(BackendInitContext&, FreeableBuffer*, + ArrayRef) const override { + return nullptr; + } + Error execute(BackendExecutionContext&, DelegateHandle*, + Span) const override { + return Error::Ok; + } + void destroy(DelegateHandle*) const override {} +}; +MyBackend g_backend; +Backend g_id{"MyTestBackend", &g_backend}; +static auto g_registered = register_backend(g_id); +} // namespace +CPP + +cat >> CMakeLists.txt <<'CMAKE' +# Coreless: undefined ExecuTorch symbols resolve from libexecutorch.so at load. +add_library(mybackend SHARED mybackend.cpp) +target_link_options(mybackend PRIVATE "LINKER:--unresolved-symbols=ignore-all") +target_include_directories(mybackend PRIVATE + $) +target_compile_definitions(mybackend PRIVATE C10_USING_CUSTOM_GENERATED_MACROS) + +add_executable(reg_consumer reg_main.cpp) +target_link_libraries(reg_consumer PRIVATE executorch::runtime ${CMAKE_DL_LIBS}) +CMAKE + +cat > reg_main.cpp <<'CPP' +#include +#include +#include +#include + +using executorch::runtime::get_backend_class; +using executorch::runtime::runtime_init; + +int main(int argc, char** argv) { + runtime_init(); + if (get_backend_class("MyTestBackend") != nullptr) { + fprintf(stderr, "backend registered before load\n"); + return 1; + } + void* handle = dlopen(argv[1], RTLD_NOW | RTLD_GLOBAL); + if (handle == nullptr) { + fprintf(stderr, "dlopen failed: %s\n", dlerror()); + return 1; + } + if (get_backend_class("MyTestBackend") == nullptr) { + fprintf(stderr, "backend NOT registered after load\n"); + return 1; + } + printf("PASS: coreless backend .so registered into libexecutorch.so\n"); + return 0; +} +CPP + +cmake -S . -B build +cmake --build build +./build/reg_consumer "$(find build -name 'libmybackend.so' | head -1)" + +echo "ALL C++ SDK WHEEL CHECKS PASSED" diff --git a/.github/workflows/pull.yml b/.github/workflows/pull.yml index 3ead9e6a49c..27c2906c9ff 100644 --- a/.github/workflows/pull.yml +++ b/.github/workflows/pull.yml @@ -116,6 +116,29 @@ jobs: # Build and test ExecuTorch with the add model on portable backend. PYTHON_EXECUTABLE=python bash .ci/scripts/test_model.sh "add" "${BUILD_TOOL}" "portable" + test-cpp-sdk-wheel-linux: + name: test-cpp-sdk-wheel-linux + uses: pytorch/test-infra/.github/workflows/linux_job_v2.yml@main + permissions: + id-token: write + contents: read + strategy: + fail-fast: false + with: + runner: linux.2xlarge + docker-image: ci-image:executorch-ubuntu-22.04-gcc11 + submodules: 'recursive' + ref: ${{ github.event_name == 'pull_request' && github.event.pull_request.head.sha || github.sha }} + timeout: 90 + script: | + # The generic Linux job chooses to use base env, not the one setup by the image + CONDA_ENV=$(conda env list --json | jq -r ".envs | .[-1]") + conda activate "${CONDA_ENV}" + + # Build the wheel, install it clean, and link a C++ consumer against the + # shipped libexecutorch.so via find_package(executorch). + PYTHON_EXECUTABLE=python bash .ci/scripts/test_cpp_sdk_wheel.sh + test-models-linux-basic: name: test-models-linux-basic uses: pytorch/test-infra/.github/workflows/linux_job_v2.yml@main diff --git a/setup.py b/setup.py index 85228bd37ae..4dcfcf904d7 100644 --- a/setup.py +++ b/setup.py @@ -320,6 +320,27 @@ def get_dynamic_lib_name(name: str) -> str: return f"lib{name}.so" +def _read_soname(lib_path: Path) -> Optional[str]: + """Return the ELF SONAME of a shared library, or None if unavailable. + + Used to recreate the SONAME symlink for a versioned .so in the wheel. Best + effort: any failure (non-ELF, no readelf, non-Linux) returns None. + """ + try: + out = subprocess.run( + ["readelf", "-d", os.fspath(lib_path)], + capture_output=True, + text=True, + check=True, + ).stdout + except Exception: + return None + for line in out.splitlines(): + if "SONAME" in line and "[" in line and "]" in line: + return line[line.index("[") + 1 : line.index("]")] + return None + + def get_executable_name(name: str) -> str: if _is_windows(): return name + ".exe" @@ -508,6 +529,43 @@ def inplace_dir(self, installer: "InstallerBuildExt") -> Path: return Path(package_dir) +class BuiltSharedLib(BuiltFile): + """Installs a SONAME-versioned shared library plus its symlink chain. + + A normal ``BuiltFile`` copies one file. A shared library like + ``libexecutorch.so.1.4.0`` also needs the loader-visible SONAME symlink + (``libexecutorch.so.1``) and the developer symlink (``libexecutorch.so``), + or a consumer that links ``-lexecutorch`` fails at runtime because the + SONAME recorded in dependents cannot be found. This recreates that chain in + the wheel, matching a standard ``cmake --install`` layout. + """ + + def __init__(self, src_dir: str, src_name: str, dst: str): + # src_name is the base library name (e.g. "executorch"); the real file + # is libexecutorch.so., resolved by glob in src_path(). + super().__init__( + src_dir=src_dir, + src_name=f"lib{src_name}.so.*", + dst=dst, + dependent_cmake_flags=[], + ) + + def src_path(self, installer: "InstallerBuildExt") -> Path: + # The glob matches the versioned real file and any symlinks; pick the + # regular file (the real library), not the symlinks. + build_dir = self._get_build_dir(installer) + pattern = self.src.replace("%CMAKE_CACHE_DIR%/", "") + matches = [ + p for p in build_dir.glob(pattern) if p.is_file() and not p.is_symlink() + ] + if len(matches) != 1: + raise ValueError( + f"Expecting exactly 1 real shared library matching {self.src} " + f"in {build_dir}, found {matches}." + ) + return matches[0] + + class BuiltExtension(_BaseExtension): """An extension that installs a python extension that was built by cmake.""" @@ -663,6 +721,31 @@ def build_extension(self, ext: _BaseExtension) -> None: # Copy the file. self.copy_file(os.fspath(src_file), os.fspath(dst_file)) + # For a SONAME-versioned shared library, also recreate the symlink chain + # (libexecutorch.so -> libexecutorch.so. -> libexecutorch.so.) + # so `-lexecutorch` links and the SONAME resolves at load time. + if isinstance(ext, BuiltSharedLib): + self._create_soname_symlinks(src_file, dst_file) + + def _create_soname_symlinks(self, src_file: Path, dst_file: Path) -> None: + real_name = dst_file.name # e.g. libexecutorch.so.1.4.0 + link_dir = dst_file.parent + # SONAME (e.g. libexecutorch.so.1) read from the built library; fall back + # to none if unreadable. The dev symlink drops all version suffixes. + links = set() + soname = _read_soname(src_file) + if soname and soname != real_name: + links.add(soname) + dev_name = real_name.split(".so")[0] + ".so" + if dev_name != real_name: + links.add(dev_name) + for link_name in links: + link_path = link_dir / link_name + if link_path.exists() or link_path.is_symlink(): + link_path.unlink() + # Relative link so the wheel is relocatable. + os.symlink(real_name, os.fspath(link_path)) + # Ensure that the destination file is writable, even if the source was # not. build_py does this by passing preserve_mode=False to copy_file, # but that would clobber the X bit on any executables. TODO(dbort): This @@ -749,6 +832,38 @@ def run(self): src_to_dst.append( (str(src), os.path.join("include/executorch", str(src))) ) + # Delegate-only C++ SDK headers: the Program/Module/Tensor/DataLoader/ + # .ptd APIs a standalone C++ runner needs, so it can link the prebuilt + # runtime without an ExecuTorch source tree. These are listed + # explicitly (not rglob) so we only advertise APIs whose implementation + # archive is actually shipped in executorch/lib/. Excluded on purpose: + # bundled_module.h (needs the separate bundled_module archive), + # flat_tensor/serialize/serialize.h (serializer .cpp not shipped), + # file_descriptor_data_loader.h (impl not in the shipped archive), and + # cpu_caching_malloc_allocator.h (needs a memory_allocator archive we + # do not ship). flat_tensor_header.h IS shipped: it is the .ptd reader. + # Linux only, matching the SDK archives below; keeps Windows/macOS + # wheels unchanged. + sdk_headers = ( + [ + "extension/module/module.h", + "extension/data_loader/buffer_data_loader.h", + "extension/data_loader/file_data_loader.h", + "extension/data_loader/mmap_data_loader.h", + "extension/data_loader/mman.h", + "extension/data_loader/mman_windows.h", + "extension/data_loader/shared_ptr_data_loader.h", + "extension/flat_tensor/flat_tensor_data_map.h", + "extension/flat_tensor/serialize/flat_tensor_header.h", + "extension/named_data_map/merged_data_map.h", + "extension/memory_allocator/malloc_memory_allocator.h", + "extension/memory_allocator/memory_allocator_utils.h", + ] + if sys.platform == "linux" + else [] + ) + for src in sdk_headers: + src_to_dst.append((src, os.path.join("include/executorch", src))) for src, dst in src_to_dst: dst = os.path.join(dst_root, dst) @@ -900,6 +1015,12 @@ def run(self): # noqa C901 ): cmake_configuration_args += ["-DEXECUTORCH_BUILD_OPENVINO=ON"] + # Build the consolidated shared runtime libexecutorch.so so the wheel can + # ship a linkable C++ SDK (see the executorch_shared build target and the + # packaging step). Linux only; the SDK is not shipped on Windows/macOS. + if not minimal_build and sys.platform == "linux": + cmake_configuration_args += ["-DEXECUTORCH_BUILD_SHARED=ON"] + with Buck2EnvironmentFixer(): # Generate the cmake cache from scratch to ensure that the cache state # is predictable. @@ -954,6 +1075,16 @@ def run(self): # noqa C901 # list explicitly rather than relying on each flag being OFF. cmake_build_args += ["--target", "flatbuffers_ep"] else: + # Delegate-only C++ SDK: ship the consolidated shared runtime + # libexecutorch.so (the executorch_shared target), which bundles the + # runtime core plus the common extensions (module, tensor, + # data_loader, flat_tensor, named_data_map). Shared is required so a + # separately distributed backend/delegate .so can register into the + # one process-global registry inside libexecutorch.so. Build it + # explicitly, Linux only, so packaging below always finds it. + if sys.platform == "linux": + cmake_build_args += ["--target", "executorch_shared"] + if cmake_cache.is_enabled("EXECUTORCH_BUILD_PYBIND"): cmake_build_args += ["--target", "portable_lib"] cmake_build_args += ["--target", "data_loader"] @@ -1125,6 +1256,28 @@ def run(self): # noqa C901 modpath="executorch.backends.qualcomm.python.PyQnnManagerAdaptor", dependent_cmake_flags=["EXECUTORCH_BUILD_QNN"], ), + # Delegate-only C++ SDK: the consolidated shared runtime + # libexecutorch.so (core + module/tensor/data_loader/flat_tensor/ + # named_data_map), plus its SONAME symlink chain. Shipping it + # shared (not static archives) lets a separately distributed + # backend/delegate .so register into the one process-global + # registry inside libexecutorch.so, which is what coalesced + # multi-backend .pte execution requires. Paired with + # executorch-config.cmake (executorch::runtime target) and + # executorch.utils.cmake_prefix_path. Linux only: the .so naming + # and symlink chain are Unix specific, so Windows/macOS wheels are + # unchanged. + *( + [ + BuiltSharedLib( + src_dir="%CMAKE_CACHE_DIR%/", + src_name="executorch", + dst="executorch/lib/", + ) + ] + if sys.platform == "linux" + else [] + ), ] ), ], diff --git a/src/executorch/utils/__init__.py b/src/executorch/utils/__init__.py new file mode 100644 index 00000000000..3d5919dcc5c --- /dev/null +++ b/src/executorch/utils/__init__.py @@ -0,0 +1,23 @@ +# Copyright (c) Meta Platforms, Inc. and affiliates. +# All rights reserved. +# +# This source code is licensed under the BSD-style license found in the +# LICENSE file in the root directory of this source tree. + +"""Utilities for locating ExecuTorch's packaged assets. + +``cmake_prefix_path`` points at the directory that contains the installed +ExecuTorch CMake package config, so a C++ project can discover it with: + + cmake -DCMAKE_PREFIX_PATH="$(python -c 'import executorch.utils as u; print(u.cmake_prefix_path)')" +""" + +import os as _os + +# Mirror torch.utils.cmake_prefix_path: /share/cmake. This file +# lives at /utils/__init__.py, so go up one level. +cmake_prefix_path = _os.path.join( + _os.path.dirname(_os.path.dirname(__file__)), "share", "cmake" +) + +__all__ = ["cmake_prefix_path"] diff --git a/tools/cmake/executorch-wheel-config.cmake b/tools/cmake/executorch-wheel-config.cmake index 1d6096a2e96..de53a5b12bd 100644 --- a/tools/cmake/executorch-wheel-config.cmake +++ b/tools/cmake/executorch-wheel-config.cmake @@ -19,10 +19,24 @@ # EXECUTORCH_INCLUDE_DIRS -- The include directories for ExecuTorch # EXECUTORCH_LIBRARIES -- Libraries to link against # -cmake_minimum_required(VERSION 3.19) +# In addition to the legacy variables above, this config defines namespaced +# imported targets for the prebuilt delegate-only C++ SDK when the corresponding +# static libraries are shipped in the wheel (see the "C++ SDK targets" section +# below): +# +# executorch::core -- executorch_core (runtime, no ops) +# executorch::runtime -- executorch (adds primitive ops) +# executorch::extension_data_loader executorch::extension_flat_tensor +# executorch::extension_named_data_map executorch::extension_tensor +# executorch::extension_module +# +cmake_minimum_required(VERSION 3.24) -# Find prebuilt _portable_lib..so. This file should be installed -# under /executorch/share/cmake +# --------------------------------------------------------------------------- +# Legacy: discover the CPython _portable_lib extension for custom-op authors. +# This keeps `find_package(executorch)` working for prebuilt custom-op +# extensions that link the Python runtime module, unchanged from before. +# --------------------------------------------------------------------------- # Find python if(DEFINED ENV{CONDA_DEFAULT_ENV} AND NOT $ENV{CONDA_DEFAULT_ENV} STREQUAL @@ -43,36 +57,119 @@ execute_process( OUTPUT_STRIP_TRAILING_WHITESPACE ) +set(EXECUTORCH_INCLUDE_DIRS + "${CMAKE_CURRENT_LIST_DIR}/../../include" + "${CMAKE_CURRENT_LIST_DIR}/../../include/executorch/runtime/core/portable_type/c10" +) +set(EXECUTORCH_LIBRARIES) +set(EXECUTORCH_FOUND OFF) + +# Only discover the portable Python module when we could read EXT_SUFFIX; +# probing with an empty suffix would match a wrong/generic file. A missing +# suffix is not fatal because a pure-C++ consumer (see the C++ SDK section +# below) does not need the Python extension at all. if(SYSCONFIG_RESULT EQUAL 0) message(STATUS "Sysconfig extension suffix: ${EXT_SUFFIX}") + find_library( + _portable_lib_LIBRARY + NAMES _portable_lib${EXT_SUFFIX} + PATHS "${CMAKE_CURRENT_LIST_DIR}/../../extension/pybindings/" + ) else() message( - FATAL_ERROR - "Failed to retrieve sysconfig config var EXT_SUFFIX: ${SYSCONFIG_ERROR}" + WARNING + "Failed to retrieve sysconfig config var EXT_SUFFIX: ${SYSCONFIG_ERROR}. " + "The _portable_lib Python runtime target will not be available; the C++ " + "SDK targets (executorch::*) are unaffected." ) endif() -find_library( - _portable_lib_LIBRARY - NAMES _portable_lib${EXT_SUFFIX} - PATHS "${CMAKE_CURRENT_LIST_DIR}/../../extension/pybindings/" -) - -set(EXECUTORCH_LIBRARIES) -set(EXECUTORCH_FOUND OFF) if(_portable_lib_LIBRARY) set(EXECUTORCH_FOUND ON) message( STATUS "ExecuTorch portable library is found at ${_portable_lib_LIBRARY}" ) list(APPEND EXECUTORCH_LIBRARIES _portable_lib) - add_library(_portable_lib STATIC IMPORTED) - set(EXECUTORCH_INCLUDE_DIRS ${CMAKE_CURRENT_LIST_DIR}/../../include) - # PyTorch requires C++20, so pybindings must be compiled with C++20. - set_target_properties( - _portable_lib - PROPERTIES IMPORTED_LOCATION "${_portable_lib_LIBRARY}" - INTERFACE_INCLUDE_DIRECTORIES "${EXECUTORCH_INCLUDE_DIRS}" - CXX_STANDARD 20 + if(NOT TARGET _portable_lib) + add_library(_portable_lib STATIC IMPORTED) + # PyTorch requires C++20, so pybindings must be compiled with C++20. + set_target_properties( + _portable_lib + PROPERTIES IMPORTED_LOCATION "${_portable_lib_LIBRARY}" + INTERFACE_INCLUDE_DIRECTORIES "${EXECUTORCH_INCLUDE_DIRS}" + CXX_STANDARD 20 + ) + endif() +endif() + +# --------------------------------------------------------------------------- +# C++ SDK targets (delegate-only). Defined only when the prebuilt static +# archives are present in the wheel (they are shipped alongside this config +# under ../../lib). This lets a C++ application link the ExecuTorch runtime and +# the common runtime extensions without an ExecuTorch source checkout: +# +# find_package(executorch REQUIRED) target_link_libraries(app PRIVATE +# executorch::runtime executorch::extension_module executorch::extension_tensor) +# +# The set is intentionally libtorch-free and excludes CPU operator/kernel +# libraries; delegates (e.g. TensorRT, CUDA) supply their own compute. If your +# model needs portable CPU operators, link a kernel library in addition. +# --------------------------------------------------------------------------- + +get_filename_component( + _executorch_sdk_root "${CMAKE_CURRENT_LIST_DIR}/../.." ABSOLUTE +) +set(_executorch_sdk_libdir "${_executorch_sdk_root}/lib") + +# EXECUTORCH_SDK_FOUND is separate from EXECUTORCH_FOUND on purpose: the legacy +# EXECUTORCH_FOUND / EXECUTORCH_LIBRARIES contract describes the _portable_lib +# Python runtime for custom-op authors. Overloading it here would let existing +# `if(EXECUTORCH_FOUND) link(${EXECUTORCH_LIBRARIES})` code enter its branch +# with an empty library list. C++ SDK consumers should check the imported target +# (e.g. `if(TARGET executorch::runtime)`) or EXECUTORCH_SDK_FOUND. +# +# The C++ SDK ships one shared library, libexecutorch.so, which bundles the +# runtime core plus the common runtime extensions (module, tensor, data_loader, +# flat_tensor, named_data_map). Shared (not static archives) is required so that +# a separately distributed backend/delegate shared library can register into the +# one process-global registry that lives in libexecutorch.so. A backend .so is +# built "coreless" (its register_backend reference is undefined and resolves +# against libexecutorch.so at load), then force-loaded so its static-init +# registration runs. +set(EXECUTORCH_SDK_FOUND OFF) +find_library( + _executorch_shared_LIBRARY + NAMES executorch + PATHS "${_executorch_sdk_libdir}" + NO_DEFAULT_PATH +) +if(_executorch_shared_LIBRARY) + set(EXECUTORCH_SDK_FOUND ON) + if(NOT TARGET executorch::runtime) + add_library(executorch::runtime SHARED IMPORTED) + set_target_properties( + executorch::runtime + PROPERTIES IMPORTED_LOCATION "${_executorch_shared_LIBRARY}" + INTERFACE_INCLUDE_DIRECTORIES "${EXECUTORCH_INCLUDE_DIRS}" + INTERFACE_COMPILE_FEATURES cxx_std_17 + INTERFACE_COMPILE_DEFINITIONS + "C10_USING_CUSTOM_GENERATED_MACROS" + ) + endif() + + # Convenience aliases. libexecutorch.so already contains the core and these + # extensions, so all names resolve to the one shared library. Provided so + # consumer CMake can name what it uses without depending on the bundling + # layout. + foreach(_alias core extension_module extension_tensor extension_data_loader + extension_flat_tensor extension_named_data_map ) + if(NOT TARGET executorch::${_alias}) + add_library(executorch::${_alias} INTERFACE IMPORTED) + set_property( + TARGET executorch::${_alias} PROPERTY INTERFACE_LINK_LIBRARIES + executorch::runtime + ) + endif() + endforeach() endif()