diff --git a/ant/build.xml b/ant/build.xml
index 0d346b47dd..41185542ba 100644
--- a/ant/build.xml
+++ b/ant/build.xml
@@ -8,6 +8,7 @@
+
@@ -193,6 +194,11 @@
+
+
+
+
+
Tests will run only if the TARGET and HOST match:${line.separator}${line.separator}
TARGET: ${os.target.classifier}
diff --git a/pom.xml b/pom.xml
index e0ea51cd45..f2e5d1f1de 100644
--- a/pom.xml
+++ b/pom.xml
@@ -68,7 +68,7 @@
1.7.0
1.1
3.0.1
- 3.0.0-M3
+ 3.0.0-M4
@@ -253,6 +253,12 @@
org.apache.maven.plugins
maven-surefire-plugin
${plugin.surfire.version}
+
+ false
+
+ ${maven.exclude.tests}
+
+
diff --git a/src/main/java/jssc/DefaultJniExtractorStub.java b/src/main/java/jssc/DefaultJniExtractorStub.java
new file mode 100644
index 0000000000..e822eff2ef
--- /dev/null
+++ b/src/main/java/jssc/DefaultJniExtractorStub.java
@@ -0,0 +1,74 @@
+/**
+ * License: https://opensource.org/licenses/BSD-3-Clause
+ */
+package jssc;
+
+import org.scijava.nativelib.DefaultJniExtractor;
+import org.scijava.nativelib.NativeLibraryUtil;
+
+import java.io.File;
+import java.io.IOException;
+
+/**
+ * @author A. Tres Finocchiaro
+ *
+ * Stub DefaultJniExtractor
class to allow native-lib-loader to conditionally
+ * use a statically defined native search path bootPath
when provided.
+ */
+public class DefaultJniExtractorStub extends DefaultJniExtractor {
+ private File bootPath;
+
+ /**
+ * Default constructor
+ */
+ public DefaultJniExtractorStub(Class libraryJarClass) throws IOException {
+ super(libraryJarClass);
+ }
+
+ /**
+ * Force native-lib-loader to first look in the location defined as bootPath
+ * prior to extracting a native library, useful for sandboxed environments.
+ *
+ * NativeLoader.setJniExtractor(new DefaultJniExtractorStub(null, "/opt/nativelibs")));
+ * NativeLoader.loadLibrary("mylibrary");
+ *
+ */
+ public DefaultJniExtractorStub(Class libraryJarClass, String bootPath) throws IOException {
+ this(libraryJarClass);
+
+ if(bootPath != null) {
+ File bootTest = new File(bootPath);
+ if(bootTest.exists()) {
+ // assume a static, existing directory will contain the native libs
+ this.bootPath = bootTest;
+ } else {
+ System.err.println("WARNING " + DefaultJniExtractorStub.class.getCanonicalName() + ": Boot path " + bootPath + " not found, falling back to default extraction behavior.");
+ }
+ }
+ }
+
+ /**
+ * If a bootPath
was provided to the constructor and exists,
+ * calculate the File
path without any extraction logic.
+ *
+ * If a bootPath
was NOT provided or does NOT exist, fallback on
+ * the default extraction behavior.
+ */
+ @Override
+ public File extractJni(String libPath, String libName) throws IOException {
+ // Lie and pretend it's already extracted at the bootPath location
+ if(bootPath != null) {
+ return new File(bootPath, NativeLibraryUtil.getPlatformLibraryName(libName));
+ }
+ // Fallback on default behavior
+ return super.extractJni(libPath, libName);
+ }
+
+ @Override
+ public void extractRegistered() throws IOException {
+ if(bootPath != null) {
+ return; // no-op
+ }
+ super.extractRegistered();
+ }
+}
diff --git a/src/main/java/jssc/SerialNativeInterface.java b/src/main/java/jssc/SerialNativeInterface.java
index fb86ff3893..d9e54f7917 100644
--- a/src/main/java/jssc/SerialNativeInterface.java
+++ b/src/main/java/jssc/SerialNativeInterface.java
@@ -85,6 +85,13 @@ else if(osName.equals("SunOS"))
else if(osName.equals("Mac OS X") || osName.equals("Darwin"))
osType = OS_MAC_OS_X;
try {
+ /**
+ * JSSC includes a small, platform-specific shared library and uses native-lib-loader for extraction.
+ * - First, native-lib-loader will attempt to load this library from the system library path.
+ * - Next, it will fallback to jssc.boot.library.path
+ * - Finally it will attempt to extract the library from from the jssc.jar file, and load it.
+ */
+ NativeLoader.setJniExtractor(new DefaultJniExtractorStub(null, System.getProperty("jssc.boot.library.path")));
NativeLoader.loadLibrary("jssc");
} catch (IOException ioException) {
throw new UnsatisfiedLinkError("Could not load the jssc library: " + ioException.getMessage());
diff --git a/src/test/java/jssc/bootpath/ManualBootLibraryPathFailedTest.java b/src/test/java/jssc/bootpath/ManualBootLibraryPathFailedTest.java
new file mode 100644
index 0000000000..2ae9ea6781
--- /dev/null
+++ b/src/test/java/jssc/bootpath/ManualBootLibraryPathFailedTest.java
@@ -0,0 +1,31 @@
+package jssc.bootpath;
+
+import jssc.SerialNativeInterface;
+import org.junit.Test;
+
+import static org.junit.Assert.assertTrue;
+import static org.junit.Assert.fail;
+
+/**
+ * Tests if a valid jssc.boot.library.path
which does NOT contain a native library
+ * will predictably fail. This test can be run regardless of whether or not a native binary was
+ * created during the build process.
+ *
+ * TODO: This MUST be in its own class to run in a separate JVM (https://stackoverflow.com/questions/68657855)
+ * - JUnit does NOT currently offer JVM unloading between methods.
+ * - maven-surefire-plugin DOES offer JVM unloading between classes using reuseForks=false
+ * - Unloading is needed due to NativeLoader.loadLibrary(...) calls System.loadLibrary(...) which is static
+ */
+public class ManualBootLibraryPathFailedTest {
+ @Test
+ public void testBootPathOverride() {
+ String nativeLibDir = "/"; // This should be valid on all platforms
+ System.setProperty("jssc.boot.library.path", nativeLibDir);
+ try {
+ SerialNativeInterface.getNativeLibraryVersion();
+ fail("Library loading should fail if path provided exists but does not contain a native library");
+ } catch (UnsatisfiedLinkError ignore) {
+ assertTrue("Library loading failed as expected with an invalid jssc.boot.library.path", true);
+ }
+ }
+}
diff --git a/src/test/java/jssc/bootpath/ManualBootLibraryPathTest.java b/src/test/java/jssc/bootpath/ManualBootLibraryPathTest.java
new file mode 100644
index 0000000000..77983d1b6f
--- /dev/null
+++ b/src/test/java/jssc/bootpath/ManualBootLibraryPathTest.java
@@ -0,0 +1,35 @@
+package jssc.bootpath;
+
+import jssc.SerialNativeInterface;
+import org.junit.Test;
+import org.scijava.nativelib.NativeLibraryUtil;
+
+import static org.hamcrest.CoreMatchers.*;
+import static org.junit.Assert.assertThat;
+import static org.junit.Assert.fail;
+
+/**
+ * Tests if a valid jssc.boot.library.path
which DOES contain a native library
+ * will predictably pass. This test can ONLY be run regardless if a native binary was created
+ * during the build process. See also maven.exclude.tests
.
+ *
+ * TODO: This MUST be in its own class to run in a separate JVM (https://stackoverflow.com/questions/68657855)
+ * - JUnit does NOT currently offer JVM unloading between methods.
+ * - maven-surefire-plugin DOES offer JVM unloading between classes using reuseForks=false
+ * - Unloading is needed due to NativeLoader.loadLibrary(...) calls System.loadLibrary(...) which is static
+ */
+public class ManualBootLibraryPathTest {
+ @Test
+ public void testBootPathOverride() {
+ String nativeLibDir = NativeLibraryUtil.getPlatformLibraryPath(System.getProperty("user.dir") + "/target/cmake/natives/");
+ System.setProperty("jssc.boot.library.path", nativeLibDir);
+ try {
+ final String nativeLibraryVersion = SerialNativeInterface.getNativeLibraryVersion();
+ assertThat(nativeLibraryVersion, is(not(nullValue())));
+ assertThat(nativeLibraryVersion, is(not("")));
+ } catch (UnsatisfiedLinkError linkError) {
+ linkError.printStackTrace();
+ fail("Should be able to call method!");
+ }
+ }
+}