From 4de6ecb5eb3fe124124ebdfb4b7c4b5cd21bf426 Mon Sep 17 00:00:00 2001 From: Ranjithkumar Ragavan <43761047+RanjithRagavan@users.noreply.github.com> Date: Tue, 7 Jul 2026 05:50:54 -0700 Subject: [PATCH] Support pure JVM JAR compilation and packaging for desktop platforms (Linux/macOS/Windows) --- extension/android/CMakeLists.txt | 122 ++++++++++-------- .../main/java/org/pytorch/executorch/Log.kt | 43 ++++++ .../java/org/pytorch/executorch/Tensor.kt | 1 - extension/android/executorch_jvm/build.gradle | 81 ++++++++++++ extension/android/gradle/libs.versions.toml | 1 + extension/android/jni/jni_layer.cpp | 2 +- extension/android/jni/log.cpp | 30 +++++ extension/android/settings.gradle | 7 +- 8 files changed, 233 insertions(+), 54 deletions(-) create mode 100644 extension/android/executorch_android/src/main/java/org/pytorch/executorch/Log.kt create mode 100644 extension/android/executorch_jvm/build.gradle diff --git a/extension/android/CMakeLists.txt b/extension/android/CMakeLists.txt index c94b95286d0..304806d27cf 100644 --- a/extension/android/CMakeLists.txt +++ b/extension/android/CMakeLists.txt @@ -13,64 +13,80 @@ if(NOT CMAKE_CXX_STANDARD) endif() if(NOT ANDROID) - message(FATAL_ERROR "This directory is for Android build only") -endif() + find_package(JNI REQUIRED) + if(NOT FBJNI_HEADERS_DIR OR NOT FBJNI_LIBRARY) + message(FATAL_ERROR "For non-Android platforms, please specify -DFBJNI_HEADERS_DIR=/path/to/fbjni/include and -DFBJNI_LIBRARY=/path/to/libfbjni.so") + endif() -set(EXECUTORCH_ROOT "${CMAKE_CURRENT_SOURCE_DIR}/../..") -include(${EXECUTORCH_ROOT}/tools/cmake/Utils.cmake) -set(_common_compile_options - $<$:/wd4996> - $<$>:-Wno-deprecated-declarations -fPIC> -) -if(NOT ANDROID_PLATFORM) - set(ANDROID_PLATFORM android-30) -endif() + add_library(fbjni SHARED IMPORTED) + set_target_properties( + fbjni + PROPERTIES + IMPORTED_LOCATION "${FBJNI_LIBRARY}" + ) + set(_common_compile_options + $<$:/wd4996> + $<$>:-Wno-deprecated-declarations -fPIC> + ) +else() + set(EXECUTORCH_ROOT "${CMAKE_CURRENT_SOURCE_DIR}/../..") + include(${EXECUTORCH_ROOT}/tools/cmake/Utils.cmake) + set(_common_compile_options + $<$:/wd4996> + $<$>:-Wno-deprecated-declarations -fPIC> + ) + if(NOT ANDROID_PLATFORM) + set(ANDROID_PLATFORM android-30) + endif() -# We need to download fbjni library from maven, and use its "prefab" library and -# headers, and link executorch library against that fbjni library. We don't know -# which NDK is used to compile fbjni, and we need to link our executorch library -# to the version which Android APK links against for runtime to ensure the -# libc++ dependencies are consistent. WARNING # Users need to use the SAME fbjni -# version here and in app gradle dependency for runtime compatibility! -if(NOT FBJNI_VERSION) - set(FBJNI_VERSION 0.7.0) -endif() + # We need to download fbjni library from maven, and use its "prefab" library and + # headers, and link executorch library against that fbjni library. We don't know + # which NDK is used to compile fbjni, and we need to link our executorch library + # to the version which Android APK links against for runtime to ensure the + # libc++ dependencies are consistent. WARNING # Users need to use the SAME fbjni + # version here and in app gradle dependency for runtime compatibility! + if(NOT FBJNI_VERSION) + set(FBJNI_VERSION 0.7.0) + endif() -set(FBJNI_AAR_URL - https://repo1.maven.org/maven2/com/facebook/fbjni/fbjni/${FBJNI_VERSION}/fbjni-${FBJNI_VERSION}.aar -) -set(FBJNI_DOWNLOAD_PATH ${CMAKE_CURRENT_BINARY_DIR}/third-party/fbjni/fbjni.aar) + set(FBJNI_AAR_URL + https://repo1.maven.org/maven2/com/facebook/fbjni/fbjni/${FBJNI_VERSION}/fbjni-${FBJNI_VERSION}.aar + ) + set(FBJNI_DOWNLOAD_PATH ${CMAKE_CURRENT_BINARY_DIR}/third-party/fbjni/fbjni.aar) -if(NOT EXISTS "${FBJNI_DOWNLOAD_PATH}") - file(DOWNLOAD "${FBJNI_AAR_URL}" "${FBJNI_DOWNLOAD_PATH}") -endif() + if(NOT EXISTS "${FBJNI_DOWNLOAD_PATH}") + file(DOWNLOAD "${FBJNI_AAR_URL}" "${FBJNI_DOWNLOAD_PATH}") + endif() -add_custom_command( - OUTPUT - "${CMAKE_CURRENT_BINARY_DIR}/third-party/fbjni/prefab/modules/fbjni/include/" - "${CMAKE_CURRENT_BINARY_DIR}/third-party/fbjni/prefab/modules/fbjni/libs/android.${ANDROID_ABI}/libfbjni.so" - COMMAND unzip -o ${FBJNI_DOWNLOAD_PATH} -d - ${CMAKE_CURRENT_BINARY_DIR}/third-party/fbjni - DEPENDS "${FBJNI_DOWNLOAD_PATH}" -) + add_custom_command( + OUTPUT + "${CMAKE_CURRENT_BINARY_DIR}/third-party/fbjni/prefab/modules/fbjni/include/" + "${CMAKE_CURRENT_BINARY_DIR}/third-party/fbjni/prefab/modules/fbjni/libs/android.${ANDROID_ABI}/libfbjni.so" + COMMAND unzip -o ${FBJNI_DOWNLOAD_PATH} -d + ${CMAKE_CURRENT_BINARY_DIR}/third-party/fbjni + DEPENDS "${FBJNI_DOWNLOAD_PATH}" + ) -add_custom_target( - fbjni_prefab - DEPENDS - "${CMAKE_CURRENT_BINARY_DIR}/third-party/fbjni/prefab/modules/fbjni/include/" - "${CMAKE_CURRENT_BINARY_DIR}/third-party/fbjni/prefab/modules/fbjni/libs/android.${ANDROID_ABI}/libfbjni.so" -) + add_custom_target( + fbjni_prefab + DEPENDS + "${CMAKE_CURRENT_BINARY_DIR}/third-party/fbjni/prefab/modules/fbjni/include/" + "${CMAKE_CURRENT_BINARY_DIR}/third-party/fbjni/prefab/modules/fbjni/libs/android.${ANDROID_ABI}/libfbjni.so" + ) -add_library(fbjni SHARED IMPORTED) -add_dependencies(fbjni fbjni_prefab) -set_target_properties( - fbjni - PROPERTIES - IMPORTED_LOCATION - "${CMAKE_CURRENT_BINARY_DIR}/third-party/fbjni/prefab/modules/fbjni/libs/android.${ANDROID_ABI}/libfbjni.so" -) + add_library(fbjni SHARED IMPORTED) + add_dependencies(fbjni fbjni_prefab) + set_target_properties( + fbjni + PROPERTIES + IMPORTED_LOCATION + "${CMAKE_CURRENT_BINARY_DIR}/third-party/fbjni/prefab/modules/fbjni/libs/android.${ANDROID_ABI}/libfbjni.so" + ) +endif() -executorch_target_link_options_shared_lib(executorch) +if(ANDROID) + executorch_target_link_options_shared_lib(executorch) +endif() add_library( executorch_jni SHARED jni/jni_layer.cpp jni/log.cpp jni/jni_layer_runtime.cpp @@ -248,4 +264,8 @@ target_link_options( ) target_link_options_gc_sections(executorch_jni) -target_link_libraries(executorch_jni ${link_libraries} log) +if(ANDROID) + target_link_libraries(executorch_jni ${link_libraries} log) +else() + target_link_libraries(executorch_jni ${link_libraries} ${JNI_LIBRARIES}) +endif() diff --git a/extension/android/executorch_android/src/main/java/org/pytorch/executorch/Log.kt b/extension/android/executorch_android/src/main/java/org/pytorch/executorch/Log.kt new file mode 100644 index 00000000000..6353ee050bc --- /dev/null +++ b/extension/android/executorch_android/src/main/java/org/pytorch/executorch/Log.kt @@ -0,0 +1,43 @@ +/* + * 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. + */ + +package org.pytorch.executorch + +import java.lang.reflect.Method + +/** + * Platform-agnostic logging helper that forwards logs to android.util.Log when running + * on Android, and falls back to standard error console logging when running on desktop JVMs. + */ +internal object Log { + private var androidLogMethod: Method? = null + private var lookupFailed = false + + fun e(tag: String, msg: String) { + if (!lookupFailed && androidLogMethod == null) { + try { + val logClass = Class.forName("android.util.Log") + androidLogMethod = logClass.getMethod("e", String::class.java, String::class.java) + } catch (e: Exception) { + lookupFailed = true + } + } + + val method = androidLogMethod + if (method != null) { + try { + method.invoke(null, tag, msg) + return + } catch (e: Exception) { + // Fallback to console printing if invocation fails + } + } + + System.err.println("[$tag] ERROR: $msg") + } +} diff --git a/extension/android/executorch_android/src/main/java/org/pytorch/executorch/Tensor.kt b/extension/android/executorch_android/src/main/java/org/pytorch/executorch/Tensor.kt index f2f3ebea214..433667ccae0 100644 --- a/extension/android/executorch_android/src/main/java/org/pytorch/executorch/Tensor.kt +++ b/extension/android/executorch_android/src/main/java/org/pytorch/executorch/Tensor.kt @@ -8,7 +8,6 @@ package org.pytorch.executorch -import android.util.Log import com.facebook.jni.HybridData import com.facebook.jni.annotations.DoNotStrip import java.nio.Buffer diff --git a/extension/android/executorch_jvm/build.gradle b/extension/android/executorch_jvm/build.gradle new file mode 100644 index 00000000000..b88b53174b7 --- /dev/null +++ b/extension/android/executorch_jvm/build.gradle @@ -0,0 +1,81 @@ +/* + * 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. + */ + +plugins { + alias(libs.plugins.jetbrains.kotlin.jvm) + id 'java-library' + id "com.vanniktech.maven.publish" version "0.31.0" +} + +def execuTorchVersion = System.properties['execuTorchVersion'] + +java { + sourceCompatibility = JavaVersion.VERSION_17 + targetCompatibility = JavaVersion.VERSION_17 +} + +repositories { + google() + mavenCentral() +} + +dependencies { + implementation "org.jetbrains.kotlin:kotlin-stdlib:${libs.versions.kotlin.get()}" + implementation "com.facebook.fbjni:fbjni-java-only:${fbjniJavaOnlyVersion}" + implementation "com.facebook.soloader:nativeloader:${soLoaderNativeLoaderVersion}" +} + +sourceSets { + main { + kotlin { + srcDirs = ['../executorch_android/src/main/java'] + exclude '**/androidTest/**' + exclude '**/test/**' + } + } +} + +compileKotlin { + kotlinOptions { + jvmTarget = "17" + freeCompilerArgs += ["-Xjvm-default=all"] + } +} + +mavenPublishing { + publishToMavenCentral() + signAllPublications() + + coordinates("org.pytorch", "executorch-jvm", execuTorchVersion ? execuTorchVersion : "1.2.0-SNAPSHOT") + + pom { + name = "ExecuTorch JVM" + description = "ExecuTorch Java/Kotlin bindings for standard desktop JVM (Linux, macOS, Windows)" + inceptionYear = "2026" + url = "https://github.com/pytorch/executorch/" + licenses { + license { + name = "BSD 3-Clause" + url = "https://github.com/pytorch/executorch/blob/main/LICENSE" + distribution = "https://github.com/pytorch/executorch/blob/main/LICENSE" + } + } + developers { + developer { + id = "pytorch" + name = "pytorch" + url = "https://github.com/pytorch/executorch/" + } + } + scm { + url = "https://github.com/pytorch/executorch.git" + connection = "scm:git:https://github.com/pytorch/executorch" + developerConnection = "scm:git:git@github.com:pytorch/executorch.git" + } + } +} diff --git a/extension/android/gradle/libs.versions.toml b/extension/android/gradle/libs.versions.toml index fcd6a356536..d051c046a22 100644 --- a/extension/android/gradle/libs.versions.toml +++ b/extension/android/gradle/libs.versions.toml @@ -15,3 +15,4 @@ junit = { module = "junit:junit", version.ref = "junit" } core-ktx = { group = "androidx.core", name = "core-ktx", version.ref = "core-ktx" } [plugins] jetbrains-kotlin-android = { id = "org.jetbrains.kotlin.android", version.ref = "kotlin" } +jetbrains-kotlin-jvm = { id = "org.jetbrains.kotlin.jvm", version.ref = "kotlin" } diff --git a/extension/android/jni/jni_layer.cpp b/extension/android/jni/jni_layer.cpp index 2459746df0d..8adaee00afe 100644 --- a/extension/android/jni/jni_layer.cpp +++ b/extension/android/jni/jni_layer.cpp @@ -477,7 +477,7 @@ class ExecuTorchJni : public facebook::jni::HybridClass { return ret; #else - return facebook::jni::JArrayClass::newArray(0); + return facebook::jni::JArrayClass::newArray(0); #endif } diff --git a/extension/android/jni/log.cpp b/extension/android/jni/log.cpp index 663198e1271..d8c5288d44e 100644 --- a/extension/android/jni/log.cpp +++ b/extension/android/jni/log.cpp @@ -66,4 +66,34 @@ void access_log_buffer(std::function&)> accessor) { } // namespace executorch::extension +#else + +#include + +namespace executorch::extension { + +void access_log_buffer(std::function&)> accessor) { + // No-op for non-Android + (void)accessor; +} + +} // namespace executorch::extension + +void et_pal_emit_log_message( + et_timestamp_t timestamp, + et_pal_log_level_t level, + const char* filename, + const char* function, + size_t line, + const char* message, + size_t length) { + (void)timestamp; + (void)filename; + (void)function; + (void)line; + (void)length; + // Fallback console log for JVM desktop + fprintf(stderr, "[ExecuTorch JNI %c] %s\n", level, message); +} + #endif diff --git a/extension/android/settings.gradle b/extension/android/settings.gradle index 95d46203058..83a62397671 100644 --- a/extension/android/settings.gradle +++ b/extension/android/settings.gradle @@ -21,4 +21,9 @@ plugins { rootProject.name = 'executorch' -include('executorch_android') +if (System.getenv("ANDROID_HOME") != null || System.getenv("ANDROID_SDK_ROOT") != null || new java.io.File(rootDir, "local.properties").exists()) { + include('executorch_android') +} else { + logger.warn("WARNING: ANDROID_HOME is not set, skipping Android subproject.") +} +include('executorch_jvm')