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)
This commit is contained in:
Matt
2024-09-23 22:44:09 -04:00
committed by GitHub
parent f33218c49c
commit a0c85fc95f
9 changed files with 183 additions and 49 deletions

View File

@@ -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 }}

View File

@@ -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.

View File

@@ -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 {

View File

@@ -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 <https://www.gnu.org/licenses/>.
*/
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;
}
}

View File

@@ -24,6 +24,7 @@ import org.photonvision.common.dataflow.DataChangeService;
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 class NetworkManager {
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!");
}

View File

@@ -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())

View File

@@ -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

View File

@@ -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")

View File

@@ -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 @@ public enum Platform {
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 @@ public enum Platform {
// 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 enum Platform {
return currentPlatform.nativeLibraryFolderName;
}
public static boolean isRoot() {
return isRoot;
}
public static boolean isSupported() {
return currentPlatform.isSupported;
}
@@ -131,29 +123,6 @@ public enum Platform {
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()) {