All Downloads are FREE. Search and download functionalities are using the official Maven repository.

com.shankyank.jniloader.JNILoader Maven / Gradle / Ivy

Go to download

Dynamically loads bundled native libraries based on the current runtime platform and system architecture.

The newest version!
package com.shankyank.jniloader;

import static com.shankyank.jniloader.Architecture.*;
import static com.shankyank.jniloader.OperatingSystem.*;

import java.io.File;
import java.io.FileFilter;
import java.io.FileInputStream;
import java.io.FileNotFoundException;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.lang.reflect.Field;
import java.security.DigestOutputStream;
import java.security.MessageDigest;
import java.security.NoSuchAlgorithmException;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collections;
import java.util.HashMap;
import java.util.HashSet;
import java.util.Iterator;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.TreeSet;
import java.util.regex.Pattern;
import java.util.zip.ZipEntry;
import java.util.zip.ZipInputStream;
import org.apache.commons.cli.CommandLine;
import org.apache.commons.cli.GnuParser;
import org.apache.commons.cli.HelpFormatter;
import org.apache.commons.cli.OptionBuilder;
import org.apache.commons.cli.Options;
import org.apache.commons.cli.ParseException;
import org.apache.commons.io.IOUtils;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

/**
 * The JNILoader dynamically extracts a set of native library files found
 * on the application classpath to a temporary directory and triggers a
 * refresh of the JVM's cached java.library.path so those libraries can
 * be loaded. This must be done before any calls to System.loadLibrary().
 *
 * It also provides a command line utility that outputs the architecture
 * and library suffix used to identify the appropriate files on the current
 * platform.
 */
public final class JNILoader {
    /** The class logger. */
    private static final Logger LOG = LoggerFactory.getLogger(JNILoader.class);

    /** The System library path property. */
    private static final String JAVA_LIBRARY_PATH = "java.library.path";

    /** The set of standard JVM platforms based on the JDK8 installers. */
    private static final List JAVA_STANDARD_PLATFORMS = Collections.unmodifiableList(Arrays.asList(
            new Platform(WINDOWS, X86),
            new Platform(WINDOWS, X86_64),
            new Platform(LINUX, X86),
            new Platform(LINUX, X86_64),
            new Platform(DARWIN, X86_64),
            new Platform(SOLARIS, SPARCV9),
            new Platform(SOLARIS, X86_64),
            new Platform(CYGWIN, X86),
            new Platform(CYGWIN, X86_64),
            new Platform(MINGW, X86),
            new Platform(MINGW, X86_64),
            new Platform(MSYS, X86),
            new Platform(MSYS, X86_64)
    ));

    /**
     * The map of fallback platforms. If an archive is not found for
     * a particular platform, the fallback platform will be used.
     */
    private static final Map FALLBACK_PLATFORMS = initFallbackPlatforms();
    private static Map initFallbackPlatforms() {
        Map map = new HashMap<>();
        map.put(new Platform(CYGWIN, X86), new Platform(WINDOWS, X86));
        map.put(new Platform(CYGWIN, X86_64), new Platform(WINDOWS, X86_64));
        map.put(new Platform(MINGW, X86), new Platform(WINDOWS, X86));
        map.put(new Platform(MINGW, X86_64), new Platform(WINDOWS, X86_64));
        map.put(new Platform(MSYS, X86), new Platform(WINDOWS, X86));
        map.put(new Platform(MSYS, X86_64), new Platform(WINDOWS, X86_64));
        return Collections.unmodifiableMap(map);
    }

    /** The pattern used to check for parent directory indicators in the temporary library path. */
    private static final Pattern PARENT_DIR = Pattern.compile("(^|/)\\.\\.(/|$)");

    /** The system temp directory. */
    private static final File TMP_DIR = new File(System.getProperty("java.io.tmpdir"));

    /** A filter that returns only directories. */
    private static final FileFilter DIR_FILTER = new FileFilter() {
        @Override
        public boolean accept(final File file) {
            return file.isDirectory();
        }
    };

    /** The canonical Platform for the current runtime. */
    public static final Platform RUNTIME_PLATFORM = new Platform(getSystemOS(), getSystemArchitecture());

    /** The system path lock. */
    private static final Object SYS_PATH_LOCK = new Object();

    /** The platform used by this loader. */
    private final Platform platform;

    /** The temporary directory where native libraries will be extracted. */
    private final File libraryPath;

    /** The set of library names that have been initialized. */
    private final Set extractedLibs;

    /**
     * Create a new JNILoader for the current runtime platform that
     * extracts libraries to ${java.io.tmpdir}/jni-loader/${os}/${arch}.
     */
    public JNILoader() {
        this("", RUNTIME_PLATFORM);
    }

    /**
     * Create a new JNILoader for the current runtime platform that extracts
     * libraries to ${java.io.tmpdir}/${tmpPath}/jni-loader/${os}/${arch}.
     * @param tmpPath a subdirectory below java.io.tmpdir where native libraries will be extracted
     */
    public JNILoader(final String tmpPath) {
        this(tmpPath, RUNTIME_PLATFORM);
    }

    /**
     * Creates a new JNILoader for the specified runtime platform that
     * extracts libraries to ${java.io.tmpdir}/${tmpPath}/jni-loader/${os}/${arch}.
     * @param tmpPath a subdirectory below java.io.tmpdir where native libraries will be extracted
     * @param pform the target platform
     */
    protected JNILoader(final String tmpPath, final Platform pform) {
        if (pform == null) {
            throw new NullPointerException("Platform is required");
        }
        platform = pform;

        // remove trailing slashes
        String subDir = (tmpPath != null ? tmpPath.trim() : "").replaceFirst("^/*", "");
        if (PARENT_DIR.matcher(subDir).find()) {
            throw new IllegalArgumentException(String.format("Extraction path [%s] cannot traverse parent directories", subDir));
        }
        String path = String.format("%s/jni-loader/%s", subDir, platform.getSubdirectory());
        libraryPath = new File(TMP_DIR, path);

        extractedLibs = Collections.synchronizedSet(new HashSet());
    }

    /**
     * Extracts the requested native libraries for the runtime platform and
     * updates the java.library.path so they can be loaded. This method must
     * be called for a particular set of libraries before any calls to
     * System.loadLibrary() referencing the extracted files.
     * @param resourcePath the path (relative to the classpath root) containing the native library packages
     * @param libPackage the basename of the archive containing the desired native libraries
     * @return true if the native libraries are successfully extracted
     * @throws IOException if the libraries cannot be extracted
     */
    public boolean extractLibs(final String resourcePath, final String libPackage) throws IOException {
        NativeLib nativeLib = new NativeLib(resourcePath, libPackage);
        String libPath = libraryPath.getPath();
        synchronized (extractedLibs) {
            if (!extractedLibs.contains(nativeLib)) {
                LOG.info("Extracting {} native libraries from {} to {}", nativeLib.libPackage, nativeLib.getArchivePath(platform), libPath);
                if (!(libraryPath.isDirectory() || libraryPath.mkdirs())) {
                    throw new FileNotFoundException(String.format("Unable to create library directory: %s", libPath));
                }
                if (verifyLibs(nativeLib)) {
                    LOG.info("{} native libraries already exist.", nativeLib.libPackage);
                    extractedLibs.add(nativeLib);
                } else {
                    ZipInputStream packaged = openNativeArchive(nativeLib);
                    try {
                        List extractedFiles = new ArrayList<>();
                        for (ZipEntry entry = packaged.getNextEntry(); entry != null; entry = packaged.getNextEntry()) {
                            File tmpFile = new File(libraryPath, entry.getName());
                            if (entry.isDirectory()) {
                                LOG.debug("Creating directory: {}", tmpFile.getPath());
                                if (!(tmpFile.isDirectory() || tmpFile.mkdirs())) {
                                    LOG.error("Unable to create directory {}", tmpFile.getPath());
                                    return false;
                                }
                            } else {
                                LOG.debug("Extracting native library: {}", tmpFile.getPath());
                                try {
                                    FileOutputStream out = new FileOutputStream(tmpFile);
                                    IOUtils.copy(new EntryStream(packaged), out);
                                    extractedFiles.add(tmpFile);
                                } catch (IOException ioe) {
                                    throw new IOException(String.format("Error extracting native library [%s] to %s", entry.getName(), libPath), ioe);
                                }
                            }
                        }
                        // if running on OS X, ensure both .dylib and .jnilib files exist
                        // Java 6 expects .jnilib, Java 7+ expects .dylib
                        if (platform.getOperatingSystem() == DARWIN) {
                            for (File lib : extractedFiles) {
                                String altExt;
                                if (lib.getName().endsWith(".dylib")) {
                                    altExt = ".jnilib";
                                } else if (lib.getName().endsWith(".jnilib")) {
                                    altExt = ".dylib";
                                } else {
                                    // skip all non-library files in the archive
                                    continue;
                                }
                                File target = new File(lib.getParentFile(), lib.getName().replaceAll("\\.(dylib|jnilib)$", altExt));
                                if (!target.exists()) {
                                    LOG.info("[{}] (OS X) Copying {} to {}", libPackage, lib.getName(), target.getName());
                                    IOUtils.copy(new FileInputStream(lib), new FileOutputStream(target));
                                    String srcMd5 = md5sum(new FileInputStream(lib));
                                    String destMd5 = md5sum(new FileInputStream(target));
                                    if (!srcMd5.equals(destMd5)) {
                                        LOG.error("[{}] Error copying {} to {}. Bad checksum", libPackage, lib.getName(), target.getName());
                                        throw new IOException(String.format("Error copying %s to %s. Bad checksum.", lib.getName(), target.getName()));
                                    }
                                }
                            }
                        }
                    } finally {
                        packaged.close();
                    }
                    if (!verifyLibs(nativeLib)) {
                        throw new IOException(String.format("%s native libraries were not properly extracted to %s", nativeLib.libPackage, libPath));
                    }
                }
            }
        }
        return updateSystemPath();
    }

    /**
     * Updates the system java.library.path to include the directory where
     * native libraries will be placed by this loader.
     * @return true if the system path has been successfully updated
     * @throws IOException if an error occurs updating the path
     */
    private boolean updateSystemPath() throws IOException {
        Set libDirs = buildLibTree(libraryPath, new TreeSet());
        synchronized (SYS_PATH_LOCK) {
            String javaLibPath = System.getProperty(JAVA_LIBRARY_PATH);
            if (javaLibPath == null) {
                javaLibPath = "";
            }
            List sysPaths = Arrays.asList(javaLibPath.split(File.pathSeparator));
            for (String path : sysPaths) {
                String cPath = new File(path).getCanonicalPath();
                for (Iterator libIter = libDirs.iterator(); libIter.hasNext();) {
                    File libDir = libIter.next();
                    if (libDir.getCanonicalPath().equals(cPath)) {
                        libIter.remove();
                    }
                }
            }
            LOG.debug("Adding {} directories to java.library.path", libDirs.size());
            if (!libDirs.isEmpty()) {
                StringBuilder pathBuilder = new StringBuilder(javaLibPath);
                for (File libDir : libDirs) {
                    if (pathBuilder.length() > 0) {
                        pathBuilder.append(File.pathSeparator);
                    }
                    pathBuilder.append(libDir.getCanonicalPath());
                }
                LOG.info("Updating java.library.path: {}", pathBuilder);
                System.setProperty(JAVA_LIBRARY_PATH, pathBuilder.toString());

                try {
                    Field fieldSysPath = ClassLoader.class.getDeclaredField("sys_paths");
                    fieldSysPath.setAccessible(true);
                    fieldSysPath.set(null, null);
                } catch (IllegalAccessException | NoSuchFieldException e) {
                    throw new IllegalStateException("Unable to clear system path cache", e);
                }
            }
            LOG.debug("java.library.path: {}", System.getProperty(JAVA_LIBRARY_PATH));
        }
        return true;
    }

    /**
     * Builds a depth-first list of all subdirectories of the
     * library extraction path for this loader.
     * @param root the root of the current directory tree
     * @param tree the set of subdirectories
     * @throws IOException if errors occur identifying the canonical directories
     */
    private Set buildLibTree(final File root, final Set tree) throws IOException {
        if (root.isDirectory()) {
            tree.add(root.getCanonicalFile());
            for (File dir : root.listFiles(DIR_FILTER)) {
                buildLibTree(dir, tree);
            }
        }
        return tree;
    }

    /**
     * Verifies that all native libraries have been successfully extracted.
     * @param nativeLib the package of libraries to verify
     * @return true if all native libraries have been successfully extracted
     * @throws IOException if errors occur verifying the libraries
     */
    private boolean verifyLibs(final NativeLib nativeLib) throws IOException {
        ZipInputStream packaged = openNativeArchive(nativeLib);
        try {
            for (ZipEntry entry = packaged.getNextEntry(); entry != null; entry = packaged.getNextEntry()) {
                File extractedFile = new File(libraryPath, entry.getName());
                if (entry.isDirectory()) {
                    if (!extractedFile.isDirectory()) {
                        LOG.warn("[{}] missing directory: {}", nativeLib.libPackage, entry.getName());
                    }
                } else {
                    if (!extractedFile.isFile()) {
                        LOG.warn("[{}] missing file: {}", nativeLib.libPackage, entry.getName());
                        return false;
                    }

                    String packagedMd5 = md5sum(new EntryStream(packaged));
                    String extractedMd5 = md5sum(new FileInputStream(extractedFile));

                    LOG.debug("[{}] {} (packaged):  {}", nativeLib.libPackage, entry.getName(), packagedMd5);
                    LOG.debug("[{}] {} (extracted): {}", nativeLib.libPackage, entry.getName(), extractedMd5);

                    if (!packagedMd5.equals(extractedMd5)) {
                        LOG.warn("[{}] bad checksum: {}", nativeLib.libPackage, entry.getName());
                        return false;
                    }
                }
            }
        } finally {
            packaged.close();
        }
        return true;
    }

    /**
     * Generates an MD5 hash for the provided InputStream.
     * @param input the input stream
     * @return the MD5 hash of the input
     * @throws IOException if errors occur processing the stream
     */
    private String md5sum(final InputStream input) throws IOException {
        try {
            MessageDigest digest = MessageDigest.getInstance("MD5");
            DigestOutputStream digestStream = new DigestOutputStream(new SinkOutputStream(), digest);
            IOUtils.copy(input, digestStream);
            StringBuilder hash = new StringBuilder();
            for (byte b : digest.digest()) {
                hash.append(Integer.toHexString(b & 0xff));
            }
            return hash.toString();
        } catch (NoSuchAlgorithmException nsae) {
            throw new IllegalStateException("MD5 algorithm is not available.", nsae);
        } finally {
            input.close();
        }
    }

    /**
     * Opens the packaged archive containing the native libraries for the current platform.
     * @param nativeLib the package of libraries to open
     * @return a ZipFile for reading the archive
     * @throws FileNotFoundException if the library package for the runtime platform is not available
     */
    private ZipInputStream openNativeArchive(final NativeLib nativeLib) throws FileNotFoundException {
        InputStream nativeArchive = null;
        // iterate over all available fallback platforms to find archive
        for (Platform pform = platform; nativeArchive == null && pform != null; pform = FALLBACK_PLATFORMS.get(pform)) {
            String archive = nativeLib.getArchivePath(pform);
            LOG.debug("[{}] Opening archive {}", nativeLib.libPackage, archive);
            nativeArchive = JNILoader.class.getResourceAsStream(archive);
        }
        if (nativeArchive == null) {
            throw new FileNotFoundException(String.format("Unable to find native library for %s [%s]", RUNTIME_PLATFORM.getArchiveSuffix(),
                    nativeLib.getArchivePath(platform)));
        }
        return new ZipInputStream(nativeArchive);
    }

    public static void main(final String[] args) {
        Options opts = new Options();
        opts.addOption(OptionBuilder.withLongOpt("help").withDescription("Display this help text.").create('?'));
        opts.addOption(OptionBuilder.withLongOpt("list-platforms").
                withDescription("List the standard platforms recognized by the JNILoader. " +
                        "Other platforms may be supported by supplying archive files in the format --.").
                create('p'));
        opts.addOption(OptionBuilder.withLongOpt("os").withDescription("Display canonical OS name.").create('o'));
        opts.addOption(OptionBuilder.withLongOpt("arch").withDescription("Display canonical architecture name.").create('a'));
        opts.addOption(OptionBuilder.withLongOpt("init").
                        withDescription("Extracts the native libraries for the current platform and updates the system library path.").
                        create('i')
        );
        opts.addOption(OptionBuilder.
                        withLongOpt("resource-path").
                        hasArg(true).
                        withArgName("path").
                        withDescription("The path, relative to the classpath root, containing the library bundles.").
                        create('r')
        );
        opts.addOption(OptionBuilder.
                        withLongOpt("lib-name").
                        hasArg(true).
                        withArgName("lib_package").
                        withDescription("The base name of the library bundles. Bundles must be named ${basename}-${os}-${arch}.zip").
                        create('l')
        );

        String usage = "JNILoader -? | -p | -l  [-r ] [-a] [-o] [-i]";
        HelpFormatter help = new HelpFormatter();
        help.setWidth(120);
        CommandLine commandLine = null;
        try {
            commandLine = new GnuParser().parse(opts, args);
        } catch (ParseException e) {
            System.out.println(e.getMessage());
            help.printHelp(usage, opts);
            System.exit(1);
        }

        if (commandLine.hasOption('?')) {
            help.printHelp(usage, opts);
            System.exit(0);
        }

        if (commandLine.hasOption('p')) {
            for (Platform platform : JAVA_STANDARD_PLATFORMS) {
                System.out.println(platform.toString());
            }
            System.exit(0);
        }

        if (!commandLine.hasOption('l')) {
            System.out.println("Missing required option: -l");
            help.printHelp(usage, opts);
            System.exit(1);
        }
        String resourcePath = commandLine.getOptionValue('r', "");
        String libPackage = commandLine.getOptionValue('l');

        NativeLib lib = null;
        try {
            lib = new NativeLib(resourcePath, libPackage);
        } catch (IllegalArgumentException iae) {
            System.out.println(iae.getMessage());
            help.printHelp(usage, opts);
            System.exit(1);
        }

        JNILoader loader = new JNILoader();
        if (commandLine.hasOption('o')) {
            System.out.printf("OS:   %s%n", loader.platform.getOperatingSystem().getNativeString());
        }
        if (commandLine.hasOption('a')) {
            System.out.printf("Arch: %s%n", loader.platform.getArchitecture().getCanonicalName());
        }
        System.out.printf("Native Lib Archive: %s%n", lib.getArchivePath(loader.platform));
        System.out.printf("Temp Directory:     %s%n", loader.libraryPath.getAbsolutePath());
        if (commandLine.hasOption('i')) {
            try {
                System.out.printf("Init? %s%n", loader.extractLibs(resourcePath, libPackage));
            } catch (IOException ioe) {
                System.out.printf("Error extracting native libraries [%s]: %s", lib, ioe.getMessage());
                ioe.printStackTrace();
                System.exit(1);
            }
        }
    }

    /**
     * Wrapper for a combination of resource path and library name.
     */
    private static class NativeLib {
        public final String resourcePath;
        public final String libPackage;

        public NativeLib(final String rPath, final String lPkg) {
            if (rPath != null && !rPath.trim().isEmpty() && !rPath.trim().startsWith("/")) {
                throw new IllegalArgumentException("Resource path must be empty or start with /");
            }
            if (lPkg == null || lPkg.trim().isEmpty()) {
                throw new IllegalArgumentException("Library package cannot be empty");
            }
            if (lPkg != null && lPkg.contains("/")) {
                throw new IllegalArgumentException("Library package cannot contain /");
            }
            String resPath = rPath != null ? rPath.trim() : "";
            resourcePath = resPath.isEmpty() || resPath.endsWith("/") ? resPath : String.format("%s/", resPath);
            libPackage = lPkg.trim();
        }

        public String getArchivePath(final Platform pform) {
            return String.format("%s%s-%s.zip", resourcePath, libPackage, pform.getArchiveSuffix());
        }

        @Override
        public String toString() {
            return String.format("%s [%s]", libPackage, resourcePath);
        }

        @Override
        public boolean equals(Object o) {
            if (this == o) return true;
            if (o == null || getClass() != o.getClass()) return false;

            NativeLib nativeLib = (NativeLib) o;

            if (!libPackage.equals(nativeLib.libPackage)) return false;
            if (!resourcePath.equals(nativeLib.resourcePath)) return false;

            return true;
        }

        @Override
        public int hashCode() {
            int result = resourcePath.hashCode();
            result = 31 * result + libPackage.hashCode();
            return result;
        }
    }

    /**
     * An input stream that delegates to a ZipInputStream, calling the closeEntry() method
     * when reading is complete instead of closing the entire stream.
     */
    private static class EntryStream extends InputStream {
        private final ZipInputStream zipStream;

        public EntryStream(final ZipInputStream zis) {
            zipStream = zis;
        }

        @Override
        public int read() throws IOException {
            return zipStream.read();
        }

        @Override
        public int read(byte[] b) throws IOException {
            return zipStream.read(b);
        }

        @Override
        public int read(byte[] b, int off, int len) throws IOException {
            return zipStream.read(b, off, len);
        }

        @Override
        public long skip(long n) throws IOException {
            return zipStream.skip(n);
        }

        @Override
        public int available() throws IOException {
            return zipStream.available();
        }

        @Override
        public void close() throws IOException {
            zipStream.closeEntry();
        }

        @Override
        public void mark(int readlimit) {
            zipStream.mark(readlimit);
        }

        @Override
        public void reset() throws IOException {
            zipStream.reset();
        }

        @Override
        public boolean markSupported() {
            return zipStream.markSupported();
        }
    }

    /**
     * OutputStream that ignores all writes.
     */
    private static class SinkOutputStream extends OutputStream {
        @Override
        public void write(int b) throws IOException {
        }

        @Override
        public void write(byte[] b) throws IOException {
        }

        @Override
        public void write(byte[] b, int off, int len) throws IOException {
        }
    }

    /**
     * Container class for an OS/Architecture combination.
     */
    public static class Platform {
        private final OperatingSystem operatingSystem;
        private final Architecture architecture;

        /**
         * Create a new platform.
         * @param os the operating system
         * @param arch the architecture
         */
        public Platform(final OperatingSystem os, final Architecture arch) {
            this.operatingSystem = os;
            this.architecture = arch;
        }

        /**
         * @return the operating system of this platform
         */
        public OperatingSystem getOperatingSystem() {
            return operatingSystem;
        }

        /**
         * @return the architecture of this platform
         */
        public Architecture getArchitecture() {
            return architecture;
        }

        /**
         * @return the archive suffix for this platform: [os]-[arch]
         */
        private String getArchiveSuffix() {
            return String.format("%s-%s", operatingSystem.getNativeString(), architecture.getCanonicalName());
        }

        /**
         * @return the subdirectory for this platform: [os]/[arch]
         */
        private String getSubdirectory() {
            return String.format("%s/%s", operatingSystem.getNativeString(), architecture.getCanonicalName());
        }

        @Override
        public String toString() {
            return getArchiveSuffix();
        }

        @Override
        public boolean equals(Object o) {
            if (this == o) return true;
            if (o == null || getClass() != o.getClass()) return false;

            Platform platform = (Platform) o;

            if (architecture != platform.architecture) return false;
            if (operatingSystem != platform.operatingSystem) return false;

            return true;
        }

        @Override
        public int hashCode() {
            int result = operatingSystem.hashCode();
            result = 31 * result + architecture.hashCode();
            return result;
        }
    }
}




© 2015 - 2025 Weber Informatics LLC | Privacy Policy