Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@
**/.vscode/**
**/out/
/.gradle/
**/.gradle/
/.nb-gradle/
/.idea/
/dist/
Expand Down
25 changes: 25 additions & 0 deletions build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,12 @@ buildscript {
}
}

plugins {
id 'org.jmonkeyengine.nativeimage' apply false
}

import org.gradle.api.file.RelativePath

// Set the license for IDEs that understand this
ext.license = file("$rootDir/source-file-header-template.txt")

Expand Down Expand Up @@ -54,6 +60,25 @@ subprojects {
}
}

def nativeImagePluginBuild = gradle.includedBuild('jme3-nativeimage-plugin')
def nativeImagePluginPublishTasks = [
publish : ':publish',
publishToMavenLocal : ':publishToMavenLocal',
install : ':install',
publishAllPublicationsToCentralRepository : ':publishAllPublicationsToCentralRepository',
publishAllPublicationsToSNAPSHOTRepository : ':publishAllPublicationsToSNAPSHOTRepository',
publishAllPublicationsToDistRepository : ':publishAllPublicationsToDistRepository'
]

subprojects {
tasks.configureEach { task ->
String includedTaskPath = nativeImagePluginPublishTasks[task.name]
if (includedTaskPath != null) {
task.dependsOn nativeImagePluginBuild.task(includedTaskPath)
}
}
}

tasks.register('run') {
dependsOn ':jme3-examples:run'
description = 'Run the jME3 examples'
Expand Down
2 changes: 2 additions & 0 deletions common.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,8 @@ java {
}
}

apply plugin: 'org.jmonkeyengine.nativeimage'

tasks.withType(JavaCompile) { // compile-time options:
//options.compilerArgs << '-Xlint:deprecation' // to show deprecation warnings
options.compilerArgs << '-Xlint:unchecked'
Expand Down
14 changes: 14 additions & 0 deletions jme3-core/src/main/java/com/jme3/system/Platform.java
Original file line number Diff line number Diff line change
Expand Up @@ -221,6 +221,7 @@ public enum Os {

private final boolean is64bit;
private final Os os;
private static final boolean NATIVE_IMAGE_RUNTIME = detectNativeImageRuntime();

/**
* Test for a 64-bit address space.
Expand All @@ -240,6 +241,19 @@ public Os getOs() {
return os;
}

/**
* Test whether this process is running as a GraalVM native-image executable.
*
* @return true if running inside a native-image runtime, otherwise false
*/
public boolean isGraalVMNativeImage() {
return NATIVE_IMAGE_RUNTIME;
}

private static boolean detectNativeImageRuntime() {
return System.getProperty("org.graalvm.nativeimage.imagecode") != null;
}

private Platform(Os os, boolean is64bit) {
this.os = os;
this.is64bit = is64bit;
Expand Down
270 changes: 178 additions & 92 deletions jme3-desktop/src/main/java/com/jme3/system/NativeLibraryLoader.java
Original file line number Diff line number Diff line change
Expand Up @@ -70,13 +70,25 @@
*
* @author Kirill Vainer
*/
public final class NativeLibraryLoader {

private static final Logger logger = Logger.getLogger(NativeLibraryLoader.class.getName());
private static File extractionFolderOverride = null;
private static File extractionFolder = null;

private static final HashMap<NativeLibrary.Key, NativeLibrary> nativeLibraryMap = new HashMap<>();
public final class NativeLibraryLoader {
/**
* System property containing the filesystem directory where native libraries
* should be extracted to or loaded from.
*/
public static final String CUSTOM_EXTRACTION_FOLDER_PROPERTY = "com.jme3.NativeLibraryExtractionFolder";

/**
* System property controlling whether native libraries should be extracted
* from the classpath before loading. Defaults to {@code true}.
*/
public static final String EXTRACT_NATIVE_LIBRARIES_PROPERTY = "com.jme3.ExtractNativeLibraries";

private static final Logger logger = Logger.getLogger(NativeLibraryLoader.class.getName());
private static File extractionFolderOverride = null;
private static File extractionFolder = null;
private static Boolean extractNativeLibrariesOverride = null;

private static final HashMap<NativeLibrary.Key, NativeLibrary> nativeLibraryMap = new HashMap<>();

static {
NativeLibraries.registerDefaultLibraries();
Expand Down Expand Up @@ -174,18 +186,75 @@ public static boolean isUsingNativeBullet() {
*
* @param path Path where to extract native libraries.
*/
public static void setCustomExtractionFolder(String path) {
extractionFolderOverride = new File(path).getAbsoluteFile();
}
public static void setCustomExtractionFolder(String path) {
extractionFolderOverride = path == null ? null : new File(path).getAbsoluteFile();
}

/**
* Returns the configured custom extraction folder.
*
* @return the programmatic override if set, otherwise the
* {@link #CUSTOM_EXTRACTION_FOLDER_PROPERTY} system property
*/
public static File getCustomExtractionFolder() {
if (extractionFolderOverride != null) {
return extractionFolderOverride;
}

String extractionFolderProperty = System.getProperty(CUSTOM_EXTRACTION_FOLDER_PROPERTY);
if (extractionFolderProperty != null && !extractionFolderProperty.trim().isEmpty()) {
return new File(extractionFolderProperty).getAbsoluteFile();
}

return null;
}

/**
* Specify whether native libraries should be extracted from the classpath
* before loading. Set to {@code true} to preserve the default behavior.
*
* @param extractNativeLibraries true to extract classpath natives, false to
* load existing files from the extraction folder
*/
public static void setExtractNativeLibraries(boolean extractNativeLibraries) {
extractNativeLibrariesOverride = extractNativeLibraries;
}

/**
* Clears the programmatic extraction flag override.
*/
public static void clearExtractNativeLibrariesOverride() {
extractNativeLibrariesOverride = null;
}

/**
* Returns whether native libraries should be extracted before loading.
*
* @return the programmatic override if set, otherwise the
* {@link #EXTRACT_NATIVE_LIBRARIES_PROPERTY} system property,
* defaulting to true
*/
public static boolean isExtractNativeLibraries() {
if (extractNativeLibrariesOverride != null) {
return extractNativeLibrariesOverride;
}

String extractNativeLibrariesProperty = System.getProperty(EXTRACT_NATIVE_LIBRARIES_PROPERTY);
return extractNativeLibrariesProperty == null
|| extractNativeLibrariesProperty.trim().isEmpty()
|| Boolean.parseBoolean(extractNativeLibrariesProperty);
}

/**
* Returns the folder where native libraries will be extracted.
* This is automatically determined at run-time based on the
* following criteria:<br>
* <ul>
* <li>If a {@link #setCustomExtractionFolder(java.lang.String) custom
* extraction folder} has been specified, it is returned.
* <li>If the user can write to "java.io.tmpdir" folder, then it
* following criteria:<br>
* <ul>
* <li>If a {@link #setCustomExtractionFolder(java.lang.String) custom
* extraction folder} has been specified, it is returned.</li>
* <li>If the {@link #CUSTOM_EXTRACTION_FOLDER_PROPERTY} system property
* has been specified, it is returned.</li>
* <li>If the user can write to "java.io.tmpdir" folder, then it
* is used.</li>
* <li>Otherwise, the {@link JmeSystem#getStorageFolder() storage folder}
* is used, to prevent collisions, a special subfolder is used
Expand All @@ -196,10 +265,11 @@ public static void setCustomExtractionFolder(String path) {
*
* @return Path where natives will be extracted to.
*/
public static File getExtractionFolder() {
if (extractionFolderOverride != null) {
return extractionFolderOverride;
}
public static File getExtractionFolder() {
File customExtractionFolder = getCustomExtractionFolder();
if (customExtractionFolder != null) {
return customExtractionFolder;
}
if (extractionFolder == null) {
File userTempDir = new File(System.getProperty("java.io.tmpdir"));
if (!userTempDir.canWrite()) {
Expand Down Expand Up @@ -480,79 +550,95 @@ public static String loadNativeLibrary(String name, boolean isRequired) {
return null;
}

URL url = Resources.getResource(pathInJar);

if (url == null) {
if (isRequired) {
throw new UnsatisfiedLinkError(
"The required native library '" + library.getName() + "'"
+ " was not found in the classpath via '" + pathInJar);
} else if (logger.isLoggable(Level.FINE)) {
logger.log(Level.FINE, "The optional native library ''{0}''" +
" was not found in the classpath via ''{1}''.",
new Object[]{library.getName(), pathInJar});
}
return null;
}

// The library has been found and is ready to be extracted.
// Determine what filename it should be extracted as.
String loadedAsFileName;
if (library.getExtractedAsName() != null) {
loadedAsFileName = library.getExtractedAsName();
} else {
// Just use the original filename as it is in the JAR.
loadedAsFileName = Paths.get(pathInJar).getFileName().toString();
}

File extractionDirectory = getExtractionFolder();
URLConnection conn;

try {
conn = url.openConnection();
} catch (IOException ex) {
// Maybe put more detail here? Not sure.
throw new UncheckedIOException("Failed to open file: '" + url +
"'. Error: " + ex, ex);
}

File targetFile = new File(extractionDirectory, loadedAsFileName);
try (InputStream in = conn.getInputStream()) {
if (isExtractingRequired(conn, targetFile)) {
Files.copy(in, targetFile.toPath(), StandardCopyOption.REPLACE_EXISTING);

// NOTE: On OSes that support "Date Created" property,
// this will cause the last modified date to be lower than
// date created which makes no sense
targetFile.setLastModified(conn.getLastModified());

if (logger.isLoggable(Level.FINE)) {
logger.log(Level.FINE, "Extracted native library from ''{0}'' into ''{1}''. ",
new Object[]{url, targetFile});
}
} else {
if (logger.isLoggable(Level.FINE)) {
logger.log(Level.FINE, "Not copying library {0}. Latest already extracted.",
loadedAsFileName);
}
}

library.getLoadFunction().accept(targetFile.getAbsolutePath());

if (logger.isLoggable(Level.FINE)) {
logger.log(Level.FINE, "Loaded native library {0}.", library.getName());
}

return targetFile.getAbsolutePath();
} catch (IOException ex) {
/*if (ex.getMessage().contains("used by another process")) {
return;
}*/

throw new UncheckedIOException("Failed to extract native library to: "
+ targetFile, ex);
}
}
String loadedAsFileName = getLoadedAsFileName(library, pathInJar);
File extractionDirectory = getExtractionFolder();
File targetFile = new File(extractionDirectory, loadedAsFileName);

if (isExtractNativeLibraries()) {
URL url = Resources.getResource(pathInJar);

if (url == null) {
if (isRequired) {
throw new UnsatisfiedLinkError(
"The required native library '" + library.getName() + "'"
+ " was not found in the classpath via '" + pathInJar);
} else if (logger.isLoggable(Level.FINE)) {
logger.log(Level.FINE, "The optional native library ''{0}''" +
" was not found in the classpath via ''{1}''.",
new Object[]{library.getName(), pathInJar});
}
return null;
}

// The library has been found and is ready to be extracted.
URLConnection conn;

try {
conn = url.openConnection();
} catch (IOException ex) {
// Maybe put more detail here? Not sure.
throw new UncheckedIOException("Failed to open file: '" + url +
"'. Error: " + ex, ex);
}

try (InputStream in = conn.getInputStream()) {
if (isExtractingRequired(conn, targetFile)) {
Files.copy(in, targetFile.toPath(), StandardCopyOption.REPLACE_EXISTING);

// NOTE: On OSes that support "Date Created" property,
// this will cause the last modified date to be lower than
// date created which makes no sense
targetFile.setLastModified(conn.getLastModified());

if (logger.isLoggable(Level.FINE)) {
logger.log(Level.FINE, "Extracted native library from ''{0}'' into ''{1}''. ",
new Object[]{url, targetFile});
}
} else {
if (logger.isLoggable(Level.FINE)) {
logger.log(Level.FINE, "Not copying library {0}. Latest already extracted.",
loadedAsFileName);
}
}
} catch (IOException ex) {
/*if (ex.getMessage().contains("used by another process")) {
return;
}*/

throw new UncheckedIOException("Failed to extract native library to: "
+ targetFile, ex);
}
} else if (!targetFile.isFile()) {
if (isRequired) {
throw new UnsatisfiedLinkError(
"The required native library '" + library.getName() + "'"
+ " was not found at '" + targetFile
+ "' and native library extraction is disabled");
} else if (logger.isLoggable(Level.FINE)) {
logger.log(Level.FINE, "The optional native library ''{0}''" +
" was not found at ''{1}'' and native library extraction is disabled.",
new Object[]{library.getName(), targetFile});
}
return null;
}

library.getLoadFunction().accept(targetFile.getAbsolutePath());

if (logger.isLoggable(Level.FINE)) {
logger.log(Level.FINE, "Loaded native library {0}.", library.getName());
}

return targetFile.getAbsolutePath();
}

private static String getLoadedAsFileName(NativeLibrary library, String pathInJar) {
if (library.getExtractedAsName() != null) {
return library.getExtractedAsName();
}

// Just use the original filename as it is in the JAR.
return Paths.get(pathInJar).getFileName().toString();
}

/**
* Checks if library extraction is required by comparing source and target
Expand Down
Loading
Loading