From 0519ab40a138bfdf14bde9933140363639e86141 Mon Sep 17 00:00:00 2001 From: John Gibson Date: Thu, 23 Apr 2026 15:07:04 -0400 Subject: [PATCH 1/4] Android: build/runtime fixes for modern Clang + ETDump path override Three small fixes needed to build and run ExecuTorch Android on hosts with Apple Clang 21 and on devices where /data/local/tmp isn't app-writable: 1. Workaround flatcc's vendored CMakeLists.txt hitting -Werror on new Clang warnings (-Wimplicit-int-conversion-on-negation, -Wunterminated-string-initialization). Passes FLATCC_ALLOW_WERROR=OFF into both the top-level flatcc CMake build and the flatcc_ep ExternalProject. 2. scripts/build_android_library.sh was setting EXECUTORCH_ENABLE_EVENT_TRACER from $EXECUTORCH_ANDROID_PROFILING but never forwarding the separate -DEXECUTORCH_ANDROID_PROFILING define that gates ETDumpGen allocation in extension/android/jni/jni_layer.cpp. As a result, the event tracer was compiled in but the JNI never attached it, and Module.etdump() always returned false. 3. Add Module.etdump(String outputPath) / etdumpTo() JNI method so apps can write the ETDump into their own filesDir. The existing etdump() hardcodes /data/local/tmp/result.etdump, which is not app-writable on modern Android (Pixel 9 / API 36 returns EACCES). The new overload factors the write path through a shared etdump_to_path() helper; etdump() now delegates to it with the legacy path. --- .../java/org/pytorch/executorch/Module.java | 22 ++++++++++++++++ extension/android/jni/jni_layer.cpp | 26 +++++++++++++++---- scripts/build_android_library.sh | 2 ++ third-party/CMakeLists.txt | 1 + 4 files changed, 46 insertions(+), 5 deletions(-) diff --git a/extension/android/executorch_android/src/main/java/org/pytorch/executorch/Module.java b/extension/android/executorch_android/src/main/java/org/pytorch/executorch/Module.java index f7e2e37dcec..0e13284e0ac 100644 --- a/extension/android/executorch_android/src/main/java/org/pytorch/executorch/Module.java +++ b/extension/android/executorch_android/src/main/java/org/pytorch/executorch/Module.java @@ -227,6 +227,28 @@ public String[] readLogBuffer() { @DoNotStrip public native boolean etdump(); + /** + * Dump the ExecuTorch ETDump file to {@code outputPath}. + * + * @param outputPath absolute path to write the etdump file to. + * @return true if the etdump was successfully written, false otherwise. + */ + @Experimental + public boolean etdump(String outputPath) { + mLock.lock(); + try { + if (!mHybridData.isValid()) { + throw new IllegalStateException("Module has been destroyed"); + } + return etdumpToNative(outputPath); + } finally { + mLock.unlock(); + } + } + + @DoNotStrip + private native boolean etdumpToNative(String outputPath); + /** * Explicitly destroys the native Module object. Calling this method is not required, as the * native object will be destroyed when this object is garbage-collected. However, the timing of diff --git a/extension/android/jni/jni_layer.cpp b/extension/android/jni/jni_layer.cpp index beff72119b8..39d8021ed51 100644 --- a/extension/android/jni/jni_layer.cpp +++ b/extension/android/jni/jni_layer.cpp @@ -466,25 +466,36 @@ class ExecuTorchJni : public facebook::jni::HybridClass { } jboolean etdump() { + return etdump_to_path("/data/local/tmp/result.etdump"); + } + + jboolean etdumpTo(facebook::jni::alias_ref outputPath) { + return etdump_to_path(outputPath->toStdString().c_str()); + } + + private: + jboolean etdump_to_path(const char* path) { #ifdef EXECUTORCH_ANDROID_PROFILING executorch::etdump::ETDumpGen* etdumpgen = (executorch::etdump::ETDumpGen*)module_->event_tracer(); auto etdump_data = etdumpgen->get_etdump_data(); if (etdump_data.buf != nullptr && etdump_data.size > 0) { - int etdump_file = - open("/data/local/tmp/result.etdump", O_WRONLY | O_CREAT, 0644); + int etdump_file = open(path, O_WRONLY | O_CREAT | O_TRUNC, 0644); if (etdump_file == -1) { - ET_LOG(Error, "Cannot create result.etdump error: %d", errno); + ET_LOG(Error, "Cannot create %s error: %d", path, errno); + free(etdump_data.buf); return false; } ssize_t bytes_written = write(etdump_file, (uint8_t*)etdump_data.buf, etdump_data.size); if (bytes_written == -1) { - ET_LOG(Error, "Cannot write result.etdump error: %d", errno); + ET_LOG(Error, "Cannot write %s error: %d", path, errno); + close(etdump_file); + free(etdump_data.buf); return false; } else { - ET_LOG(Info, "ETDump written %d bytes to file.", bytes_written); + ET_LOG(Info, "ETDump written %zd bytes to %s.", bytes_written, path); } close(etdump_file); free(etdump_data.buf); @@ -492,10 +503,14 @@ class ExecuTorchJni : public facebook::jni::HybridClass { } else { ET_LOG(Error, "No ETDump data available!"); } +#else + (void)path; #endif return false; } + public: + facebook::jni::local_ref> getMethods() { const auto& names_result = module_->method_names(); if (!names_result.ok()) { @@ -565,6 +580,7 @@ class ExecuTorchJni : public facebook::jni::HybridClass { makeNativeMethod( "readLogBufferStaticNative", ExecuTorchJni::readLogBufferStatic), makeNativeMethod("etdump", ExecuTorchJni::etdump), + makeNativeMethod("etdumpToNative", ExecuTorchJni::etdumpTo), makeNativeMethod("getMethods", ExecuTorchJni::getMethods), makeNativeMethod("getUsedBackends", ExecuTorchJni::getUsedBackends), }); diff --git a/scripts/build_android_library.sh b/scripts/build_android_library.sh index 0fb9e909884..de3f62699ea 100755 --- a/scripts/build_android_library.sh +++ b/scripts/build_android_library.sh @@ -40,6 +40,7 @@ build_android_native_library() { --preset "android-${ANDROID_ABI}" \ -DANDROID_PLATFORM=android-26 \ -DEXECUTORCH_ENABLE_EVENT_TRACER="${EXECUTORCH_ANDROID_PROFILING:-OFF}" \ + -DEXECUTORCH_ANDROID_PROFILING="${EXECUTORCH_ANDROID_PROFILING:-OFF}" \ -DEXECUTORCH_BUILD_EXTENSION_LLM="${EXECUTORCH_BUILD_EXTENSION_LLM:-ON}" \ -DEXECUTORCH_BUILD_EXTENSION_LLM_RUNNER="${EXECUTORCH_BUILD_EXTENSION_LLM:-ON}" \ -DEXECUTORCH_BUILD_EXTENSION_ASR_RUNNER="${EXECUTORCH_BUILD_EXTENSION_LLM:-ON}" \ @@ -51,6 +52,7 @@ build_android_native_library() { -DQNN_SDK_ROOT="${QNN_SDK_ROOT}" \ -DEXECUTORCH_BUILD_VULKAN="${EXECUTORCH_BUILD_VULKAN}" \ -DXNNPACK_ENABLE_ARM_SME2="${XNNPACK_ENABLE_ARM_SME2}" \ + -DFLATCC_ALLOW_WERROR=OFF \ -DSUPPORT_REGEX_LOOKAHEAD=ON \ -DCMAKE_BUILD_TYPE="${EXECUTORCH_CMAKE_BUILD_TYPE}" \ -B"${CMAKE_OUT}" diff --git a/third-party/CMakeLists.txt b/third-party/CMakeLists.txt index bafcd74ec77..b81ad94de89 100644 --- a/third-party/CMakeLists.txt +++ b/third-party/CMakeLists.txt @@ -100,6 +100,7 @@ ExternalProject_Add( -DFLATCC_TEST=OFF -DFLATCC_REFLECTION=OFF -DFLATCC_DEBUG_CLANG_SANITIZE=OFF + -DFLATCC_ALLOW_WERROR=OFF -DFLATCC_INSTALL=ON -DCMAKE_POLICY_VERSION_MINIMUM=3.5 -DCMAKE_INSTALL_PREFIX:PATH= From 6a05ca4b897efbc2ec38511089c07eb4f5637e03 Mon Sep 17 00:00:00 2001 From: John Gibson Date: Thu, 23 Apr 2026 15:45:49 -0400 Subject: [PATCH 2/4] tools/cmake/common: quote DESCRIPTION in define_overridable_option MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The set(..CACHE TYPE DOCSTRING [FORCE]) form treats the docstring as a single argument, but `${DESCRIPTION}` was being expanded unquoted, so multi-word descriptions were spilling their trailing words into subsequent set() arguments. This is latent for the common case because most existing STRING options are driven by the else-branch (not overridden via -D), but any STRING option overridden on the cmake command line with a multi-word description hits it — for example: cmake -DEXECUTORCH_VULKAN_FP16_PRECISION=mediump ... fails with the description text being fed to the downstream shader generator as CLI args. One-char fix: quote "${DESCRIPTION}" in both set() calls. Correct for both single- and multi-word descriptions. --- tools/cmake/common/preset.cmake | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/tools/cmake/common/preset.cmake b/tools/cmake/common/preset.cmake index 4ac45e28562..7d016db2347 100644 --- a/tools/cmake/common/preset.cmake +++ b/tools/cmake/common/preset.cmake @@ -82,12 +82,12 @@ macro(define_overridable_option NAME DESCRIPTION VALUE_TYPE DEFAULT_VALUE) if(DEFINED ${NAME} AND NOT DEFINED CACHE{${NAME}}) set(${NAME} ${${NAME}} - CACHE ${VALUE_TYPE} ${DESCRIPTION} FORCE + CACHE ${VALUE_TYPE} "${DESCRIPTION}" FORCE ) else() set(${NAME} ${DEFAULT_VALUE} - CACHE ${VALUE_TYPE} ${DESCRIPTION} + CACHE ${VALUE_TYPE} "${DESCRIPTION}" ) endif() From d5911a2c884e002e40b5dd0d56e88202ac20ef22 Mon Sep 17 00:00:00 2001 From: John Gibson Date: Tue, 5 May 2026 10:29:55 -0400 Subject: [PATCH 3/4] scripts/build_android_library: forward PYTHON_EXECUTABLE to cmake MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The script defaults `PYTHON_EXECUTABLE=python3` (and honors any value the caller sets in the env), but never passed it to the `cmake` invocation. As a result, cmake's `find_package(Python3)` picked up whatever interpreter came first on PATH, which on macOS is typically Homebrew's `python3` rather than the project's torch-equipped venv. The build then failed at `find_package_torch_headers` (`tools/cmake/Utils.cmake:118`) with: AttributeError: 'NoneType' object has no attribute 'submodule_search_locations' (i.e. `import torch` in the wrong interpreter, returning None.) One-line fix: pass `-DPYTHON_EXECUTABLE="${PYTHON_EXECUTABLE}"` so the script's documented behavior — that the caller can pin the interpreter — actually flows through to the cmake configure step. --- scripts/build_android_library.sh | 1 + 1 file changed, 1 insertion(+) diff --git a/scripts/build_android_library.sh b/scripts/build_android_library.sh index de3f62699ea..5363c64b87c 100755 --- a/scripts/build_android_library.sh +++ b/scripts/build_android_library.sh @@ -37,6 +37,7 @@ build_android_native_library() { cmake . -DCMAKE_INSTALL_PREFIX="${CMAKE_OUT}" \ -DCMAKE_TOOLCHAIN_FILE="${ANDROID_NDK}/build/cmake/android.toolchain.cmake" \ + -DPYTHON_EXECUTABLE="${PYTHON_EXECUTABLE}" \ --preset "android-${ANDROID_ABI}" \ -DANDROID_PLATFORM=android-26 \ -DEXECUTORCH_ENABLE_EVENT_TRACER="${EXECUTORCH_ANDROID_PROFILING:-OFF}" \ From dc1bcb3fdd737a562b417e046c99856399cda7cc Mon Sep 17 00:00:00 2001 From: John Gibson Date: Tue, 5 May 2026 10:35:07 -0400 Subject: [PATCH 4/4] Address review feedback on etdump_to_path * Null-guard module_->event_tracer() before casting to ETDumpGen*. If the tracer is unset (e.g. ANDROID_PROFILING was compiled in but the module was constructed without an ETDumpGen on some path), we'd previously segfault on get_etdump_data(). Switch the C-style cast to a static_cast while we're here. * Move etdump_to_path() into a private: section at the end of the class, rather than splicing private:/public: visibility blocks into the middle of the public method group. * Reject null outputPath in etdumpTo() before calling toStdString(); a null jstring would otherwise crash. --- extension/android/jni/jni_layer.cpp | 82 ++++++++++++++++------------- 1 file changed, 44 insertions(+), 38 deletions(-) diff --git a/extension/android/jni/jni_layer.cpp b/extension/android/jni/jni_layer.cpp index 39d8021ed51..c0c8cf8e7f0 100644 --- a/extension/android/jni/jni_layer.cpp +++ b/extension/android/jni/jni_layer.cpp @@ -470,47 +470,13 @@ class ExecuTorchJni : public facebook::jni::HybridClass { } jboolean etdumpTo(facebook::jni::alias_ref outputPath) { - return etdump_to_path(outputPath->toStdString().c_str()); - } - - private: - jboolean etdump_to_path(const char* path) { -#ifdef EXECUTORCH_ANDROID_PROFILING - executorch::etdump::ETDumpGen* etdumpgen = - (executorch::etdump::ETDumpGen*)module_->event_tracer(); - auto etdump_data = etdumpgen->get_etdump_data(); - - if (etdump_data.buf != nullptr && etdump_data.size > 0) { - int etdump_file = open(path, O_WRONLY | O_CREAT | O_TRUNC, 0644); - if (etdump_file == -1) { - ET_LOG(Error, "Cannot create %s error: %d", path, errno); - free(etdump_data.buf); - return false; - } - ssize_t bytes_written = - write(etdump_file, (uint8_t*)etdump_data.buf, etdump_data.size); - if (bytes_written == -1) { - ET_LOG(Error, "Cannot write %s error: %d", path, errno); - close(etdump_file); - free(etdump_data.buf); - return false; - } else { - ET_LOG(Info, "ETDump written %zd bytes to %s.", bytes_written, path); - } - close(etdump_file); - free(etdump_data.buf); - return true; - } else { - ET_LOG(Error, "No ETDump data available!"); + if (!outputPath) { + ET_LOG(Error, "etdumpTo called with null outputPath"); + return false; } -#else - (void)path; -#endif - return false; + return etdump_to_path(outputPath->toStdString().c_str()); } - public: - facebook::jni::local_ref> getMethods() { const auto& names_result = module_->method_names(); if (!names_result.ok()) { @@ -585,6 +551,46 @@ class ExecuTorchJni : public facebook::jni::HybridClass { makeNativeMethod("getUsedBackends", ExecuTorchJni::getUsedBackends), }); } + + private: + jboolean etdump_to_path(const char* path) { +#ifdef EXECUTORCH_ANDROID_PROFILING + auto* tracer = module_->event_tracer(); + if (!tracer) { + ET_LOG(Error, "ETDump not available: no event tracer attached"); + return false; + } + auto* etdumpgen = static_cast(tracer); + auto etdump_data = etdumpgen->get_etdump_data(); + + if (etdump_data.buf != nullptr && etdump_data.size > 0) { + int etdump_file = open(path, O_WRONLY | O_CREAT | O_TRUNC, 0644); + if (etdump_file == -1) { + ET_LOG(Error, "Cannot create %s error: %d", path, errno); + free(etdump_data.buf); + return false; + } + ssize_t bytes_written = + write(etdump_file, (uint8_t*)etdump_data.buf, etdump_data.size); + if (bytes_written == -1) { + ET_LOG(Error, "Cannot write %s error: %d", path, errno); + close(etdump_file); + free(etdump_data.buf); + return false; + } else { + ET_LOG(Info, "ETDump written %zd bytes to %s.", bytes_written, path); + } + close(etdump_file); + free(etdump_data.buf); + return true; + } else { + ET_LOG(Error, "No ETDump data available!"); + } +#else + (void)path; +#endif + return false; + } }; } // namespace executorch::extension