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!"); + } + } +}