From a0c85fc95f5406e62ef37df09613855c9b42a47f Mon Sep 17 00:00:00 2001 From: Matt Date: Mon, 23 Sep 2024 22:44:09 -0400 Subject: [PATCH] Create photon-targeting-JNI framework (#1428) Initial framework for adding JNI libraries. Auto generated JNI headers and sticks native libraries into the JAR (and adds to class path for testing) --- .github/workflows/build.yml | 6 +- README.md | 2 + build.gradle | 3 + .../common/hardware/PlatformUtils.java | 54 +++++++++ .../common/networking/NetworkManager.java | 3 +- photon-lib/build.gradle | 25 +++++ photon-server/build.gradle | 4 +- photon-targeting/build.gradle | 104 ++++++++++++++++-- .../common/hardware/Platform.java | 31 ------ 9 files changed, 183 insertions(+), 49 deletions(-) create mode 100644 photon-core/src/main/java/org/photonvision/common/hardware/PlatformUtils.java rename {photon-core => photon-targeting}/src/main/java/org/photonvision/common/hardware/Platform.java (90%) diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 6c7e710ebb..7d342d07cb 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -160,14 +160,14 @@ jobs: - run: git fetch --tags --force - run: | chmod +x gradlew - ./gradlew photon-targeting:build photon-lib:build -Pbuildalldesktop -i - - run: ./gradlew photon-lib:publish photon-targeting:publish -Pbuildalldesktop + ./gradlew photon-targeting:build photon-lib:build -i + - run: ./gradlew photon-lib:publish photon-targeting:publish name: Publish env: ARTIFACTORY_API_KEY: ${{ secrets.ARTIFACTORY_API_KEY }} if: github.event_name == 'push' && github.repository_owner == 'photonvision' # Copy artifacts to build/outputs/maven - - run: ./gradlew photon-lib:publish photon-targeting:publish -PcopyOfflineArtifacts -Pbuildalldesktop + - run: ./gradlew photon-lib:publish photon-targeting:publish -PcopyOfflineArtifacts - uses: actions/upload-artifact@v4 with: name: maven-${{ matrix.artifact-name }} diff --git a/README.md b/README.md index c559bb963c..475d386054 100644 --- a/README.md +++ b/README.md @@ -40,6 +40,8 @@ Note that these are case sensitive! - `-PtgtIP`: Specifies where `./gradlew deploy` should try to copy the fat JAR to - `-Pprofile`: enables JVM profiling +If you're cross-compiling, you'll need the wpilib toolchain installed. This can be done via Gradle: for example `./gradlew installArm64Toolchain` or `./gradlew installRoboRioToolchain` + ## Out-of-Source Dependencies PhotonVision uses the following additonal out-of-source repositories for building code. diff --git a/build.gradle b/build.gradle index 4ba2dad4cc..18aac2cc63 100644 --- a/build.gradle +++ b/build.gradle @@ -1,12 +1,15 @@ import edu.wpi.first.toolchain.* plugins { + id "java" + id "cpp" id "com.diffplug.spotless" version "6.24.0" id "edu.wpi.first.NativeUtils" version "2024.6.1" apply false id "edu.wpi.first.wpilib.repositories.WPILibRepositoriesPlugin" version "2020.2" id "edu.wpi.first.GradleRIO" version "2024.3.2" id 'edu.wpi.first.WpilibTools' version '1.3.0' id 'com.google.protobuf' version '0.9.4' apply false + id 'edu.wpi.first.GradleJni' version '1.1.0' } allprojects { diff --git a/photon-core/src/main/java/org/photonvision/common/hardware/PlatformUtils.java b/photon-core/src/main/java/org/photonvision/common/hardware/PlatformUtils.java new file mode 100644 index 0000000000..c5bee8d20a --- /dev/null +++ b/photon-core/src/main/java/org/photonvision/common/hardware/PlatformUtils.java @@ -0,0 +1,54 @@ +/* + * Copyright (C) Photon Vision. + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +package org.photonvision.common.hardware; + +import java.io.IOException; +import org.photonvision.common.util.ShellExec; + +@SuppressWarnings("unused") +public class PlatformUtils { + private static final ShellExec shell = new ShellExec(true, false); + private static final boolean isRoot = checkForRoot(); + + @SuppressWarnings("StatementWithEmptyBody") + private static boolean checkForRoot() { + if (Platform.isLinux()) { + try { + shell.executeBashCommand("id -u"); + } catch (IOException e) { + e.printStackTrace(); + } + + while (!shell.isOutputCompleted()) { + // TODO: add timeout + } + + if (shell.getExitCode() == 0) { + return shell.getOutput().split("\n")[0].equals("0"); + } + + } else { + return true; + } + return false; + } + + public static boolean isRoot() { + return isRoot; + } +} diff --git a/photon-core/src/main/java/org/photonvision/common/networking/NetworkManager.java b/photon-core/src/main/java/org/photonvision/common/networking/NetworkManager.java index 0ce9b169c5..47b6540708 100644 --- a/photon-core/src/main/java/org/photonvision/common/networking/NetworkManager.java +++ b/photon-core/src/main/java/org/photonvision/common/networking/NetworkManager.java @@ -24,6 +24,7 @@ import org.photonvision.common.dataflow.DataChangeSource; import org.photonvision.common.dataflow.events.DataChangeEvent; import org.photonvision.common.hardware.Platform; +import org.photonvision.common.hardware.PlatformUtils; import org.photonvision.common.logging.LogGroup; import org.photonvision.common.logging.Logger; import org.photonvision.common.util.ShellExec; @@ -54,7 +55,7 @@ public void initialize(boolean shouldManage) { var config = ConfigManager.getInstance().getConfig().getNetworkConfig(); logger.info("Setting " + config.connectionType + " with team " + config.ntServerAddress); if (Platform.isLinux()) { - if (!Platform.isRoot()) { + if (!PlatformUtils.isRoot()) { logger.error("Cannot manage hostname without root!"); } diff --git a/photon-lib/build.gradle b/photon-lib/build.gradle index 455bde947d..6b9d1f04e3 100644 --- a/photon-lib/build.gradle +++ b/photon-lib/build.gradle @@ -1,3 +1,7 @@ +plugins { + id 'edu.wpi.first.WpilibTools' version '1.3.0' +} + import java.nio.file.Path ext { @@ -314,3 +318,24 @@ publishing { } } } + +// setup wpilib bundled native libs +wpilibTools.deps.wpilibVersion = wpi.versions.wpilibVersion.get() + +def nativeConfigName = 'wpilibNatives' +def nativeConfig = configurations.create(nativeConfigName) + +def nativeTasks = wpilibTools.createExtractionTasks { + configurationName = nativeConfigName +} + +nativeTasks.addToSourceSetResources(sourceSets.test) + +nativeConfig.dependencies.add wpilibTools.deps.wpilib("wpimath") +nativeConfig.dependencies.add wpilibTools.deps.wpilib("wpinet") +nativeConfig.dependencies.add wpilibTools.deps.wpilib("wpiutil") +nativeConfig.dependencies.add wpilibTools.deps.wpilib("ntcore") +nativeConfig.dependencies.add wpilibTools.deps.wpilib("cscore") +nativeConfig.dependencies.add wpilibTools.deps.wpilib("apriltag") +nativeConfig.dependencies.add wpilibTools.deps.wpilib("hal") +nativeConfig.dependencies.add wpilibTools.deps.wpilibOpenCv("frc" + wpi.frcYear.get(), wpi.versions.opencvVersion.get()) diff --git a/photon-server/build.gradle b/photon-server/build.gradle index 7c8c2ba078..2515d65b9e 100644 --- a/photon-server/build.gradle +++ b/photon-server/build.gradle @@ -47,9 +47,7 @@ tasks.register("buildAndCopyUI") { run { environment "PATH_PREFIX", "../" -} -run { if (project.hasProperty("profile")) { jvmArgs=[ "-Dcom.sun.management.jmxremote=true", @@ -105,7 +103,7 @@ task findDeployTarget { task deploy { dependsOn findDeployTarget - dependsOn assemble + dependsOn 'shadowJar' doLast { println 'Starting deployment to ' + findDeployTarget.rmt.host diff --git a/photon-targeting/build.gradle b/photon-targeting/build.gradle index a00897e23e..da7dde2d00 100644 --- a/photon-targeting/build.gradle +++ b/photon-targeting/build.gradle @@ -1,3 +1,7 @@ +plugins { + id 'edu.wpi.first.WpilibTools' version '1.3.0' +} + ext { nativeName = "photontargeting" } @@ -5,6 +9,7 @@ ext { apply plugin: 'cpp' apply plugin: 'google-test-test-suite' apply plugin: 'edu.wpi.first.NativeUtils' +apply plugin: 'edu.wpi.first.GradleJni' apply from: "${rootDir}/shared/config.gradle" apply from: "${rootDir}/shared/javacommon.gradle" @@ -19,6 +24,20 @@ nativeUtils { sourceSets.main.java.srcDir "${projectDir}/src/generated/main/java" +// Folder whose contents will be included in the final jar +def outputsFolder = file("$buildDir/extra_resources") + +// Sync task: like the copy task, but all files that exist in the destination directory will be deleted before copying files +task syncOutputsFolder(type: Sync) { + into outputsFolder +} + +// And package our outputs folder into the final jar +jar { + from outputsFolder + dependsOn syncOutputsFolder +} + model { components { "${nativeName}"(NativeLibrarySpec) { @@ -42,14 +61,59 @@ model { it.tasks.withType(CppCompile) { it.dependsOn generateProto } - if(project.hasProperty('includePhotonTargeting')) { - lib project: ':photon-targeting', library: 'photontargeting', linkage: 'shared' + } + + nativeUtils.useRequiredLibrary(it, "wpiutil_shared") + nativeUtils.useRequiredLibrary(it, "wpimath_shared") + nativeUtils.useRequiredLibrary(it, "wpinet_shared") + nativeUtils.useRequiredLibrary(it, "wpilibc_shared") + nativeUtils.useRequiredLibrary(it, "ntcore_shared") + } + "${nativeName}JNI"(JniNativeLibrarySpec) { + + enableCheckTask project.hasProperty('doJniCheck') + javaCompileTasks << compileJava + jniCrossCompileOptions << JniCrossCompileOptions(nativeUtils.wpi.platforms.roborio) + jniCrossCompileOptions << JniCrossCompileOptions(nativeUtils.wpi.platforms.linuxarm32) + jniCrossCompileOptions << JniCrossCompileOptions(nativeUtils.wpi.platforms.linuxarm64) + + sources { + cpp { + source { + srcDirs 'src/main/native/jni' + include '**/*.cpp', '**/*.cc' + } } } - nativeUtils.useRequiredLibrary(it, "wpilib_shared") - nativeUtils.useRequiredLibrary(it, "apriltag_shared") - nativeUtils.useRequiredLibrary(it, "opencv_shared") + binaries.all { + lib library: nativeName, linkage: 'shared' + } + + nativeUtils.useRequiredLibrary(it, "wpiutil_shared") + nativeUtils.useRequiredLibrary(it, "wpinet_shared") + nativeUtils.useRequiredLibrary(it, "ntcore_shared") + } + + all { + binaries.withType(SharedLibraryBinarySpec) { binary -> + // check that we're building for the platform (per PArchOverride/wpilib plat detection) + if (binary.targetPlatform.name == jniPlatform) { + + // only include release binaries (hard coded for now) + def isDebug = binary.buildType.name.contains('debug') + if (!isDebug) { + syncOutputsFolder { + // Just shove the shared library into the root of the jar output by photon-targeting:jar + from(binary.sharedLibraryFile) { + into "nativelibraries/${wpilibNativeName}/" + } + // And (not sure if this is a hack) make the jar task depend on the build task + dependsOn binary.identifier.projectScopedName + } + } + } + } } } testSuites { @@ -76,17 +140,11 @@ model { it.tasks.withType(CppCompile) { it.dependsOn generateProto } - if(project.hasProperty('includePhotonTargeting')) { - lib project: ':photon-targeting', library: 'photontargeting', linkage: 'shared' - } } - nativeUtils.useRequiredLibrary(it, "cscore_shared") - nativeUtils.useRequiredLibrary(it, "cameraserver_shared") nativeUtils.useRequiredLibrary(it, "wpilib_executable_shared") nativeUtils.useRequiredLibrary(it, "googletest_static") nativeUtils.useRequiredLibrary(it, "apriltag_shared") - nativeUtils.useRequiredLibrary(it, "opencv_shared") } } @@ -129,3 +187,27 @@ cppHeadersZip { into '/' } } + +// make sure native libraries can be loaded in tests +test { + classpath += files(outputsFolder) + dependsOn syncOutputsFolder +} + +// setup wpilib bundled native libs +wpilibTools.deps.wpilibVersion = wpi.versions.wpilibVersion.get() + +def nativeConfigName = 'wpilibNatives' +def nativeConfig = configurations.create(nativeConfigName) + +def nativeTasks = wpilibTools.createExtractionTasks { + configurationName = nativeConfigName +} + +nativeTasks.addToSourceSetResources(sourceSets.test) + +nativeConfig.dependencies.add wpilibTools.deps.wpilib("wpiutil") +nativeConfig.dependencies.add wpilibTools.deps.wpilib("wpimath") +nativeConfig.dependencies.add wpilibTools.deps.wpilib("wpinet") +nativeConfig.dependencies.add wpilibTools.deps.wpilib("ntcore") +nativeConfig.dependencies.add wpilibTools.deps.wpilib("hal") diff --git a/photon-core/src/main/java/org/photonvision/common/hardware/Platform.java b/photon-targeting/src/main/java/org/photonvision/common/hardware/Platform.java similarity index 90% rename from photon-core/src/main/java/org/photonvision/common/hardware/Platform.java rename to photon-targeting/src/main/java/org/photonvision/common/hardware/Platform.java index 57f1eab868..e2d0155609 100644 --- a/photon-core/src/main/java/org/photonvision/common/hardware/Platform.java +++ b/photon-targeting/src/main/java/org/photonvision/common/hardware/Platform.java @@ -17,13 +17,11 @@ package org.photonvision.common.hardware; -import com.jogamp.common.os.Platform.OSType; import edu.wpi.first.util.RuntimeDetector; import java.io.BufferedReader; import java.io.IOException; import java.nio.file.Files; import java.nio.file.Paths; -import org.photonvision.common.util.ShellExec; @SuppressWarnings("unused") public enum Platform { @@ -63,7 +61,6 @@ private enum OSType { UNKNOWN } - private static final ShellExec shell = new ShellExec(true, false); public final String description; public final String nativeLibraryFolderName; public final boolean isPi; @@ -72,7 +69,6 @@ private enum OSType { // Set once at init, shouldn't be needed after. private static final Platform currentPlatform = getCurrentPlatform(); - private static final boolean isRoot = checkForRoot(); Platform( String description, @@ -115,10 +111,6 @@ public static String getNativeLibraryFolderName() { return currentPlatform.nativeLibraryFolderName; } - public static boolean isRoot() { - return isRoot; - } - public static boolean isSupported() { return currentPlatform.isSupported; } @@ -131,29 +123,6 @@ public static boolean isSupported() { private static final String UnknownPlatformString = String.format("Unknown Platform. OS: %s, Architecture: %s", OS_NAME, OS_ARCH); - @SuppressWarnings("StatementWithEmptyBody") - private static boolean checkForRoot() { - if (isLinux()) { - try { - shell.executeBashCommand("id -u"); - } catch (IOException e) { - e.printStackTrace(); - } - - while (!shell.isOutputCompleted()) { - // TODO: add timeout - } - - if (shell.getExitCode() == 0) { - return shell.getOutput().split("\n")[0].equals("0"); - } - - } else { - return true; - } - return false; - } - private static Platform getCurrentPlatform() { if (RuntimeDetector.isWindows()) { if (RuntimeDetector.is32BitIntel()) {