diff --git a/shared/jni/setupBuild.gradle b/shared/jni/setupBuild.gradle index 6c6148cfe4..15805d2ba2 100644 --- a/shared/jni/setupBuild.gradle +++ b/shared/jni/setupBuild.gradle @@ -99,7 +99,7 @@ model { baseName = nativeName + 'jni' } - enableCheckTask true + enableCheckTask !project.hasProperty('skipJniCheck') javaCompileTasks << compileJava jniCrossCompileOptions << JniCrossCompileOptions(nativeUtils.wpi.platforms.roborio) jniCrossCompileOptions << JniCrossCompileOptions(nativeUtils.wpi.platforms.raspbian) @@ -141,7 +141,7 @@ model { baseName = nativeName + 'jni' } - enableCheckTask true + enableCheckTask !project.hasProperty('skipJniCheck') javaCompileTasks << compileJava jniCrossCompileOptions << JniCrossCompileOptions(nativeUtils.wpi.platforms.roborio) jniCrossCompileOptions << JniCrossCompileOptions(nativeUtils.wpi.platforms.raspbian) diff --git a/wpiutil/build.gradle b/wpiutil/build.gradle index a79bae4078..fbd1d732ce 100644 --- a/wpiutil/build.gradle +++ b/wpiutil/build.gradle @@ -2,6 +2,7 @@ apply from: "${rootDir}/shared/resources.gradle" ext { noWpiutil = true + skipJniCheck = true baseId = 'wpiutil' groupId = 'edu.wpi.first.wpiutil' diff --git a/wpiutil/src/main/java/edu/wpi/first/wpiutil/CombinedRuntimeLoader.java b/wpiutil/src/main/java/edu/wpi/first/wpiutil/CombinedRuntimeLoader.java new file mode 100644 index 0000000000..b54ccf6c7b --- /dev/null +++ b/wpiutil/src/main/java/edu/wpi/first/wpiutil/CombinedRuntimeLoader.java @@ -0,0 +1,176 @@ +/*----------------------------------------------------------------------------*/ +/* Copyright (c) 2020 FIRST. All Rights Reserved. */ +/* Open Source Software - may be modified and shared by FRC teams. The code */ +/* must be accompanied by the FIRST BSD license file in the root directory of */ +/* the project. */ +/*----------------------------------------------------------------------------*/ + +package edu.wpi.first.wpiutil; + +import java.io.File; +import java.io.IOException; +import java.nio.file.Files; +import java.nio.file.Paths; +import java.util.ArrayList; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.Objects; + +import com.fasterxml.jackson.core.type.TypeReference; +import com.fasterxml.jackson.databind.ObjectMapper; + +public final class CombinedRuntimeLoader { + private CombinedRuntimeLoader() { + } + + private static String extractionDirectory; + + public static synchronized String getExtractionDirectory() { + return extractionDirectory; + } + + private static synchronized void setExtractionDirectory(String directory) { + extractionDirectory = directory; + } + + public static native boolean addDllSearchDirectory(String directory); + + private static String getLoadErrorMessage(String libraryName, UnsatisfiedLinkError ule) { + StringBuilder msg = new StringBuilder(512); + msg.append(libraryName).append(" could not be loaded from path\n" + + "\tattempted to load for platform ") + .append(RuntimeDetector.getPlatformPath()) + .append("\nLast Load Error: \n") + .append(ule.getMessage()) + .append('\n'); + if (RuntimeDetector.isWindows()) { + msg.append("A common cause of this error is missing the C++ runtime.\n" + + "Download the latest at https://support.microsoft.com/en-us/help/2977003/the-latest-supported-visual-c-downloads\n"); + } + return msg.toString(); + } + + /** + * Extract a list of native libraries. + * @param The class where the resources would be located + * @param clazz The actual class object + * @param resourceName The resource name on the classpath to use for file lookup + * @return List of all libraries that were extracted + * @throws IOException Thrown if resource not found or file could not be extracted + */ + @SuppressWarnings({"PMD.AvoidInstantiatingObjectsInLoops", "PMD.UnnecessaryCastRule"}) + public static List extractLibraries(Class clazz, String resourceName) + throws IOException { + TypeReference> typeRef = new TypeReference>() { + }; + ObjectMapper mapper = new ObjectMapper(); + Map map; + try (var stream = clazz.getResourceAsStream(resourceName)) { + map = mapper.readValue(stream, typeRef); + } + + var platformPath = Paths.get(RuntimeDetector.getPlatformPath()); + var platform = platformPath.getName(0).toString(); + var arch = platformPath.getName(1).toString(); + + var platformMap = (Map>) map.get(platform); + + var fileList = platformMap.get(arch); + + var extractionPathString = getExtractionDirectory(); + + if (extractionPathString == null) { + String hash = (String) map.get("hash"); + + var defaultExtractionRoot = RuntimeLoader.getDefaultExtractionRoot(); + var extractionPath = Paths.get(defaultExtractionRoot, platform, arch, hash); + extractionPathString = extractionPath.toString(); + + setExtractionDirectory(extractionPathString); + } + + List extractedFiles = new ArrayList<>(); + + byte[] buffer = new byte[0x10000]; // 64K copy buffer + + for (var file : fileList) { + try (var stream = clazz.getResourceAsStream(file)) { + Objects.requireNonNull(stream); + + var outputFile = Paths.get(extractionPathString, new File(file).getName()); + extractedFiles.add(outputFile.toString()); + if (outputFile.toFile().exists()) { + continue; + } + outputFile.getParent().toFile().mkdirs(); + + try (var os = Files.newOutputStream(outputFile)) { + int readBytes; + while ((readBytes = stream.read(buffer)) != -1) { // NOPMD + os.write(buffer, 0, readBytes); + } + } + } + } + + return extractedFiles; + } + + /** + * Load a single library from a list of extracted files. + * @param libraryName The library name to load + * @param extractedFiles The extracted files to search + * @throws IOException If library was not found + */ + public static void loadLibrary(String libraryName, List extractedFiles) + throws IOException { + String currentPath = null; + try { + for (var extractedFile : extractedFiles) { + if (extractedFile.contains(libraryName)) { + // Load it + currentPath = extractedFile; + System.load(extractedFile); + return; + } + } + throw new IOException("Could not find library " + libraryName); + } catch (UnsatisfiedLinkError ule) { + throw new IOException(getLoadErrorMessage(currentPath, ule)); + } + } + + /** + * Load a list of native libraries out of a single directory. + * + * @param The class where the resources would be located + * @param clazz The actual class object + * @param librariesToLoad List of libraries to load + * @throws IOException Throws an IOException if not found + */ + public static void loadLibraries(Class clazz, String... librariesToLoad) + throws IOException { + // Extract everything + + var extractedFiles = extractLibraries(clazz, "/ResourceInformation.json"); + + String currentPath = ""; + + try { + if (RuntimeDetector.isWindows()) { + var extractionPathString = getExtractionDirectory(); + // Load windows, set dll directory + currentPath = Paths.get(extractionPathString, "WindowsLoaderHelper.dll").toString(); + System.load(currentPath); + addDllSearchDirectory(extractionPathString); + } + } catch (UnsatisfiedLinkError ule) { + throw new IOException(getLoadErrorMessage(currentPath, ule)); + } + + for (var library : librariesToLoad) { + loadLibrary(library, extractedFiles); + } + } +}