diff --git a/.ci/scripts/wheel/test_cpp_sdk.py b/.ci/scripts/wheel/test_cpp_sdk.py new file mode 100644 index 00000000000..6769ca40143 --- /dev/null +++ b/.ci/scripts/wheel/test_cpp_sdk.py @@ -0,0 +1,227 @@ +# 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. + +"""Checks that the installed wheel is usable as a C++ SDK. + +The wheel ships a prebuilt runtime library plus a CMake package config, so a +standalone application can find_package(executorch) and link +executorch::runtime without building ExecuTorch from source. These checks run +against the installed wheel only; they never look at the source tree's build +directory. + +Two properties are verified: + +1. Exactly one shipped library defines the backend registry. Backends register + into a process-wide table owned by the runtime, so a second definition would + silently give the process two tables and let a backend register into the one + nobody reads. +2. A C++ consumer builds and runs against the wheel, and records a dependency + on the shipped runtime with a relocatable RUNPATH. +""" + +import os +import re +import shutil +import subprocess +from pathlib import Path + +# Registry entry points. A second definer of any of these means a second +# process-wide registry. +_REGISTRY_SYMBOLS = ( + "executorch::runtime::register_backend", + "executorch::runtime::get_num_registered_backends", + "executorch::runtime::get_backend_class", +) + +# `nm -DC` prints " " for a definition and +# " U " for an undefined reference. +_DEFINED = re.compile(r"^[0-9a-fA-F]+\s+(?P[A-Za-z])\s+(?P.+)$") + +# Symbol kinds that mean the object owns the code or storage. +_OWNING_KINDS = frozenset("TtBbDdGgSsRrWV") + +_CONSUMER_SOURCE = """\ +#include +#include + +#include + +int main() { + executorch::runtime::runtime_init(); + std::printf( + "registered backends: %zu\\n", + (size_t)executorch::runtime::get_num_registered_backends()); + return 0; +} +""" + +_CONSUMER_CMAKE = """\ +cmake_minimum_required(VERSION 3.24) +project(executorch_wheel_consumer CXX) +find_package(executorch REQUIRED) +add_executable(consumer consumer.cpp) +target_link_libraries(consumer PRIVATE executorch::runtime) +""" + + +def _installed_package_dir() -> Path: + """The installed executorch package, never the source checkout.""" + import executorch + + return Path(list(executorch.__path__)[0]).resolve() + + +def _shipped_shared_objects(package_dir: Path): + return [ + path + for path in sorted(package_dir.rglob("*.so*")) + if path.is_file() and not path.is_symlink() + ] + + +def _defines_symbol(library: Path, symbol: str) -> bool: + result = subprocess.run( + ["nm", "-DC", str(library)], capture_output=True, text=True, check=False + ) + # A library nm cannot read would otherwise look like one that simply defines + # nothing, letting the single-definer checks pass without having actually + # inspected every shipped library. + assert result.returncode == 0, ( + f"nm could not read {library}, so the symbol checks cannot be trusted: " + f"{result.stderr.strip()}" + ) + for line in result.stdout.splitlines(): + if symbol not in line: + continue + match = _DEFINED.match(line) + if ( + match + and match.group("name").startswith(symbol) + and match.group("kind") in _OWNING_KINDS + ): + return True + return False + + +def test_single_backend_registry() -> None: + """Exactly one shipped library may define the backend registry.""" + assert shutil.which("nm") is not None, "nm is required to inspect the wheel" + + package_dir = _installed_package_dir() + libraries = _shipped_shared_objects(package_dir) + assert libraries, f"no shared libraries found under {package_dir}" + + for symbol in _REGISTRY_SYMBOLS: + definers = [lib for lib in libraries if _defines_symbol(lib, symbol)] + pretty = [str(lib.relative_to(package_dir)) for lib in definers] + assert len(definers) == 1, ( + f"expected exactly one library to define {symbol}, found " + f"{len(definers)}: {pretty}. More than one definition means the " + f"process has more than one backend registry." + ) + print(f"✓ single backend registry across {len(libraries)} shipped libraries") + + +def test_cpp_consumer(work_dir: Path) -> None: + """A standalone C++ app builds and runs against the installed wheel.""" + assert shutil.which("cmake") is not None, "cmake is required to build a consumer" + + package_dir = _installed_package_dir() + config = package_dir / "share" / "cmake" / "executorch-config.cmake" + assert config.is_file(), f"wheel is missing its CMake package config: {config}" + + source_dir = work_dir / "consumer" + build_dir = work_dir / "consumer-build" + source_dir.mkdir(parents=True, exist_ok=True) + (source_dir / "consumer.cpp").write_text(_CONSUMER_SOURCE) + (source_dir / "CMakeLists.txt").write_text(_CONSUMER_CMAKE) + + subprocess.run( + [ + "cmake", + "-S", + str(source_dir), + "-B", + str(build_dir), + f"-DCMAKE_PREFIX_PATH={config.parent}", + ], + check=True, + ) + subprocess.run(["cmake", "--build", str(build_dir)], check=True) + + consumer = build_dir / "consumer" + # No LD_LIBRARY_PATH: the imported target is responsible for making the + # shipped runtime findable. + environment = { + key: value for key, value in os.environ.items() if key != "LD_LIBRARY_PATH" + } + subprocess.run([str(consumer)], check=True, env=environment) + print("✓ C++ consumer builds and runs against the installed wheel") + + _assert_runs_relocated(consumer, package_dir, work_dir, environment) + + assert shutil.which("readelf") is not None, "readelf is required to check the ELF" + + dynamic = subprocess.run( + ["readelf", "-d", str(consumer)], capture_output=True, text=True, check=True + ).stdout + assert "libexecutorch.so" in dynamic, ( + "the consumer does not depend on the shipped runtime; " + f"dynamic section was:\n{dynamic}" + ) + assert "$ORIGIN" in dynamic, ( + "the consumer has no $ORIGIN-relative RUNPATH, so it is not " + f"relocatable; dynamic section was:\n{dynamic}" + ) + print("✓ consumer depends on the shipped runtime with a relocatable RUNPATH") + + +def _assert_runs_relocated(consumer, package_dir, work_dir, environment) -> None: + """The app still runs after being moved away from the wheel. + + Building in place leaves an absolute path to the wheel's lib directory in the + binary's RUNPATH, which resolves the runtime no matter what `$ORIGIN` says. + Copying the app next to a copy of the runtime, with that absolute entry + removed, is what actually proves the package is relocatable. + + The layout mirrors what the package config supports: the app in `bin/` with + the libraries in a sibling `lib/`, which is what `$ORIGIN/../lib` resolves. + """ + if shutil.which("patchelf") is None: + print("- patchelf not available, skipping the relocated run") + return + + deploy = work_dir / "deployed" + (deploy / "bin").mkdir(parents=True, exist_ok=True) + (deploy / "lib").mkdir(parents=True, exist_ok=True) + moved = deploy / "bin" / consumer.name + shutil.copy2(consumer, moved) + for library in (package_dir / "lib").glob("*.so*"): + shutil.copy2(library, deploy / "lib" / library.name) + + # Keep only the $ORIGIN-relative entries, so nothing absolute can help. + current = subprocess.run( + ["patchelf", "--print-rpath", str(moved)], + capture_output=True, + text=True, + check=True, + ).stdout.strip() + relative = [entry for entry in current.split(":") if entry.startswith("$ORIGIN")] + assert relative, ( + "the consumer has no $ORIGIN-relative RUNPATH entry, so it cannot be " + f"relocated; RUNPATH was: {current}" + ) + subprocess.run( + ["patchelf", "--set-rpath", ":".join(relative), str(moved)], check=True + ) + + subprocess.run([str(moved)], check=True, env=environment, cwd=str(deploy)) + print("✓ consumer still runs when deployed beside a copy of the runtime") + + +def run_tests(work_dir: Path) -> None: + test_single_backend_registry() + test_cpp_consumer(work_dir) diff --git a/.ci/scripts/wheel/test_linux.py b/.ci/scripts/wheel/test_linux.py index 812eec89215..532eb850d94 100644 --- a/.ci/scripts/wheel/test_linux.py +++ b/.ci/scripts/wheel/test_linux.py @@ -7,8 +7,11 @@ # LICENSE file in the root directory of this source tree. import platform +import tempfile +from pathlib import Path import test_base +import test_cpp_sdk from examples.models import Backend, Model if __name__ == "__main__": @@ -41,6 +44,12 @@ test_base.test_cmsis_nn_install() + # The wheel ships a prebuilt C++ runtime and a CMake package config, so + # check that a standalone application can actually link and run against + # them, and that the process still has a single backend registry. + with tempfile.TemporaryDirectory() as work_dir: + test_cpp_sdk.run_tests(Path(work_dir)) + test_base.run_tests( model_tests=[ test_base.ModelTest( diff --git a/.ci/scripts/wheel/test_linux_aarch64.py b/.ci/scripts/wheel/test_linux_aarch64.py index c0cca95b3fb..db7dd28f81e 100644 --- a/.ci/scripts/wheel/test_linux_aarch64.py +++ b/.ci/scripts/wheel/test_linux_aarch64.py @@ -5,7 +5,11 @@ # This source code is licensed under the BSD-style license found in the # LICENSE file in the root directory of this source tree. +import tempfile +from pathlib import Path + import test_base +import test_cpp_sdk from examples.models import Backend, Model if __name__ == "__main__": @@ -26,6 +30,12 @@ ), f"OpenvinoBackend not found in registered backends: {registered}" print("✓ OpenvinoBackend is registered") + # The wheel ships a prebuilt C++ runtime and a CMake package config, so check + # that a standalone application can actually link and run against them, and + # that the process still has a single backend registry. + with tempfile.TemporaryDirectory() as work_dir: + test_cpp_sdk.run_tests(Path(work_dir)) + test_base.run_tests( model_tests=[ test_base.ModelTest( diff --git a/.github/workflows/build-wheels-aarch64-linux.yml b/.github/workflows/build-wheels-aarch64-linux.yml index b0b9a9c0fee..8adf4268228 100644 --- a/.github/workflows/build-wheels-aarch64-linux.yml +++ b/.github/workflows/build-wheels-aarch64-linux.yml @@ -6,9 +6,11 @@ on: paths: - .ci/**/* - .github/workflows/build-wheels-aarch64-linux.yml + - '**/CMakeLists.txt' - examples/**/* - pyproject.toml - setup.py + - tools/cmake/**/* push: branches: - nightly diff --git a/.github/workflows/build-wheels-linux.yml b/.github/workflows/build-wheels-linux.yml index 1a89079e428..7428b68a773 100644 --- a/.github/workflows/build-wheels-linux.yml +++ b/.github/workflows/build-wheels-linux.yml @@ -6,9 +6,11 @@ on: paths: - .ci/**/* - .github/workflows/build-wheels-linux.yml + - '**/CMakeLists.txt' - examples/**/* - pyproject.toml - setup.py + - tools/cmake/**/* push: branches: - nightly diff --git a/.github/workflows/build-wheels-macos.yml b/.github/workflows/build-wheels-macos.yml index 3fddb8e6d26..6ace109edf7 100644 --- a/.github/workflows/build-wheels-macos.yml +++ b/.github/workflows/build-wheels-macos.yml @@ -6,9 +6,11 @@ on: paths: - .ci/**/* - .github/workflows/build-wheels-macos.yml + - '**/CMakeLists.txt' - examples/**/* - pyproject.toml - setup.py + - tools/cmake/**/* push: branches: - nightly diff --git a/.github/workflows/build-wheels-windows.yml b/.github/workflows/build-wheels-windows.yml index 9b1f8663bd2..60c6520b944 100644 --- a/.github/workflows/build-wheels-windows.yml +++ b/.github/workflows/build-wheels-windows.yml @@ -5,9 +5,11 @@ on: paths: - .ci/**/* - .github/workflows/build-wheels-windows.yml + - '**/CMakeLists.txt' - examples/**/* - pyproject.toml - setup.py + - tools/cmake/**/* push: branches: - nightly diff --git a/CMakeLists.txt b/CMakeLists.txt index ff3b9e86f7e..a3cedecbdb3 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -778,11 +778,6 @@ if(EXECUTORCH_BUILD_OPENVINO) list(APPEND _executorch_backends openvino_backend) endif() -if(EXECUTORCH_BUILD_QNN) - add_subdirectory(${CMAKE_CURRENT_SOURCE_DIR}/backends/qualcomm) - list(APPEND _executorch_backends qnn_executorch_backend) -endif() - if(EXECUTORCH_BUILD_ENN) add_subdirectory(${CMAKE_CURRENT_SOURCE_DIR}/backends/samsung) list(APPEND _executorch_backends enn_backend) @@ -932,6 +927,62 @@ if(EXECUTORCH_BUILD_PTHREADPOOL AND EXECUTORCH_BUILD_CPUINFO) add_subdirectory(${CMAKE_CURRENT_SOURCE_DIR}/extension/threadpool) endif() +# Consolidated shared library: bundles executorch_core plus commonly used +# extensions into a single libexecutorch.so. Defined before the pybind and +# kernel targets below so they can link this one runtime instead of embedding a +# private copy of the core, which would give the process a second backend +# registry. +if(EXECUTORCH_BUILD_SHARED) + executorch_add_shared_library(executorch_shared) + set_target_properties( + executorch_shared + PROPERTIES OUTPUT_NAME executorch + ARCHIVE_OUTPUT_NAME executorch_shared + EXPORT_NAME executorch-shared + ) + target_include_directories( + executorch_shared PUBLIC ${_common_include_directories} + ) + target_compile_definitions( + executorch_shared PUBLIC C10_USING_CUSTOM_GENERATED_MACROS + ) + # Link executorch without WHOLE_ARCHIVE because its INTERFACE link options + # (from executorch_target_link_options_shared_lib) already force + # whole-archive. Everything else is pulled in through link options rather than + # the WHOLE_ARCHIVE link feature, because these archives also reference each + # other plainly and CMake before 3.29 refuses to mix a feature with a plain + # reference to the same item. + target_link_libraries(executorch_shared PRIVATE executorch) + set(_executorch_shared_whole_archive executorch_core) + foreach(_ext_target + extension_data_loader extension_flat_tensor extension_named_data_map + extension_module_static extension_tensor + ) + if(TARGET ${_ext_target}) + list(APPEND _executorch_shared_whole_archive ${_ext_target}) + endif() + endforeach() + foreach(_whole_target ${_executorch_shared_whole_archive}) + executorch_target_whole_archive(executorch_shared ${_whole_target}) + endforeach() + configure_file( + tools/cmake/executorch.pc.in ${CMAKE_CURRENT_BINARY_DIR}/executorch.pc + @ONLY + ) + install(FILES ${CMAKE_CURRENT_BINARY_DIR}/executorch.pc + DESTINATION ${CMAKE_INSTALL_LIBDIR}/pkgconfig + ) +endif() + +# Added after the shared runtime rather than with the other backends: this +# backend resolves the runtime from executorch_shared, and naming a target that +# does not exist yet makes the link fail because the library has not been built +# at that point on the line. +if(EXECUTORCH_BUILD_QNN) + add_subdirectory(${CMAKE_CURRENT_SOURCE_DIR}/backends/qualcomm) + list(APPEND _executorch_backends qnn_executorch_backend) +endif() + if(EXECUTORCH_BUILD_KERNELS_TORCHAO) if(NOT TARGET cpuinfo) message( @@ -1016,10 +1067,19 @@ if(EXECUTORCH_BUILD_PYBIND) # Ensure bundled_module waits for bundled_program's generated headers add_dependencies(bundled_module bundled_program) - target_link_libraries(bundled_module PRIVATE extension_data_loader) - target_link_libraries( - bundled_module PUBLIC extension_module_static bundled_program - ) + # extension_module_static and the data loader are bundled into + # libexecutorch.so, so link that instead of pulling private static copies in + # through this target's PUBLIC interface. + if(EXECUTORCH_BUILD_SHARED) + target_link_libraries( + bundled_module PUBLIC executorch_shared bundled_program + ) + else() + target_link_libraries(bundled_module PRIVATE extension_data_loader) + target_link_libraries( + bundled_module PUBLIC extension_module_static bundled_program + ) + endif() target_include_directories( bundled_module PUBLIC ${_common_include_directories} @@ -1038,16 +1098,36 @@ if(EXECUTORCH_BUILD_PYBIND) TORCH_PYTHON_LIBRARY torch_python PATHS "${TORCH_INSTALL_PREFIX}/lib" ) - set(_dep_libs - ${TORCH_PYTHON_LIBRARY} - bundled_program - etdump - flatccrt - executorch - extension_data_loader - util - torch - ) + # When the consolidated shared runtime is built, the pybind extension links it + # instead of whole-archiving the static core, so Python and C++ consumers + # share one backend registry. `executorch` and the extensions bundled into + # libexecutorch.so must stay off this list: their INTERFACE link options force + # whole-archive, which would give this module a private second registry. + # executorch_shared is named here for its include directories and compile + # definitions; executorch_target_link_shared_runtime below is what fixes its + # position on the link line. + if(EXECUTORCH_BUILD_SHARED) + set(_dep_libs + ${TORCH_PYTHON_LIBRARY} + bundled_program + etdump + flatccrt + executorch_shared + util + torch + ) + else() + set(_dep_libs + ${TORCH_PYTHON_LIBRARY} + bundled_program + etdump + flatccrt + executorch + extension_data_loader + util + torch + ) + endif() # Build common AOTI functionality if needed by CUDA or Metal backends if(EXECUTORCH_BUILD_CUDA) @@ -1058,13 +1138,24 @@ if(EXECUTORCH_BUILD_PYBIND) list(APPEND _dep_libs aoti_common) endif() - # RPATH for _portable_lib.so + # RPATH for _portable_lib.so. It sits in + # /executorch/extension/pybindings, so torch is three levels up + # and the wheel's own lib/ directory is two. The second entry is + # wheel-specific: a normal install puts the runtime in CMAKE_INSTALL_LIBDIR + # instead, where this relative path would not reach it. set(_portable_lib_rpath "$ORIGIN/../../../torch/lib") + if(EXECUTORCH_BUILD_SHARED AND EXECUTORCH_BUILD_WHEEL_DO_NOT_USE) + string(APPEND _portable_lib_rpath ":$ORIGIN/../../lib") + endif() if(EXECUTORCH_BUILD_EXTENSION_MODULE) - # Always use static linking for pybindings to avoid runtime symbol - # resolution issues - list(APPEND _dep_libs extension_module_static) + # extension_module_static is already bundled into libexecutorch.so; linking + # it again here would whole-archive a second copy. + if(NOT EXECUTORCH_BUILD_SHARED) + # Always use static linking for pybindings to avoid runtime symbol + # resolution issues + list(APPEND _dep_libs extension_module_static) + endif() # Add bundled_module if available if(TARGET bundled_module) list(APPEND _dep_libs bundled_module) @@ -1150,7 +1241,11 @@ if(EXECUTORCH_BUILD_PYBIND) target_compile_definitions(util PUBLIC C10_USING_CUSTOM_GENERATED_MACROS) target_compile_options(util PUBLIC ${_pybind_compile_options}) - target_link_libraries(util PRIVATE torch c10 executorch extension_tensor) + if(EXECUTORCH_BUILD_SHARED) + target_link_libraries(util PRIVATE torch c10 executorch_shared) + else() + target_link_libraries(util PRIVATE torch c10 executorch extension_tensor) + endif() # pybind portable_lib pybind11_add_module(portable_lib SHARED extension/pybindings/pybindings.cpp) @@ -1167,6 +1262,7 @@ if(EXECUTORCH_BUILD_PYBIND) target_include_directories(portable_lib PRIVATE ${TORCH_INCLUDE_DIRS}) target_compile_options(portable_lib PUBLIC ${_pybind_compile_options}) target_link_libraries(portable_lib PRIVATE ${_dep_libs}) + executorch_target_link_shared_runtime(portable_lib) # Set RPATH to find PyTorch and backend libraries relative to the installation # location. This goes from executorch/extension/pybindings up to @@ -1199,6 +1295,9 @@ if(EXECUTORCH_BUILD_PYBIND) strip_python_lib(data_loader) target_include_directories(data_loader PRIVATE ${_common_include_directories}) target_compile_options(data_loader PUBLIC ${_pybind_compile_options}) + # This module only exposes a pybind type and calls into no runtime symbols, so + # it links the static core as before and needs nothing from the shared + # runtime. target_link_libraries(data_loader PRIVATE executorch) install(TARGETS data_loader LIBRARY DESTINATION executorch/extension/pybindings @@ -1246,49 +1345,6 @@ if(EXECUTORCH_BUILD_KERNELS_LLM) list(APPEND _executorch_kernels custom_ops_aot_lib) endif() -# Consolidated shared library: bundles executorch_core plus commonly used -# extensions into a single libexecutorch.so. -if(EXECUTORCH_BUILD_SHARED) - executorch_add_shared_library(executorch_shared) - set_target_properties( - executorch_shared - PROPERTIES OUTPUT_NAME executorch - ARCHIVE_OUTPUT_NAME executorch_shared - EXPORT_NAME executorch-shared - ) - target_include_directories( - executorch_shared PUBLIC ${_common_include_directories} - ) - target_compile_definitions( - executorch_shared PUBLIC C10_USING_CUSTOM_GENERATED_MACROS - ) - # Link executorch without WHOLE_ARCHIVE because its INTERFACE link options - # (from executorch_target_link_options_shared_lib) already force - # whole-archive. Link executorch_core explicitly since executorch only has a - # PRIVATE dep on it (symbols wouldn't propagate otherwise). - target_link_libraries( - executorch_shared PRIVATE executorch - $ - ) - foreach(_ext_target - extension_data_loader extension_flat_tensor extension_named_data_map - extension_module_static extension_tensor - ) - if(TARGET ${_ext_target}) - target_link_libraries( - executorch_shared PRIVATE $ - ) - endif() - endforeach() - configure_file( - tools/cmake/executorch.pc.in ${CMAKE_CURRENT_BINARY_DIR}/executorch.pc - @ONLY - ) - install(FILES ${CMAKE_CURRENT_BINARY_DIR}/executorch.pc - DESTINATION ${CMAKE_INSTALL_LIBDIR}/pkgconfig - ) -endif() - if(EXECUTORCH_BUILD_KERNELS_QUANTIZED) add_subdirectory(${CMAKE_CURRENT_SOURCE_DIR}/kernels/quantized) executorch_target_link_options_shared_lib(quantized_ops_lib) diff --git a/backends/qualcomm/CMakeLists.txt b/backends/qualcomm/CMakeLists.txt index 3f4aefa2b76..a1a7e5fa0a5 100644 --- a/backends/qualcomm/CMakeLists.txt +++ b/backends/qualcomm/CMakeLists.txt @@ -255,8 +255,25 @@ target_link_libraries( ) target_link_libraries( qnn_executorch_backend PRIVATE qnn_executorch_header qnn_schema qnn_manager - executorch_core qnn_backend_options + qnn_backend_options ) +# Resolve the runtime from the shared library when one is built, so this +# delegate does not carry its own copy of the backend registry. +if(EXECUTORCH_BUILD_SHARED) + target_link_libraries(qnn_executorch_backend PRIVATE executorch_shared) + executorch_target_link_shared_runtime(qnn_executorch_backend) + if(NOT APPLE AND EXECUTORCH_BUILD_WHEEL_DO_NOT_USE) + # Wheel-specific: ships in executorch/backends/qualcomm, two levels below + # the wheel's lib/. A normal install puts the runtime in + # CMAKE_INSTALL_LIBDIR, which this relative path would not reach. + set_target_properties( + qnn_executorch_backend PROPERTIES BUILD_RPATH "$ORIGIN/../../lib" + INSTALL_RPATH "$ORIGIN/../../lib" + ) + endif() +else() + target_link_libraries(qnn_executorch_backend PRIVATE executorch_core) +endif() if(${CMAKE_SYSTEM_PROCESSOR} MATCHES Hexagon) # Add macro here so we can dlopen the correct .so library. @@ -359,12 +376,27 @@ if(${CMAKE_SYSTEM_PROCESSOR} MATCHES "x86_64|AMD64") qnn_schema qnn_manager qnn_executorch_header - executorch extension_tensor qnn_backend_options wrappers qnn_executorch_logging ) + # Same reasoning as the delegate above: take the runtime from the shared + # library when there is one, rather than embedding a second registry. + if(EXECUTORCH_BUILD_SHARED) + target_link_libraries(PyQnnManagerAdaptor PRIVATE executorch_shared) + executorch_target_link_shared_runtime(PyQnnManagerAdaptor) + if(NOT APPLE AND EXECUTORCH_BUILD_WHEEL_DO_NOT_USE) + # Wheel-specific: ships in executorch/backends/qualcomm/python, three + # levels below the wheel's lib/. + set_target_properties( + PyQnnManagerAdaptor PROPERTIES BUILD_RPATH "$ORIGIN/../../../lib" + INSTALL_RPATH "$ORIGIN/../../../lib" + ) + endif() + else() + target_link_libraries(PyQnnManagerAdaptor PRIVATE executorch) + endif() pybind11_extension(PyQnnManagerAdaptor) if(NOT MSVC AND NOT ${CMAKE_BUILD_TYPE} MATCHES RelWithDebInfo) diff --git a/codegen/tools/CMakeLists.txt b/codegen/tools/CMakeLists.txt index b829e83c340..c5097eed537 100644 --- a/codegen/tools/CMakeLists.txt +++ b/codegen/tools/CMakeLists.txt @@ -43,7 +43,25 @@ if(TARGET bundled_program) target_compile_definitions(selective_build PRIVATE -DET_BUNDLE_IO) target_link_libraries(selective_build PRIVATE bundled_program) endif() -target_link_libraries(selective_build PRIVATE executorch_core program_schema) +if(EXECUTORCH_BUILD_SHARED) + # This module calls into the runtime, so resolve those symbols from + # libexecutorch.so rather than baking in a second copy of the core. It lands + # in /executorch/codegen/tools, two levels below the wheel's + # lib/. + target_link_libraries( + selective_build PRIVATE executorch_shared program_schema + ) + if(NOT APPLE AND EXECUTORCH_BUILD_WHEEL_DO_NOT_USE) + # Wheel-specific: the runtime ships two levels up under lib/. A normal + # install puts it in CMAKE_INSTALL_LIBDIR, which this would not reach. + set_target_properties( + selective_build PROPERTIES BUILD_RPATH "$ORIGIN/../../lib" + INSTALL_RPATH "$ORIGIN/../../lib" + ) + endif() +else() + target_link_libraries(selective_build PRIVATE executorch_core program_schema) +endif() # Install the module install(TARGETS selective_build LIBRARY DESTINATION executorch/codegen/tools) diff --git a/devtools/bundled_program/CMakeLists.txt b/devtools/bundled_program/CMakeLists.txt index 0c213d9a83c..375f7487f06 100644 --- a/devtools/bundled_program/CMakeLists.txt +++ b/devtools/bundled_program/CMakeLists.txt @@ -40,7 +40,14 @@ add_library( bundled_program ${_schema_outputs} ${CMAKE_CURRENT_SOURCE_DIR}/bundled_program.cpp ) -target_link_libraries(bundled_program PUBLIC executorch) +# The `executorch` target forces whole-archive of itself, which would duplicate +# the primitive operator registrations already inside libexecutorch.so and abort +# at load. Resolve them from the shared runtime instead when it is built. +if(EXECUTORCH_BUILD_SHARED) + target_link_libraries(bundled_program PUBLIC executorch_shared) +else() + target_link_libraries(bundled_program PUBLIC executorch) +endif() target_include_directories( bundled_program PUBLIC diff --git a/devtools/etdump/CMakeLists.txt b/devtools/etdump/CMakeLists.txt index 9ef3c8cd6f7..fe754d8129a 100644 --- a/devtools/etdump/CMakeLists.txt +++ b/devtools/etdump/CMakeLists.txt @@ -49,11 +49,15 @@ add_library( ${CMAKE_CURRENT_SOURCE_DIR}/data_sinks/file_data_sink.cpp ${CMAKE_CURRENT_SOURCE_DIR}/data_sinks/file_data_sink.h ) -target_link_libraries( - etdump - PUBLIC flatccrt - PRIVATE executorch -) +target_link_libraries(etdump PUBLIC flatccrt) +# As with bundled_program, avoid the whole-archive of the `executorch` target so +# the primitive operator registrations are not duplicated alongside the copy +# already inside libexecutorch.so. +if(EXECUTORCH_BUILD_SHARED) + target_link_libraries(etdump PRIVATE executorch_shared) +else() + target_link_libraries(etdump PRIVATE executorch) +endif() target_include_directories( etdump PUBLIC ${DEVTOOLS_INCLUDE_DIR} diff --git a/extension/llm/custom_ops/CMakeLists.txt b/extension/llm/custom_ops/CMakeLists.txt index 8a43a5ddf5c..444d39ab4e0 100644 --- a/extension/llm/custom_ops/CMakeLists.txt +++ b/extension/llm/custom_ops/CMakeLists.txt @@ -144,19 +144,33 @@ if(EXECUTORCH_BUILD_KERNELS_LLM_AOT) set(RPATH "@loader_path/../../pybindings") else() set(RPATH "$ORIGIN/../../pybindings") + if(EXECUTORCH_BUILD_SHARED AND EXECUTORCH_BUILD_WHEEL_DO_NOT_USE) + # Wheel-specific: this library lands in + # /executorch/extension/llm/custom_ops, three levels below + # the wheel's lib/. A normal install puts the runtime in + # CMAKE_INSTALL_LIBDIR, which this relative path would not reach. + string(APPEND RPATH ":$ORIGIN/../../../lib") + endif() endif() - set_target_properties(custom_ops_aot_lib PROPERTIES INSTALL_RPATH ${RPATH}) + # The wheel copies this library straight out of the build tree, so the runtime + # search path has to be set on the built artifact and not only on install. + set_target_properties( + custom_ops_aot_lib PROPERTIES BUILD_RPATH ${RPATH} INSTALL_RPATH ${RPATH} + ) if(TARGET portable_lib) # If we have portable_lib built, custom_ops_aot_lib gives the ability to use # the ops in PyTorch and ExecuTorch through pybind target_link_libraries(custom_ops_aot_lib PUBLIC portable_lib) - else() + elseif(NOT EXECUTORCH_BUILD_SHARED) # If no portable_lib, custom_ops_aot_lib still gives the ability to use the # ops in PyTorch target_link_libraries( custom_ops_aot_lib PUBLIC executorch_core kernels_util_all_deps ) + else() + target_link_libraries(custom_ops_aot_lib PUBLIC kernels_util_all_deps) endif() + executorch_target_link_shared_runtime(custom_ops_aot_lib) target_link_libraries( custom_ops_aot_lib PUBLIC cpublas torch extension_tensor diff --git a/extension/llm/runner/CMakeLists.txt b/extension/llm/runner/CMakeLists.txt index 5247a4ba0a6..d8bdcbce441 100644 --- a/extension/llm/runner/CMakeLists.txt +++ b/extension/llm/runner/CMakeLists.txt @@ -123,6 +123,7 @@ if(EXECUTORCH_BUILD_PYBIND) _llm_runner PRIVATE extension_llm_runner tokenizers::tokenizers portable_lib ${TORCH_PYTHON_LIBRARY} ${TORCH_LIBRARIES} ) + executorch_target_link_shared_runtime(_llm_runner) set_target_properties( _llm_runner @@ -137,6 +138,13 @@ if(EXECUTORCH_BUILD_PYBIND) ) else() set(RPATH "$ORIGIN/../../pybindings:$ORIGIN/../../../../torch/lib") + if(EXECUTORCH_BUILD_SHARED AND EXECUTORCH_BUILD_WHEEL_DO_NOT_USE) + # Wheel-specific: this module lands in + # /executorch/extension/llm/runner, three levels below the + # wheel's lib/. A normal install puts the runtime in CMAKE_INSTALL_LIBDIR, + # which this relative path would not reach. + string(APPEND RPATH ":$ORIGIN/../../../lib") + endif() endif() set_target_properties( _llm_runner PROPERTIES BUILD_RPATH "${RPATH}" INSTALL_RPATH "${RPATH}" diff --git a/extension/training/CMakeLists.txt b/extension/training/CMakeLists.txt index e835ae0e0a3..978d1420e6c 100644 --- a/extension/training/CMakeLists.txt +++ b/extension/training/CMakeLists.txt @@ -49,10 +49,18 @@ target_link_libraries( target_compile_options(train_xor PUBLIC ${_common_compile_options}) if(EXECUTORCH_BUILD_PYBIND) - # Pybind library. - set(_pybind_training_dep_libs ${TORCH_PYTHON_LIBRARY} etdump executorch util - torch extension_training - ) + # Pybind library. When the consolidated shared runtime is built, the runtime + # is resolved from it rather than from the whole-archive-forcing static + # `executorch` target, so this module shares the one backend registry. + if(EXECUTORCH_BUILD_SHARED) + set(_pybind_training_dep_libs ${TORCH_PYTHON_LIBRARY} etdump util torch + extension_training + ) + else() + set(_pybind_training_dep_libs ${TORCH_PYTHON_LIBRARY} etdump executorch + util torch extension_training + ) + endif() if(EXECUTORCH_BUILD_XNNPACK) # need to explicitly specify XNNPACK and xnnpack-microkernels-prod here @@ -81,6 +89,21 @@ if(EXECUTORCH_BUILD_PYBIND) -fexceptions> ) target_link_libraries(_training_lib PRIVATE ${_pybind_training_dep_libs}) + executorch_target_link_shared_runtime(_training_lib) + + if(EXECUTORCH_BUILD_SHARED + AND NOT APPLE + AND EXECUTORCH_BUILD_WHEEL_DO_NOT_USE + ) + # Wheel-specific: this module lands in + # /executorch/extension/training/pybindings, three levels + # below the wheel's lib/. A normal install puts the runtime in + # CMAKE_INSTALL_LIBDIR, which this relative path would not reach. + set_target_properties( + _training_lib PROPERTIES BUILD_RPATH "$ORIGIN/../../../lib" + INSTALL_RPATH "$ORIGIN/../../../lib" + ) + endif() install(TARGETS _training_lib LIBRARY DESTINATION executorch/extension/training/pybindings diff --git a/kernels/quantized/CMakeLists.txt b/kernels/quantized/CMakeLists.txt index 2dac38205b4..9269a8c00f4 100644 --- a/kernels/quantized/CMakeLists.txt +++ b/kernels/quantized/CMakeLists.txt @@ -86,6 +86,24 @@ if(NOT CMAKE_GENERATOR STREQUAL "Xcode" gen_custom_ops_aot_lib( LIB_NAME "quantized_ops_aot_lib" KERNEL_SOURCES "${_quantized_sources}" ) + # Only when the pybind extension is absent. When it is present, the block + # below sets the full runtime path for this target in one place, and setting + # it here as well would leave two independent copies of the same logic where + # the later one wins. + if(EXECUTORCH_BUILD_SHARED + AND NOT APPLE + AND EXECUTORCH_BUILD_WHEEL_DO_NOT_USE + AND NOT TARGET portable_lib + ) + # Wheel-specific: the generated library lands in + # /executorch/kernels/quantized, two levels below the + # wheel's lib/. A normal install puts the runtime in CMAKE_INSTALL_LIBDIR, + # which this relative path would not reach. + set_target_properties( + quantized_ops_aot_lib PROPERTIES BUILD_RPATH "$ORIGIN/../../lib" + INSTALL_RPATH "$ORIGIN/../../lib" + ) + endif() # Register quantized ops to portable_lib, so that they're available via # pybindings. @@ -128,9 +146,13 @@ if(NOT CMAKE_GENERATOR STREQUAL "Xcode" # installed location of our _portable_lib.so file. To see these LC_* # values, run `otool -l libquantized_ops_lib.dylib`. if(APPLE) - set(RPATH "@loader_path/../../extensions/pybindings") + set(RPATH "@loader_path/../../extension/pybindings") else() - set(RPATH "$ORIGIN/../../extensions/pybindings") + set(RPATH "$ORIGIN/../../extension/pybindings") + if(EXECUTORCH_BUILD_SHARED AND EXECUTORCH_BUILD_WHEEL_DO_NOT_USE) + # Wheel-specific: two levels below the wheel's lib/ directory. + string(APPEND RPATH ":$ORIGIN/../../lib") + endif() endif() set_target_properties( quantized_ops_aot_lib PROPERTIES BUILD_RPATH ${RPATH} INSTALL_RPATH diff --git a/setup.py b/setup.py index 8331deec8ad..f33e5478476 100644 --- a/setup.py +++ b/setup.py @@ -322,6 +322,18 @@ def get_dynamic_lib_name(name: str) -> str: return f"lib{name}.so" +def get_runtime_soname_major() -> str: + """The major version in the shared runtime's SONAME. + + CMake derives SOVERSION from version.txt, so read the major from the same + place. Version.string() is not usable here because BUILD_VERSION can + override it without changing what the linker recorded. + """ + root = os.path.dirname(os.path.abspath(__file__)) + with open(os.path.join(root, "version.txt")) as f: + return f.read().strip().split(".")[0] + + def get_executable_name(name: str) -> str: if _is_windows(): return name + ".exe" @@ -758,7 +770,11 @@ def run(self): "runtime/kernel/", "runtime/backend/", "runtime/platform/", + "extension/data_loader/", + "extension/flat_tensor/", "extension/kernel_util/", + "extension/module/", + "extension/named_data_map/", "extension/tensor/", "extension/threadpool/", ]: @@ -1089,6 +1105,15 @@ def run(self): # noqa C901 [] if _is_minimal_build() else [ + # Install the shared C++ runtime so a standalone application can + # link executorch::runtime from the wheel. Shipped under its + # SONAME so the DT_NEEDED a consumer records resolves at runtime. + BuiltFile( + src_dir="%CMAKE_CACHE_DIR%/", + src_name=f"libexecutorch.so.{get_runtime_soname_major()}.*", + dst=f"executorch/lib/libexecutorch.so.{get_runtime_soname_major()}", + dependent_cmake_flags=["EXECUTORCH_BUILD_SHARED"], + ), # Install the prebuilt pybindings extension wrapper for the runtime, # portable kernels, and a selection of backends. This lets users # load and execute .pte files from python. diff --git a/tools/cmake/Codegen.cmake b/tools/cmake/Codegen.cmake index 4253fa44dc5..66d72914e67 100644 --- a/tools/cmake/Codegen.cmake +++ b/tools/cmake/Codegen.cmake @@ -261,9 +261,10 @@ function(gen_custom_ops_aot_lib) executorch_target_link_options_shared_lib(${GEN_LIB_NAME}) if(TARGET portable_lib) target_link_libraries(${GEN_LIB_NAME} PRIVATE portable_lib) - else() + elseif(NOT EXECUTORCH_BUILD_SHARED) target_link_libraries(${GEN_LIB_NAME} PRIVATE executorch_core) endif() + executorch_target_link_shared_runtime(${GEN_LIB_NAME}) endfunction() # Generate a runtime lib for registering operators in Executorch diff --git a/tools/cmake/Utils.cmake b/tools/cmake/Utils.cmake index 958b425c47c..ab884772da6 100644 --- a/tools/cmake/Utils.cmake +++ b/tools/cmake/Utils.cmake @@ -47,6 +47,27 @@ function(executorch_msvc_kernel_link_options target_name) ) endfunction() +# Add a whole-archive reference to a static library on a consumer's link line. +# +# This is deliberately a link option rather than a link library: CMake refuses +# to mix the WHOLE_ARCHIVE link feature with the plain references other targets +# make to the same archive, and link options are also emitted before the ordered +# link libraries, which is what keeps a bundled archive ahead of anything that +# would otherwise satisfy the same symbols. +function(executorch_target_whole_archive target_name archive_target) + if(APPLE) + set(_flags "SHELL:LINKER:-force_load,$") + elseif(MSVC) + set(_flags "SHELL:LINKER:/WHOLEARCHIVE:$") + else() + set(_flags + "SHELL:LINKER:--whole-archive $ LINKER:--no-whole-archive" + ) + endif() + target_link_options(${target_name} PRIVATE "${_flags}") + add_dependencies(${target_name} ${archive_target}) +endfunction() + # Ensure that the load-time constructor functions run. By default, the linker # would remove them since there are no other references to them. function(executorch_target_link_options_shared_lib target_name) @@ -212,6 +233,55 @@ function(executorch_target_copy_mlx_metallib target) endif() endfunction() +# Make a target resolve the ExecuTorch runtime from libexecutorch.so. +# +# Naming the shared runtime as an ordinary dependency is not enough. CMake +# orders link libraries so that an archive precedes what it depends on, which +# puts libexecutorch_core.a ahead of libexecutorch.so; the archive then +# satisfies the runtime symbols first and the target ends up with a private copy +# of the backend registry. Link options come before the ordered libraries, so +# naming the runtime there leaves the archive with nothing left to resolve. +# +# On ELF platforms --no-as-needed is needed around it, because a shared library +# with no already-referenced symbol at the point it appears can be dropped, and +# the static archive further along the line would then supply the registry after +# all. Other linkers keep the reference without it. +function(executorch_target_link_shared_runtime target_name) + executorch_target_retain_shared_library(${target_name} executorch_shared) +endfunction() + +# Put a shared library on a consumer's link line and keep it there. +# +# A library whose only purpose is to run a static initializer, such as a backend +# or an operator registration library, has no symbol the consumer references +# directly, so the linker is free to drop it from DT_NEEDED. Some linkers do +# exactly that and the initializer never runs, which shows up at runtime as a +# backend or kernel that is missing rather than as a link error. +function(executorch_target_retain_shared_library target_name library_target) + if(NOT EXECUTORCH_BUILD_SHARED) + return() + endif() + if(APPLE OR MSVC) + # TARGET_LINKER_FILE rather than TARGET_FILE: on Windows the linker needs + # the import library, not the DLL itself. + set(_retain_flags "SHELL:$") + else() + # push-state/pop-state rather than closing with an explicit --as-needed: + # that would leave --as-needed in force for everything after it on the line + # and drop the next library that only exists for static-init registration. + set(_retain_flags + "SHELL:LINKER:--push-state,--no-as-needed $ LINKER:--pop-state" + ) + endif() + # The generator expression alone does not order the build, so say it outright. + add_dependencies(${target_name} ${library_target}) + set_property( + TARGET ${target_name} + APPEND + PROPERTY LINK_OPTIONS "${_retain_flags}" + ) +endfunction() + # Create and install a shared library composed from dependency libraries. The # target links the provided dependencies and carries VERSION/SOVERSION. function(executorch_add_shared_library target_name) diff --git a/tools/cmake/executorch-wheel-config.cmake b/tools/cmake/executorch-wheel-config.cmake index 1d6096a2e96..f8d5601d50b 100644 --- a/tools/cmake/executorch-wheel-config.cmake +++ b/tools/cmake/executorch-wheel-config.cmake @@ -8,7 +8,10 @@ # for this file and find ExecuTorch package if it is installed. Typical usage # is: # +# ~~~ # find_package(executorch REQUIRED) +# target_link_libraries(my_app PRIVATE executorch::runtime) +# ~~~ # ------- # # Finds the ExecuTorch library @@ -19,10 +22,82 @@ # EXECUTORCH_INCLUDE_DIRS -- The include directories for ExecuTorch # EXECUTORCH_LIBRARIES -- Libraries to link against # +# and, when the prebuilt shared runtime is present, the imported target: +# +# executorch::runtime -- The prebuilt C++ runtime (libexecutorch.so) +# cmake_minimum_required(VERSION 3.19) -# Find prebuilt _portable_lib..so. This file should be installed -# under /executorch/share/cmake +# This file is installed to /executorch/share/cmake, so the +# package root is two levels up. Everything is resolved relative to this file so +# the wheel stays relocatable: no absolute path from the machine that built it +# is baked in here. +get_filename_component( + _executorch_package_root "${CMAKE_CURRENT_LIST_DIR}/../.." ABSOLUTE +) + +set(EXECUTORCH_INCLUDE_DIRS + "${_executorch_package_root}/include" + "${_executorch_package_root}/include/executorch/runtime/core/portable_type/c10" +) + +set(EXECUTORCH_LIBRARIES) +set(EXECUTORCH_FOUND OFF) + +# The prebuilt runtime. Match the versioned file rather than a hardcoded major +# so the config keeps working across releases. +file(GLOB _executorch_runtime_candidates + "${_executorch_package_root}/lib/libexecutorch.so" + "${_executorch_package_root}/lib/libexecutorch.so.*" +) +# An unversioned libexecutorch.so sorts before any libexecutorch.so., so +# a development symlink wins over the versioned file when both are present. +list(SORT _executorch_runtime_candidates) +list(LENGTH _executorch_runtime_candidates _executorch_runtime_count) +if(_executorch_runtime_count GREATER 0) + list(GET _executorch_runtime_candidates 0 _executorch_runtime_library) + + set(EXECUTORCH_FOUND ON) + message(STATUS "ExecuTorch runtime found at ${_executorch_runtime_library}") + + # The documented contract is that a consumer can link ${EXECUTORCH_LIBRARIES}. + # Leaving it empty here would make find_package succeed while offering nothing + # linkable to anyone who has not moved to the imported target. + list(APPEND EXECUTORCH_LIBRARIES executorch::runtime) + + # This file can be processed more than once in a single configure, for example + # when several subprojects each call find_package(executorch). Creating the + # target twice is an error, so only define it once and set the properties + # either way. + if(NOT TARGET executorch::runtime) + add_library(executorch::runtime SHARED IMPORTED) + endif() + set_target_properties( + executorch::runtime + PROPERTIES IMPORTED_LOCATION "${_executorch_runtime_library}" + INTERFACE_INCLUDE_DIRECTORIES "${EXECUTORCH_INCLUDE_DIRS}" + INTERFACE_COMPILE_FEATURES cxx_std_17 + INTERFACE_COMPILE_DEFINITIONS C10_USING_CUSTOM_GENERATED_MACROS + ) + # Consumers get the wheel's lib/ directory in their RUNPATH automatically, + # because CMake adds the imported library's directory. Also record + # $ORIGIN-relative entries so an application that is deployed next to a copy + # of the runtime keeps working without relinking or LD_LIBRARY_PATH. $ORIGIN + # is a loader token, so it belongs only in RUNPATH, never in + # IMPORTED_LOCATION. + if(CMAKE_SYSTEM_NAME STREQUAL "Linux") + set_property( + TARGET executorch::runtime + APPEND + PROPERTY INTERFACE_LINK_OPTIONS "LINKER:-rpath,$ORIGIN" + "LINKER:-rpath,$ORIGIN/../lib" + ) + endif() +endif() + +# Find prebuilt _portable_lib..so. This is the legacy contract used +# to build custom-op extensions against the Python module, and is kept working +# independently of the runtime target above. # Find python if(DEFINED ENV{CONDA_DEFAULT_ENV} AND NOT $ENV{CONDA_DEFAULT_ENV} STREQUAL @@ -45,6 +120,20 @@ execute_process( if(SYSCONFIG_RESULT EQUAL 0) message(STATUS "Sysconfig extension suffix: ${EXT_SUFFIX}") +elseif(TARGET executorch::runtime) + # A C++ application linking only the shared runtime does not need Python at + # all, so a missing interpreter must not fail its configure. Skip locating the + # Python extension instead; the legacy _portable_lib target is simply not + # offered in that case. + message( + STATUS + "Python not usable, skipping the Python extension: ${SYSCONFIG_ERROR}" + ) + set(EXT_SUFFIX "") + # find_library caches its result, so a value left by an earlier configure + # would survive and the extension would still be offered despite being skipped + # here. + unset(_portable_lib_LIBRARY CACHE) else() message( FATAL_ERROR @@ -52,27 +141,48 @@ else() ) endif() -find_library( - _portable_lib_LIBRARY - NAMES _portable_lib${EXT_SUFFIX} - PATHS "${CMAKE_CURRENT_LIST_DIR}/../../extension/pybindings/" -) +if(EXT_SUFFIX) + find_library( + _portable_lib_LIBRARY + NAMES _portable_lib${EXT_SUFFIX} + PATHS "${_executorch_package_root}/extension/pybindings/" + # This config binds to the wheel it ships in, so a same-named library + # elsewhere on the system must not be picked up instead. + NO_DEFAULT_PATH + ) +endif() -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) + if(NOT TARGET _portable_lib) + add_library(_portable_lib STATIC IMPORTED) + endif() # 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 + # An interface requirement rather than CXX_STANDARD: an imported + # target compiles nothing itself, and CXX_STANDARD does not reach + # consumers, so a custom-op build linking this could still + # compile + # as C++17 and fail against headers that need C++20. + INTERFACE_COMPILE_FEATURES cxx_std_20 + ) +endif() + +# find_package checks _FOUND, which is case-sensitive and does not +# match the EXECUTORCH_FOUND spelling this file documents. Without this, a +# REQUIRED find_package would succeed even when nothing usable was located. +set(executorch_FOUND ${EXECUTORCH_FOUND}) +if(NOT executorch_FOUND AND executorch_FIND_REQUIRED) + message( + FATAL_ERROR + "Found the ExecuTorch package but neither the shared runtime nor the Python " + "extension could be located inside it." ) endif() diff --git a/tools/cmake/preset/pybind.cmake b/tools/cmake/preset/pybind.cmake index d292c9ed240..068f80d1e2b 100644 --- a/tools/cmake/preset/pybind.cmake +++ b/tools/cmake/preset/pybind.cmake @@ -104,6 +104,11 @@ elseif(CMAKE_SYSTEM_NAME STREQUAL "Linux") endif() endif() set_overridable_option(EXECUTORCH_BUILD_OPENVINO OFF) + # Ship one shared runtime that both the pybind extension and standalone C++ + # consumers link, so a process has a single backend registry. Linux only: + # macOS C++ consumers are served by the Swift package distribution, and the + # runtime has no export annotations for a Windows DLL. + set_overridable_option(EXECUTORCH_BUILD_SHARED ON) elseif(CMAKE_SYSTEM_NAME STREQUAL "Windows" OR CMAKE_SYSTEM_NAME STREQUAL "WIN32" )