diff --git a/.gitignore b/.gitignore index 188abcc9d1d6a..99fcb0e731006 100644 --- a/.gitignore +++ b/.gitignore @@ -41,6 +41,8 @@ onnxprofile_profile_test_*.json /csharp/src/Microsoft.ML.OnnxRuntime/Microsoft.ML.OnnxRuntime.props cmake/external/FeaturizersLibrary/ # Java specific ignores -java/src/main/native/ai_onnxruntime_*.h +java/gradlew +java/gradlew.bat +java/gradle java/.gradle diff --git a/cmake/onnxruntime_java.cmake b/cmake/onnxruntime_java.cmake index 5abda326756ab..077a61e0903ef 100644 --- a/cmake/onnxruntime_java.cmake +++ b/cmake/onnxruntime_java.cmake @@ -12,99 +12,61 @@ include_directories(${JNI_INCLUDE_DIRS}) set(CMAKE_C_FLAGS "${CMAKE_C_FLAGS} -std=c11") set(JAVA_ROOT ${REPO_ROOT}/java) -set(CMAKE_JAVA_COMPILE_FLAGS "-source" "1.8" "-target" "1.8" "-encoding" "UTF-8") +set(JAVA_OUTPUT_DIR ${CMAKE_CURRENT_BINARY_DIR}/java) if (onnxruntime_RUN_ONNX_TESTS) set(JAVA_DEPENDS onnxruntime ${test_data_target}) else() set(JAVA_DEPENDS onnxruntime) endif() -# Specify the Java source files -set(onnxruntime4j_src - ${REPO_ROOT}/java/src/main/java/ai/onnxruntime/MapInfo.java - ${REPO_ROOT}/java/src/main/java/ai/onnxruntime/NodeInfo.java - ${REPO_ROOT}/java/src/main/java/ai/onnxruntime/OnnxRuntime.java - ${REPO_ROOT}/java/src/main/java/ai/onnxruntime/OnnxJavaType.java - ${REPO_ROOT}/java/src/main/java/ai/onnxruntime/OnnxMap.java - ${REPO_ROOT}/java/src/main/java/ai/onnxruntime/OnnxSequence.java - ${REPO_ROOT}/java/src/main/java/ai/onnxruntime/OnnxTensor.java - ${REPO_ROOT}/java/src/main/java/ai/onnxruntime/OnnxValue.java - ${REPO_ROOT}/java/src/main/java/ai/onnxruntime/OrtAllocator.java - ${REPO_ROOT}/java/src/main/java/ai/onnxruntime/OrtEnvironment.java - ${REPO_ROOT}/java/src/main/java/ai/onnxruntime/OrtException.java - ${REPO_ROOT}/java/src/main/java/ai/onnxruntime/OrtSession.java - ${REPO_ROOT}/java/src/main/java/ai/onnxruntime/OrtUtil.java - ${REPO_ROOT}/java/src/main/java/ai/onnxruntime/package-info.java - ${REPO_ROOT}/java/src/main/java/ai/onnxruntime/SequenceInfo.java - ${REPO_ROOT}/java/src/main/java/ai/onnxruntime/TensorInfo.java - ${REPO_ROOT}/java/src/main/java/ai/onnxruntime/ValueInfo.java - ) +# use the gradle wrapper if it exists +if(EXISTS "${JAVA_ROOT}/gradlew") + set(GRADLE_EXECUTABLE "${JAVA_ROOT}/gradlew") +else() + # fall back to gradle on our PATH + find_program(GRADLE_EXECUTABLE gradle) + if(NOT GRADLE_EXECUTABLE) + message(SEND_ERROR "Gradle installation not found") + endif() +endif() +message(STATUS "Using gradle: ${GRADLE_EXECUTABLE}") -# Build the jar and generate the native headers -add_jar(onnxruntime4j SOURCES ${onnxruntime4j_src} VERSION ${ORT_VERSION} GENERATE_NATIVE_HEADERS onnxruntime4j_generated DESTINATION ${REPO_ROOT}/java/src/main/native/) +# Specify the Java source files +file(GLOB_RECURSE onnxruntime4j_gradle_files "${JAVA_ROOT}/*.gradle") +file(GLOB_RECURSE onnxruntime4j_src "${JAVA_ROOT}/src/main/java/ai/onnxruntime/*.java") +set(JAVA_OUTPUT_JAR ${JAVA_ROOT}/build/libs/onnxruntime.jar) +# this jar is solely used to signalling mechanism for dependency management in CMake +# if any of the Java sources change, the jar (and generated headers) will be regenerated and the onnxruntime4j_jni target will be rebuilt +add_custom_command(OUTPUT ${JAVA_OUTPUT_JAR} COMMAND ${GRADLE_EXECUTABLE} clean jar WORKING_DIRECTORY ${JAVA_ROOT} DEPENDS ${onnxruntime4j_gradle_files} ${onnxruntime4j_src}) +add_custom_target(onnxruntime4j DEPENDS ${JAVA_OUTPUT_JAR}) +set_source_files_properties(${JAVA_OUTPUT_JAR} PROPERTIES GENERATED TRUE) +set_property(TARGET onnxruntime4j APPEND PROPERTY ADDITIONAL_CLEAN_FILES "${JAVA_OUTPUT_DIR}") -# Specify the native sources (without the generated headers) +# Specify the native sources file(GLOB onnxruntime4j_native_src - "${REPO_ROOT}/java/src/main/native/*.c" - "${REPO_ROOT}/java/src/main/native/OrtJniUtil.h" + "${JAVA_ROOT}/src/main/native/*.c" + "${JAVA_ROOT}/src/main/native/*.h" "${REPO_ROOT}/include/onnxruntime/core/session/*.h" ) - # Build the JNI library -add_library(onnxruntime4j_jni SHARED ${onnxruntime4j_native_src} ${onnxruntime4j_generated}) +add_library(onnxruntime4j_jni SHARED ${onnxruntime4j_native_src}) +# depend on java sources. if they change, the JNI should recompile +add_dependencies(onnxruntime4j_jni onnxruntime4j) onnxruntime_add_include_to_target(onnxruntime4j_jni onnxruntime_session) -target_include_directories(onnxruntime4j_jni PRIVATE ${REPO_ROOT}/include ${REPO_ROOT}/java/src/main/native) -target_link_libraries(onnxruntime4j_jni PUBLIC onnxruntime onnxruntime4j_generated) - -# Now the jar, jni binary and shared lib binary have been built, now to build the jar with the binaries added. - -# This blob creates the new jar name -get_property(onnxruntime_jar_name TARGET onnxruntime4j PROPERTY JAR_FILE) -get_filename_component(onnxruntime_jar_abs ${onnxruntime_jar_name} ABSOLUTE) -get_filename_component(jar_path ${onnxruntime_jar_abs} DIRECTORY) -set(onnxruntime_jar_binaries_name "${jar_path}/onnxruntime4j-${ORT_VERSION}-with-binaries.jar") -set(onnxruntime_jar_binaries_platform "$") - -# Copy the current jar -add_custom_command(TARGET onnxruntime4j_jni PRE_BUILD - COMMAND ${CMAKE_COMMAND} -E copy - ${onnxruntime_jar_name} - ${onnxruntime_jar_binaries_platform}) - -# Make a temp directory to store the binaries -add_custom_command(TARGET onnxruntime4j_jni POST_BUILD - COMMAND ${CMAKE_COMMAND} -E make_directory "${CMAKE_CURRENT_BINARY_DIR}/java-libs/lib") - -# Copy the binaries -add_custom_command(TARGET onnxruntime4j_jni POST_BUILD COMMAND ${CMAKE_COMMAND} -E copy "$" ${CMAKE_CURRENT_BINARY_DIR}/java-libs/lib/) - -if (WIN32) -add_custom_command(TARGET onnxruntime4j_jni POST_BUILD COMMAND ${CMAKE_COMMAND} -E copy "$" ${CMAKE_CURRENT_BINARY_DIR}/java-libs/lib/) -# Update the with-binaries jar so it includes the binaries -add_custom_command( - TARGET onnxruntime4j_jni POST_BUILD - COMMAND ${Java_JAR_EXECUTABLE} -uf ${onnxruntime_jar_binaries_platform} -C ${CMAKE_CURRENT_BINARY_DIR}/java-libs lib/$ -C ${CMAKE_CURRENT_BINARY_DIR}/java-libs lib/$ - DEPENDS onnxruntime4j - COMMENT "Rebuilding Java archive ${_JAVA_TARGET_OUTPUT_NAME}" - VERBATIM - ) -else () -add_custom_command(TARGET onnxruntime4j_jni POST_BUILD COMMAND ${CMAKE_COMMAND} -E copy "$" ${CMAKE_CURRENT_BINARY_DIR}/java-libs/lib/) -# Update the with-binaries jar so it includes the binaries -add_custom_command( - TARGET onnxruntime4j_jni POST_BUILD - COMMAND ${Java_JAR_EXECUTABLE} -uf ${onnxruntime_jar_binaries_platform} -C ${CMAKE_CURRENT_BINARY_DIR}/java-libs lib/$ -C ${CMAKE_CURRENT_BINARY_DIR}/java-libs lib/$ - DEPENDS onnxruntime4j - COMMENT "Rebuilding Java archive ${_JAVA_TARGET_OUTPUT_NAME}" - VERBATIM - ) -endif() +# the JNI headers are generated in the onnxruntime4j target +target_include_directories(onnxruntime4j_jni PRIVATE ${REPO_ROOT}/include ${JAVA_ROOT}/build/headers) +target_link_libraries(onnxruntime4j_jni PUBLIC onnxruntime) -create_javadoc(onnxruntime4j_javadoc - FILES ${onnxruntime4j_src} - DOCTITLE "Onnx Runtime Java API" - WINDOWTITLE "OnnxRuntime-Java-API" - AUTHOR FALSE - USE TRUE - VERSION FALSE - ) +# expose native libraries to the gradle build process +file(MAKE_DIRECTORY ${JAVA_OUTPUT_DIR}/build) +set(JAVA_PACKAGE_DIR ai/onnxruntime/native/) +set(JAVA_NATIVE_LIB_DIR ${JAVA_OUTPUT_DIR}/native-lib) +set(JAVA_NATIVE_JNI_DIR ${JAVA_OUTPUT_DIR}/native-jni) +set(JAVA_PACKAGE_LIB_DIR ${JAVA_NATIVE_LIB_DIR}/${JAVA_PACKAGE_DIR}) +set(JAVA_PACKAGE_JNI_DIR ${JAVA_NATIVE_JNI_DIR}/${JAVA_PACKAGE_DIR}) +file(MAKE_DIRECTORY ${JAVA_PACKAGE_LIB_DIR}) +file(MAKE_DIRECTORY ${JAVA_PACKAGE_JNI_DIR}) +add_custom_command(TARGET onnxruntime4j_jni POST_BUILD COMMAND ${CMAKE_COMMAND} -E create_symlink $ ${JAVA_PACKAGE_LIB_DIR}/$) +add_custom_command(TARGET onnxruntime4j_jni POST_BUILD COMMAND ${CMAKE_COMMAND} -E create_symlink $ ${JAVA_PACKAGE_JNI_DIR}/$) +# run the build process (this copies the results back into CMAKE_CURRENT_BINARY_DIR) +add_custom_command(TARGET onnxruntime4j_jni POST_BUILD COMMAND ${GRADLE_EXECUTABLE} cmakeBuild -DcmakeBuildDir=${CMAKE_CURRENT_BINARY_DIR} WORKING_DIRECTORY ${JAVA_ROOT}) \ No newline at end of file diff --git a/cmake/onnxruntime_unittests.cmake b/cmake/onnxruntime_unittests.cmake index 2e7546c90f525..4c9ae9d760c96 100644 --- a/cmake/onnxruntime_unittests.cmake +++ b/cmake/onnxruntime_unittests.cmake @@ -691,35 +691,7 @@ set_property(TARGET custom_op_library APPEND_STRING PROPERTY LINK_FLAGS ${ONNXRU if (onnxruntime_BUILD_JAVA) message(STATUS "Running Java tests") - # Build and run tests - set(onnxruntime4j_test_src - ${REPO_ROOT}/java/src/test/java/ai/onnxruntime/InferenceTest.java - ${REPO_ROOT}/java/src/test/java/ai/onnxruntime/TestHelpers.java - ${REPO_ROOT}/java/src/test/java/ai/onnxruntime/OnnxMl.java - ${REPO_ROOT}/java/src/test/java/ai/onnxruntime/UtilTest.java - ) - - # Create test directories - file(MAKE_DIRECTORY "${CMAKE_CURRENT_BINARY_DIR}/java-tests/") - file(MAKE_DIRECTORY "${CMAKE_CURRENT_BINARY_DIR}/java-tests/results") - - # Download test dependencies - if (NOT EXISTS ${CMAKE_CURRENT_BINARY_DIR}/java-tests/junit-platform-console-standalone-1.5.2.jar) - message("Downloading JUnit 5") - file(DOWNLOAD https://repo1.maven.org/maven2/org/junit/platform/junit-platform-console-standalone/1.5.2/junit-platform-console-standalone-1.5.2.jar ${CMAKE_CURRENT_BINARY_DIR}/java-tests/junit-platform-console-standalone-1.5.2.jar EXPECTED_HASH SHA1=8d937d2b461018a876836362b256629f4da5feb1) - endif() - - if (NOT EXISTS ${CMAKE_CURRENT_BINARY_DIR}/java-tests/protobuf-java-3.10.0.jar) - message("Downloading protobuf-java 3.10.0") - file(DOWNLOAD https://repo1.maven.org/maven2/com/google/protobuf/protobuf-java/3.10.0/protobuf-java-3.10.0.jar ${CMAKE_CURRENT_BINARY_DIR}/java-tests/protobuf-java-3.10.0.jar EXPECTED_HASH SHA1=410b61dd0088aab4caa05739558d43df248958c9) - endif() - - # Build the test jar - add_jar(onnxruntime4j_test SOURCES ${onnxruntime4j_test_src} VERSION ${ORT_VERSION} INCLUDE_JARS ${onnxruntime_jar_name} ${CMAKE_CURRENT_BINARY_DIR}/java-tests/junit-platform-console-standalone-1.5.2.jar ${CMAKE_CURRENT_BINARY_DIR}/java-tests/protobuf-java-3.10.0.jar) - - add_dependencies(onnxruntime4j_test onnxruntime4j_jni onnxruntime4j) - get_property(onnxruntime_test_jar_name TARGET onnxruntime4j_test PROPERTY JAR_FILE) - - # Run the tests with JUnit's console launcher - add_test(NAME java-api COMMAND ${Java_JAVA_EXECUTABLE} -jar ${CMAKE_CURRENT_BINARY_DIR}/java-tests/junit-platform-console-standalone-1.5.2.jar -cp ${CMAKE_CURRENT_BINARY_DIR}/java-tests/protobuf-java-3.10.0.jar -cp ${onnxruntime_test_jar_name} -cp ${onnxruntime_jar_binaries_platform} --scan-class-path --fail-if-no-tests --reports-dir=${CMAKE_CURRENT_BINARY_DIR}/java-tests/results --disable-banner WORKING_DIRECTORY ${REPO_ROOT}) + # delegate to gradle's test runner + add_test(NAME onnxruntime4j_test COMMAND ${GRADLE_EXECUTABLE} cmakeCheck -DcmakeBuildDir=${CMAKE_CURRENT_BINARY_DIR} WORKING_DIRECTORY ${REPO_ROOT}/java) + set_property(TEST onnxruntime4j_test APPEND PROPERTY DEPENDS onnxruntime4j_jni) endif() diff --git a/java/README.md b/java/README.md new file mode 100644 index 0000000000000..3583955f86ae9 --- /dev/null +++ b/java/README.md @@ -0,0 +1,80 @@ +# ONNX Runtime Java API + +This directory contains the Java language binding for the ONNX runtime. +Java Native Interface (JNI) is used to allow for seamless calls to ONNX runtime from Java. + +## Usage + +TBD: maven distribution + +The minimum supported Java Runtime is version 8. + +An example implementation is located in [src/test/java/sample/ScoreMNIST.java](src/test/java/sample/ScoreMNIST.java) + +This project can be built manually using the instructions below. + +### Building + +Use the main project's [build instructions](../BUILD.md) with the `--build_java` option. + +#### Requirements + +JDK version 8 or later is required. +The [Gradle](https://gradle.org/) build system is required and used here to manage the Java project's dependency management, compilation, testing, and assembly. +You may use your system Gradle installation installed on your PATH. +Version 6 or newer is recommended. +Optionally, you may use your own Gradle [wrapper](https://docs.gradle.org/current/userguide/gradle_wrapper.html) which will be locked to a version specified in the `build.gradle` configuration. +This can be done once by using system Gradle installation to invoke the wrapper task in the java project's directory: `cd REPO_ROOT/java && gradle wrapper` +Any installed wrapper is gitignored. + +#### Build Output + +The build will generate output in `$REPO_ROOT/build/$OS/$CONFIGURATION/java/build`: + +* `docs/javadoc/` - HTML javadoc +* `reports/` - detailed test results and other reports +* `libs/onnxruntime.jar` - JAR with classes, depends on `onnxruntime-jni.jar` and `onnxruntime-lib.jar ` +* `libs/onnxruntime-jni.jar`- JAR with JNI shared library +* `libs/onnxruntime-lib.jar` - JAR with onnxruntime shared library +* `libs/onnxruntime-all.jar` - the 3 preceding jars all combined: JAR with classes, JNI shared library, and onnxruntime shared library + +The reason the shared libraries are split out like that is that users can mix and match to suit their use case: + +* To support a single OS/Architecture without any dependencies, use `libs/onnxruntime-all.jar`. +* To support cross-platform: bundle a single `libs/onnxruntime.jar` and with all of the respective `libs/onnxruntime-jni.jar` and `libs/onnxruntime-lib.jar` for all of the desired OS/Architectures. +* To support use case where an onnxruntime shared library will reside in the system's library search path: bundle a single `libs/onnxruntime.jar` and with all of the `libs/onnxruntime-jni.jar`. The onnxruntime shared library should be loaded using one of the other methods described in the "Advanced Loading" section below. + +#### Build System Overview + +The main CMake build system delegates building and testing to Gradle. +This allows the CMake system to ensure all of the C/C++ compilation is achieved prior to the Java build. +The Java build depends on C/C++ onnxruntime shared library and a C JNI shared library (source located in the `src/main/native` directory). +The JNI shared library is the glue that allows for Java to call functions in onnxruntime shared library. +Given the fact that CMake injects native dependencies during CMake builds, some gradle tasks (primarily, `build`, `test`, and `check`) may fail. + +When running the build script, CMake will compile the `onnxruntime` target and the JNI glue `onnxruntime4j_jni` target and expose the resulting libraries in a place where Gradle can ingest them. +Upon successful compilation of those targets, a special Gradle task to build will be executed. The results will be placed in the output directory stated above. + +### Advanced Loading + +The default behavior is to load the shared libraries using classpath resources. +If your use case requires custom loading of the shared libraries, please consult the javadoc in the [package-info.java](src/main/java/ai/onnxruntime/package-info.java) or [OnnxRuntime.java](src/main/java/ai/onnxruntime/OnnxRuntime.java) files. + +## Development + +### Code Formatting + +[Spotless](https://github.com/diffplug/spotless/tree/master/plugin-gradle) is used to keep the code properly formatted. +Gradle's `spotlessCheck` task will show any misformatted code. +Gradle's `spotlessApply` task will try to fix the formatting. +Misformatted code will raise failures when checks are ran during test run. + +### JNI Headers + +When adding or updating native methods in the Java files, it may be necessary to examine the relevant JNI headers in `build/headers/ai_onnxruntime*.h`. +These files can be manually generated using Gradle's `compileJava` task which will compile the Java and update the header files accordingly. +Then the corresponding C files in `./src/main/native/ai_onnxruntime*.c` may be updated and the build can be ran. + +### Dependencies + +The Java API does not have any runtime or compile dependencies currently. diff --git a/java/build.gradle b/java/build.gradle new file mode 100644 index 0000000000000..0c1c38f67c0a2 --- /dev/null +++ b/java/build.gradle @@ -0,0 +1,117 @@ +plugins { + id 'java' + id 'com.diffplug.gradle.spotless' version '3.26.0' +} + +allprojects { + repositories { + mavenCentral() + } +} + +java { + sourceCompatibility = JavaVersion.VERSION_1_8 + targetCompatibility = JavaVersion.VERSION_1_8 + withJavadocJar() + withSourcesJar() +} + +wrapper { + gradleVersion = '6.1.1' +} + +spotless { + //java { + // removeUnusedImports() + // googleJavaFormat() + //} + format 'gradle', { + target '**/*.gradle' + trimTrailingWhitespace() + indentWithTabs() + } +} + +// cmake runs will inform us of the build directory of the current run +def cmakeBuildDir = System.properties['cmakeBuildDir'] +def cmakeJavaDir = "${cmakeBuildDir}/java" +def cmakeNativeLibDir = "${cmakeJavaDir}/native-lib" +def cmakeNativeJniDir = "${cmakeJavaDir}/native-jni" +def cmakeBuildOutputDir = "${cmakeJavaDir}/build" + +compileJava { + options.compilerArgs += ["-h", "${project.buildDir}/headers/"] +} + +sourceSets.test { + // add test resource files + resources.srcDirs += [ + "${rootProject.projectDir}/../csharp/testdata", + "${rootProject.projectDir}/../onnxruntime/test/testdata" + ] + if (cmakeBuildDir != null) { + // add compiled native libs + resources.srcDirs += [ + cmakeNativeLibDir, + cmakeNativeJniDir + ] + } +} + +if (cmakeBuildDir != null) { + // generate tasks to be called from cmake + + task jniJar(type: Jar) { + classifier = 'jni' + from cmakeNativeJniDir + } + + task libJar(type: Jar) { + classifier = 'lib' + from cmakeNativeLibDir + } + + task allJar(type: Jar) { + classifier = 'all' + from sourceSets.main.output + from cmakeNativeJniDir + from cmakeNativeLibDir + } + + task cmakeBuild(type: Copy) { + from project.buildDir + include 'libs/**' + include 'docs/**' + into cmakeBuildOutputDir + } + cmakeBuild.dependsOn jar + cmakeBuild.dependsOn jniJar + cmakeBuild.dependsOn libJar + cmakeBuild.dependsOn allJar + cmakeBuild.dependsOn sourcesJar + cmakeBuild.dependsOn javadocJar + cmakeBuild.dependsOn javadoc + + + task cmakeCheck(type: Copy) { + from project.buildDir + include 'reports/**' + into cmakeBuildOutputDir + } + cmakeCheck.dependsOn check + +} + + +dependencies { + testImplementation 'org.junit.jupiter:junit-jupiter-api:5.1.1' + testRuntimeOnly 'org.junit.jupiter:junit-jupiter-engine:5.1.1' + testImplementation 'com.google.protobuf:protobuf-java:3.10.0' +} + +test { + useJUnitPlatform() + testLogging { + events "passed", "skipped", "failed" + } +} diff --git a/java/settings.gradle b/java/settings.gradle new file mode 100644 index 0000000000000..0df82327d968e --- /dev/null +++ b/java/settings.gradle @@ -0,0 +1 @@ +rootProject.name = 'onnxruntime' \ No newline at end of file diff --git a/java/src/main/java/ai/onnxruntime/OnnxRuntime.java b/java/src/main/java/ai/onnxruntime/OnnxRuntime.java index b1108717cea9f..2626cdef11290 100644 --- a/java/src/main/java/ai/onnxruntime/OnnxRuntime.java +++ b/java/src/main/java/ai/onnxruntime/OnnxRuntime.java @@ -5,175 +5,135 @@ package ai.onnxruntime; import java.io.File; -import java.io.FileNotFoundException; import java.io.FileOutputStream; import java.io.IOException; import java.io.InputStream; -import java.util.Arrays; -import java.util.List; -import java.util.Properties; +import java.nio.file.Files; +import java.nio.file.Path; import java.util.logging.Level; import java.util.logging.Logger; -/** - * Static loader for the JNI binding. - */ +/** Static loader for the JNI binding. No public API, but called from various classes in this package to ensure shared libraries are properly loaded. */ final class OnnxRuntime { - private static final Logger logger = Logger.getLogger(OnnxRuntime.class.getName()); + private static final Logger logger = Logger.getLogger(OnnxRuntime.class.getName()); - // The initial release of the ORT API. - private static final int ORT_API_VERSION_1 = 1; + // The initial release of the ORT API. + private static final int ORT_API_VERSION_1 = 1; - /** - * Turns on debug logging during library loading. - */ - public static final String LIBRARY_LOAD_LOGGING = "ORT_LOAD_LOGGING"; + /** The short name of the ONNX runtime shared library */ + static final String ONNXRUNTIME_LIBRARY_NAME = "onnxruntime"; + /** The short name of the ONNX runtime JNI shared library */ + static final String ONNXRUNTIME_JNI_LIBRARY_NAME = "onnxruntime4j_jni"; - /** - * Specifies that the libraries should be loaded from java.library.path rather than unzipped from the jar file. - */ - public static final String LOAD_LIBRARY_PATH = "ORT_LOAD_FROM_LIBRARY_PATH"; + private static boolean loaded = false; - private static boolean loaded = false; + /** The API handle. */ + static long ortApiHandle; - /** - * The API handle. - */ - static long ortApiHandle; + private OnnxRuntime() {} - /** - * Library names stored in the jar. - */ - private static final List libraryNames = Arrays.asList("onnxruntime","onnxruntime4j_jni"); + /** + * Loads the native C library. + * + * @throws IOException If it can't write to disk to copy out the library from the jar file. + */ + static synchronized void init() throws IOException { + if (loaded) { + return; + } + Path tempDirectory = Files.createTempDirectory("onnxruntime-java"); + try { + load(tempDirectory, ONNXRUNTIME_LIBRARY_NAME); + load(tempDirectory, ONNXRUNTIME_JNI_LIBRARY_NAME); + ortApiHandle = initialiseAPIBase(ORT_API_VERSION_1); + loaded = true; + } finally { + cleanUp(tempDirectory.toFile()); + } + } - private OnnxRuntime() {} + /** + * Attempt to remove a file and then mark for delete on exit if it cannot be deleted at this point in time. + * + * @param file The file to remove. + */ + private static void cleanUp(File file) { + if (!file.exists()) { + return; + } + logger.log(Level.FINE, "Deleting " + file); + if (!file.delete()) { + logger.log(Level.FINE, "Deleting " + file + " on exit"); + file.deleteOnExit(); + } + } - /** - * Loads the native C library. - * @throws IOException If it can't write to disk to copy out the library from the jar file. - */ - static synchronized void init() throws IOException { - if (!loaded) { - // Check system properties for load time configuration. - Properties props = System.getProperties(); - boolean debug = props.containsKey(LIBRARY_LOAD_LOGGING); - boolean loadLibraryPath = props.containsKey(LOAD_LIBRARY_PATH); - if (loadLibraryPath) { - if (debug) { - logger.info("Loading from java.library.path"); - } - try { - for (String libraryName : libraryNames) { - if (debug) { - logger.info("Loading " + libraryName + " from java.library.path"); - } - System.loadLibrary(libraryName); - } - } catch (UnsatisfiedLinkError e) { - logger.log(Level.SEVERE, "Failed to load onnx-runtime library from library path."); - throw e; - } - } else { - if (debug) { - logger.info("Loading from classpath resource"); - } - try { - for (String libraryName : libraryNames) { - try { - // This code path is used during testing. - String libraryFromJar = "/" + System.mapLibraryName(libraryName); - if (debug) { - logger.info("Attempting to load library from classpath using " + libraryFromJar); - } - String tempLibraryPath = createTempFileFromResource(libraryFromJar, debug); - if (debug) { - logger.info("Copied resource " + libraryFromJar + " to location " + tempLibraryPath); - } - System.load(tempLibraryPath); - } catch (Exception e) { - if (debug) { - logger.info("Failed to load from testing location, looking for /lib/"); - } - String libraryFromJar = "/lib/" + System.mapLibraryName(libraryName); - if (debug) { - logger.info("Attempting to load library from classpath using " + libraryFromJar); - } - String tempLibraryPath = createTempFileFromResource(libraryFromJar, debug); - if (debug) { - logger.info("Copied resource " + libraryFromJar + " to location " + tempLibraryPath); - } - System.load(tempLibraryPath); - } - } - } catch (IOException e) { - logger.log(Level.SEVERE, "Failed to load onnx-runtime library from jar"); - throw e; - } - } - ortApiHandle = initialiseAPIBase(ORT_API_VERSION_1); - loaded = true; - } + /** + * Load a shared library by name. + * + * @param tempDirectory The temp directory to write the library resource to. + * @param library The bare name of the library. + * @throws IOException If the file failed to read or write. + */ + private static void load(Path tempDirectory, String library) throws IOException { + // 1) The user may skip loading of this library: + String skip = System.getProperty("onnxruntime.native." + library + ".skip"); + if (Boolean.TRUE.toString().equalsIgnoreCase(skip)) { + logger.log(Level.FINE, "Skipping load of native library '" + library + "'"); + return; } - /** - * Copies out the named file from the class path into a temporary directory so it can be loaded - * by {@link System#load}. - *

- * The file is marked delete on exit. Throws {@link IllegalArgumentException} if the - * supplied path is not absolute. - * @param path The path to the file in the classpath. - * @param debugLogging If true turn on debug logging. - * @return The path to the extracted file on disk. - * @throws IOException If the file failed to read or write. - */ - private static String createTempFileFromResource(String path, boolean debugLogging) throws IOException { - if (!path.startsWith("/")) { - throw new IllegalArgumentException("The path has to be absolute (start with '/')."); - } else { - String[] parts = path.split("/"); - String filename = parts.length > 1 ? parts[parts.length - 1] : null; - String prefix = ""; - String suffix = null; - if (filename != null) { - parts = filename.split("\\.", 2); - prefix = parts[0]; - suffix = parts.length > 1 ? "." + parts[parts.length - 1] : null; - } + // 2) The user may explicitly specify the path to their shared library: + String libraryPathProperty = System.getProperty("onnxruntime.native." + library + ".path"); + if (libraryPathProperty != null) { + logger.log(Level.FINE, "Attempting to load native library '" + library + "' from specified path: " + libraryPathProperty); + File libraryFile = new File(libraryPathProperty); + String libraryFilePath = libraryFile.getAbsolutePath(); + if (!libraryFile.exists()) { + throw new IOException("Native library '" + library + "' not found at "+ libraryFilePath); + } + System.load(libraryFilePath); + logger.log(Level.FINE, "Loaded native library '" + library + "' from specified path"); + return; + } - if (filename != null && prefix.length() >= 3) { - File temp = File.createTempFile(prefix, suffix); - if (debugLogging) { - logger.info("Writing " + path + " out to " + temp.getAbsolutePath()); - } - temp.deleteOnExit(); - if (!temp.exists()) { - throw new FileNotFoundException("File " + temp.getAbsolutePath() + " does not exist."); - } else { - byte[] buffer = new byte[1024]; - try (InputStream is = OnnxRuntime.class.getResourceAsStream(path)) { - if (is == null) { - throw new FileNotFoundException("File " + path + " was not found inside JAR."); - } else { - int readBytes; - try (FileOutputStream os = new FileOutputStream(temp)) { - while ((readBytes = is.read(buffer)) != -1) { - os.write(buffer, 0, readBytes); - } - } - return temp.getAbsolutePath(); - } - } - } - } else { - throw new IllegalArgumentException("The filename has to be at least 3 characters long."); - } + // 3) try loading from resources or library path: + // generate a platform specific library name + // replace Mac's jnilib extension to dylib + String libraryFileName = System.mapLibraryName(library).replace("jnilib", "dylib"); + String resourcePath = "/ai/onnxruntime/native/" + libraryFileName; + File tempFile = tempDirectory.resolve(libraryFileName).toFile(); + try(InputStream is = OnnxRuntime.class.getResourceAsStream(resourcePath)){ + if (is == null) { + // 3a) Not found in resources, load from library path + logger.log(Level.FINE, "Attempting to load native library '" + library + "' from library path"); + System.loadLibrary(library); + logger.log(Level.FINE, "Loaded native library '" + library + "' from library path"); + } else { + // 3b) Found in resources, load via temporary file + logger.log( + Level.FINE, + "Attempting to load native library '" + library + "' from resource path " + resourcePath + " copying to " + tempFile); + byte[] buffer = new byte[1024]; + int readBytes; + try (FileOutputStream os = new FileOutputStream(tempFile)) { + while ((readBytes = is.read(buffer)) != -1) { + os.write(buffer, 0, readBytes); + } } + System.load(tempFile.getAbsolutePath()); + logger.log(Level.FINE, "Loaded native library '" + library + "' from resource path"); + } + } finally { + cleanUp(tempFile); } + } - /** - * Get a reference to the API struct. - * @param apiVersionNumber The API version to use. - * @return A pointer to the API struct. - */ - private static native long initialiseAPIBase(int apiVersionNumber); + /** + * Get a reference to the API struct. + * + * @param apiVersionNumber The API version to use. + * @return A pointer to the API struct. + */ + private static native long initialiseAPIBase(int apiVersionNumber); } diff --git a/java/src/main/java/ai/onnxruntime/package-info.java b/java/src/main/java/ai/onnxruntime/package-info.java index 9d6786cd59081..1a4c082a190dd 100644 --- a/java/src/main/java/ai/onnxruntime/package-info.java +++ b/java/src/main/java/ai/onnxruntime/package-info.java @@ -4,10 +4,23 @@ */ /** - * A Java interface to the onnxruntime. + * A Java interface to the ONNX Runtime. *

* Provides access to the same execution backends as the C library. * Non-representable types in Java (such as fp16) are converted * into the nearest Java primitive type when accessed through this API. - */ + *

+ * There are two shared libraries required: onnxruntime and onnxruntime4j_jni. The loader is in {@link ai.onnxruntime.OnnxRuntime} and the logic is in this order: + *

    + *
  1. The user may signal to skip loading of a shared library using a property in the form onnxruntime.native.LIB_NAME.skip with a value of true. This means the user has decided to load the library by some other means. + *
  2. The user may specify an explicit location of the shared library file using a property in the form onnxruntime.native.LIB_NAME.path. This uses {@link java.lang.System#load}. + *
  3. The shared library is autodiscovered:
      + *
    1. If the shared library is present in the classpath resources, load using {@link java.lang.System#load} via a temporary file. + * Ideally, this should be the default use case when adding JAR's/dependencies containing the shared libraries to your classpath. + *
    2. If the shared library is not present in the classpath resources, then load using {@link java.lang.System#loadLibrary}, which usually looks elsewhere on the filesystem for the library. + * The semantics and behavior of that method are system/JVM dependent. + * Typically, the java.library.path property is used to specify the location of native libraries. + *
+ * For troubleshooting, all shared library loading events are reported to Java logging at the level FINE. +*/ package ai.onnxruntime; \ No newline at end of file diff --git a/java/src/test/java/ai/onnxruntime/InferenceTest.java b/java/src/test/java/ai/onnxruntime/InferenceTest.java index e9418cea959e0..7061e68801c05 100644 --- a/java/src/test/java/ai/onnxruntime/InferenceTest.java +++ b/java/src/test/java/ai/onnxruntime/InferenceTest.java @@ -53,22 +53,14 @@ */ public class InferenceTest { private static final Pattern LOAD_PATTERN = Pattern.compile("[,\\[\\] ]"); - private static Path resourcePath; - private static Path otherTestPath; private static String propertiesFile = "Properties.txt"; private static Pattern inputPBPattern = Pattern.compile("input_*.pb"); private static Pattern outputPBPattern = Pattern.compile("output_*.pb"); - static { - if (System.getProperty("GRADLE_TEST") != null) { - resourcePath = Paths.get("..","csharp","testdata"); - otherTestPath = Paths.get("..","onnxruntime","test", "testdata"); - } else { - resourcePath = Paths.get("csharp","testdata"); - otherTestPath = Paths.get("onnxruntime","test", "testdata"); - } + private static Path getResourcePath(String path) { + return new File(InferenceTest.class.getResource(path).getFile()).toPath(); } @Test @@ -84,7 +76,7 @@ public void repeatedCloseTest() throws OrtException { @Test public void createSessionFromPath() throws OrtException { - String modelPath = resourcePath.resolve("squeezenet.onnx").toString(); + String modelPath = getResourcePath("/squeezenet.onnx").toString(); try (OrtEnvironment env = OrtEnvironment.getEnvironment("createSessionFromPath"); OrtSession.SessionOptions options = new SessionOptions()) { try (OrtSession session = env.createSession(modelPath,options)) { @@ -123,7 +115,7 @@ public void createSessionFromPath() throws OrtException { } @Test public void createSessionFromByteArray() throws IOException, OrtException { - Path modelPath = resourcePath.resolve("squeezenet.onnx"); + Path modelPath = getResourcePath("/squeezenet.onnx"); byte[] modelBytes = Files.readAllBytes(modelPath); try (OrtEnvironment env = OrtEnvironment.getEnvironment("createSessionFromByteArray"); OrtSession.SessionOptions options = new SessionOptions()) { @@ -171,7 +163,7 @@ public void inferenceTest() throws OrtException { } private void canRunInferenceOnAModel(OptLevel graphOptimizationLevel, ExecutionMode exectionMode) throws OrtException { - String modelPath = resourcePath.resolve("squeezenet.onnx").toString(); + String modelPath = getResourcePath("/squeezenet.onnx").toString(); // Set the graph optimization level for this session. try (OrtEnvironment env = OrtEnvironment.getEnvironment("canRunInferenceOnAModel"); @@ -184,7 +176,7 @@ private void canRunInferenceOnAModel(OptLevel graphOptimizationLevel, ExecutionM Map container = new HashMap<>(); NodeInfo inputMeta = inputMetaMap.values().iterator().next(); - float[] inputData = loadTensorFromFile(resourcePath.resolve("bench.in")); + float[] inputData = loadTensorFromFile(getResourcePath("/bench.in")); // this is the data for only one input tensor for this model Object tensorData = OrtUtil.reshape(inputData,((TensorInfo) inputMeta.getInfo()).getShape()); OnnxTensor inputTensor = OnnxTensor.createTensor(env,tensorData); @@ -194,7 +186,7 @@ private void canRunInferenceOnAModel(OptLevel graphOptimizationLevel, ExecutionM try (OrtSession.Result results = session.run(container)) { assertEquals(1, results.size()); - float[] expectedOutput = loadTensorFromFile(resourcePath.resolve("bench.expected_out")); + float[] expectedOutput = loadTensorFromFile(getResourcePath("/bench.expected_out")); // validate the results // Only iterates once for (Map.Entry r : results) { @@ -485,7 +477,7 @@ public void testPreTrainedModel(String opset, String modelName) throws IOExcepti @Test public void testModelInputFLOAT() throws OrtException { // model takes 1x5 input of fixed type, echoes back - String modelPath = resourcePath.resolve("test_types_FLOAT.pb").toString(); + String modelPath = getResourcePath("/test_types_FLOAT.pb").toString(); try (OrtEnvironment env = OrtEnvironment.getEnvironment("testModelInputFLOAT"); SessionOptions options = new SessionOptions(); @@ -523,7 +515,7 @@ public void testModelInputFLOAT() throws OrtException { @Test public void testModelInputBOOL() throws OrtException { // model takes 1x5 input of fixed type, echoes back - String modelPath = resourcePath.resolve("test_types_BOOL.pb").toString(); + String modelPath = getResourcePath("/test_types_BOOL.pb").toString(); try (OrtEnvironment env = OrtEnvironment.getEnvironment("testModelInputBOOL"); SessionOptions options = new SessionOptions(); @@ -545,7 +537,7 @@ public void testModelInputBOOL() throws OrtException { @Test public void testModelInputINT32() throws OrtException { // model takes 1x5 input of fixed type, echoes back - String modelPath = resourcePath.resolve("test_types_INT32.pb").toString(); + String modelPath = getResourcePath("/test_types_INT32.pb").toString(); try (OrtEnvironment env = OrtEnvironment.getEnvironment("testModelInputINT32"); SessionOptions options = new SessionOptions(); @@ -567,7 +559,7 @@ public void testModelInputINT32() throws OrtException { @Test public void testModelInputDOUBLE() throws OrtException { // model takes 1x5 input of fixed type, echoes back - String modelPath = resourcePath.resolve("test_types_DOUBLE.pb").toString(); + String modelPath = getResourcePath("/test_types_DOUBLE.pb").toString(); try (OrtEnvironment env = OrtEnvironment.getEnvironment("testModelInputDOUBLE"); SessionOptions options = new SessionOptions(); @@ -589,7 +581,7 @@ public void testModelInputDOUBLE() throws OrtException { @Test public void testModelInputINT8() throws OrtException { // model takes 1x5 input of fixed type, echoes back - String modelPath = resourcePath.resolve("test_types_INT8.pb").toString(); + String modelPath = getResourcePath("/test_types_INT8.pb").toString(); try (OrtEnvironment env = OrtEnvironment.getEnvironment("testModelInputINT8"); SessionOptions options = new SessionOptions(); @@ -611,7 +603,7 @@ public void testModelInputINT8() throws OrtException { @Test public void testModelInputINT16() throws OrtException { // model takes 1x5 input of fixed type, echoes back - String modelPath = resourcePath.resolve("test_types_INT16.pb").toString(); + String modelPath = getResourcePath("/test_types_INT16.pb").toString(); try (OrtEnvironment env = OrtEnvironment.getEnvironment("testModelInputINT16"); SessionOptions options = new SessionOptions(); @@ -633,7 +625,7 @@ public void testModelInputINT16() throws OrtException { @Test public void testModelInputINT64() throws OrtException { // model takes 1x5 input of fixed type, echoes back - String modelPath = resourcePath.resolve("test_types_INT64.pb").toString(); + String modelPath = getResourcePath("/test_types_INT64.pb").toString(); try (OrtEnvironment env = OrtEnvironment.getEnvironment("testModelInputINT64"); SessionOptions options = new SessionOptions(); @@ -660,7 +652,7 @@ public void testModelSequenceOfMapIntFloat() throws OrtException { // "probabilities" is a sequence> // https://github.com/onnx/sklearn-onnx/blob/master/docs/examples/plot_pipeline_lightgbm.py - String modelPath = resourcePath.resolve("test_sequence_map_int_float.pb").toString(); + String modelPath = getResourcePath("/test_sequence_map_int_float.pb").toString(); try (OrtEnvironment env = OrtEnvironment.getEnvironment("testModelSequenceOfMapIntFloat"); SessionOptions options = new SessionOptions(); OrtSession session = env.createSession(modelPath, options)) { @@ -720,7 +712,7 @@ public void testModelSequenceOfMapStringFloat() throws OrtException { // "label" is a tensor, // "probabilities" is a sequence> // https://github.com/onnx/sklearn-onnx/blob/master/docs/examples/plot_pipeline_lightgbm.py - String modelPath = resourcePath.resolve("test_sequence_map_string_float.pb").toString(); + String modelPath = getResourcePath("/test_sequence_map_string_float.pb").toString(); try (OrtEnvironment env = OrtEnvironment.getEnvironment("testModelSequenceOfMapStringFloat"); SessionOptions options = new SessionOptions(); OrtSession session = env.createSession(modelPath, options)) { @@ -774,11 +766,13 @@ public void testModelSequenceOfMapStringFloat() throws OrtException { } @Test - public void testModelSerialization() throws OrtException { + public void testModelSerialization() throws OrtException, IOException { String cwd = System.getProperty("user.dir"); - Path squeezeNet = resourcePath.resolve("squeezenet.onnx"); + Path squeezeNet = getResourcePath("/squeezenet.onnx"); String modelPath = squeezeNet.toString(); - String modelOutputPath = Paths.get(cwd, "optimized-squeezenet.onnx").toString(); + File tmpFile = File.createTempFile("optimized-squeezenet", ".onnx"); + String modelOutputPath = tmpFile.getAbsolutePath(); + Assertions.assertEquals(0, tmpFile.length()); try (OrtEnvironment env = OrtEnvironment.getEnvironment()) { // Set the optimized model file path to assert that no exception are thrown. SessionOptions options = new SessionOptions(); @@ -786,14 +780,17 @@ public void testModelSerialization() throws OrtException { options.setOptimizationLevel(OptLevel.BASIC_OPT); try (OrtSession session = env.createSession(modelPath, options)) { Assertions.assertNotNull(session); - Assertions.assertTrue((new File(modelOutputPath)).exists()); + } finally { + Assertions.assertTrue(tmpFile.length() > 0); } + } finally { + tmpFile.delete(); } } @Test public void testStringIdentity() throws OrtException { - String modelPath = otherTestPath.resolve("identity_string.onnx").toString(); + String modelPath = getResourcePath("/identity_string.onnx").toString(); try (OrtEnvironment env = OrtEnvironment.getEnvironment("testStringIdentity"); SessionOptions options = new SessionOptions(); OrtSession session = env.createSession(modelPath, options)) { @@ -871,7 +868,7 @@ private static SqueezeNetTuple openSessionSqueezeNet() throws OrtException { } private static SqueezeNetTuple openSessionSqueezeNet(int cudaDeviceId) throws OrtException { - Path squeezeNet = resourcePath.resolve("squeezenet.onnx"); + Path squeezeNet = getResourcePath("/squeezenet.onnx"); String modelPath = squeezeNet.toString(); OrtEnvironment env = OrtEnvironment.getEnvironment(); SessionOptions options = new SessionOptions(); @@ -879,8 +876,8 @@ private static SqueezeNetTuple openSessionSqueezeNet(int cudaDeviceId) throws Or options.addCUDA(cudaDeviceId); } OrtSession session = env.createSession(modelPath,options); - float[] inputData = loadTensorFromFile(resourcePath.resolve("bench.in")); - float[] expectedOutput = loadTensorFromFile(resourcePath.resolve("bench.expected_out")); + float[] inputData = loadTensorFromFile(getResourcePath("/bench.in")); + float[] expectedOutput = loadTensorFromFile(getResourcePath("/bench.expected_out")); return new SqueezeNetTuple(env, session, inputData, expectedOutput); } diff --git a/java/sample/ScoreMNIST.java b/java/src/test/java/sample/ScoreMNIST.java similarity index 99% rename from java/sample/ScoreMNIST.java rename to java/src/test/java/sample/ScoreMNIST.java index 5158037133985..854c00fe13942 100644 --- a/java/sample/ScoreMNIST.java +++ b/java/src/test/java/sample/ScoreMNIST.java @@ -2,6 +2,7 @@ * Copyright (c) 2019, Oracle and/or its affiliates. All rights reserved. * Licensed under the MIT License. */ +package sample; import ai.onnxruntime.NodeInfo; import ai.onnxruntime.OnnxTensor; import ai.onnxruntime.OnnxValue; diff --git a/tools/ci_build/github/linux/docker/scripts/install_deps.sh b/tools/ci_build/github/linux/docker/scripts/install_deps.sh index bf39742be9785..be091e728fcc2 100755 --- a/tools/ci_build/github/linux/docker/scripts/install_deps.sh +++ b/tools/ci_build/github/linux/docker/scripts/install_deps.sh @@ -92,6 +92,10 @@ else popd fi +GetFile https://downloads.gradle-dn.com/distributions/gradle-6.2-bin.zip /tmp/src/gradle-6.2-bin.zip +cd /tmp/src +unzip gradle-6.2-bin.zip +mv /tmp/src/gradle-6.2 /usr/local/gradle if ! [ -x "$(command -v protoc)" ]; then diff --git a/tools/ci_build/github/linux/run_build.sh b/tools/ci_build/github/linux/run_build.sh index 36e025a7a5a48..bb3938d5997d0 100755 --- a/tools/ci_build/github/linux/run_build.sh +++ b/tools/ci_build/github/linux/run_build.sh @@ -17,6 +17,8 @@ y) YOCTO_VERSION=${OPTARG};; esac done +export PATH=$PATH:/usr/local/gradle/bin + if [ $BUILD_OS = "android" ]; then pushd /onnxruntime_src mkdir build-android && cd build-android @@ -80,6 +82,7 @@ else --cuda_home /usr/local/cuda \ --cudnn_home /usr/local/cuda $BUILD_EXTR_PAR else #cpu, ngraph and openvino + export JAVA_HOME=/usr/lib/jvm/java-8-openjdk-amd64 python3 $SCRIPT_DIR/../../build.py --build_dir /build \ --config Debug Release $COMMON_BUILD_ARGS $BUILD_EXTR_PAR fi diff --git a/tools/ci_build/github/windows/setup_env.bat b/tools/ci_build/github/windows/setup_env.bat index aef3e46c66403..51ccb5fbfef86 100644 --- a/tools/ci_build/github/windows/setup_env.bat +++ b/tools/ci_build/github/windows/setup_env.bat @@ -1 +1,2 @@ set PATH=C:\azcopy;%BUILD_BINARIESDIRECTORY%\packages\python;%BUILD_BINARIESDIRECTORY%\packages\python\DLLs;%BUILD_BINARIESDIRECTORY%\packages\python\Library\bin;%BUILD_BINARIESDIRECTORY%\packages\python\script;%PATH% +set GRADLE_OPTS=-Dorg.gradle.daemon=false diff --git a/tools/ci_build/github/windows/setup_env_x86.bat b/tools/ci_build/github/windows/setup_env_x86.bat index 815bf457e386e..44f69b9a9d2f0 100644 --- a/tools/ci_build/github/windows/setup_env_x86.bat +++ b/tools/ci_build/github/windows/setup_env_x86.bat @@ -1 +1,2 @@ set PATH=C:\azcopy;C:\Program Files (x86)\dotnet;%BUILD_BINARIESDIRECTORY%\packages\python;%BUILD_BINARIESDIRECTORY%\packages\python\DLLs;%BUILD_BINARIESDIRECTORY%\packages\python\Library\bin;%BUILD_BINARIESDIRECTORY%\packages\python\script;%PATH%;C:\local\systools +set GRADLE_OPTS=-Dorg.gradle.daemon=false