Skip to content

Commit

Permalink
Create photon-targeting-JNI framework (#1428)
Browse files Browse the repository at this point in the history
Initial framework for adding JNI libraries. Auto generated JNI headers and sticks native libraries into the JAR (and adds to class path for testing)
  • Loading branch information
mcm001 committed Sep 24, 2024
1 parent f33218c commit a0c85fc
Show file tree
Hide file tree
Showing 9 changed files with 183 additions and 49 deletions.
6 changes: 3 additions & 3 deletions .github/workflows/build.yml
Original file line number Diff line number Diff line change
Expand Up @@ -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 }}
Expand Down
2 changes: 2 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down
3 changes: 3 additions & 0 deletions build.gradle
Original file line number Diff line number Diff line change
@@ -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 {
Expand Down
Original file line number Diff line number Diff line change
@@ -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;
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -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!");
}

Expand Down
25 changes: 25 additions & 0 deletions photon-lib/build.gradle
Original file line number Diff line number Diff line change
@@ -1,3 +1,7 @@
plugins {
id 'edu.wpi.first.WpilibTools' version '1.3.0'
}

import java.nio.file.Path

ext {
Expand Down Expand Up @@ -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())
4 changes: 1 addition & 3 deletions photon-server/build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -47,9 +47,7 @@ tasks.register("buildAndCopyUI") {

run {
environment "PATH_PREFIX", "../"
}

run {
if (project.hasProperty("profile")) {
jvmArgs=[
"-Dcom.sun.management.jmxremote=true",
Expand Down Expand Up @@ -105,7 +103,7 @@ task findDeployTarget {

task deploy {
dependsOn findDeployTarget
dependsOn assemble
dependsOn 'shadowJar'

doLast {
println 'Starting deployment to ' + findDeployTarget.rmt.host
Expand Down
104 changes: 93 additions & 11 deletions photon-targeting/build.gradle
Original file line number Diff line number Diff line change
@@ -1,10 +1,15 @@
plugins {
id 'edu.wpi.first.WpilibTools' version '1.3.0'
}

ext {
nativeName = "photontargeting"
}

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"
Expand All @@ -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) {
Expand All @@ -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 {
Expand All @@ -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")
}
}

Expand Down Expand Up @@ -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")
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand Down Expand Up @@ -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;
Expand All @@ -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,
Expand Down Expand Up @@ -115,10 +111,6 @@ public static String getNativeLibraryFolderName() {
return currentPlatform.nativeLibraryFolderName;
}

public static boolean isRoot() {
return isRoot;
}

public static boolean isSupported() {
return currentPlatform.isSupported;
}
Expand All @@ -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()) {
Expand Down

0 comments on commit a0c85fc

Please sign in to comment.