Home | History | Annotate | Download | only in utils
      1 /*******************************************************************************
      2  * Copyright 2011 See AUTHORS file.
      3  *
      4  * Licensed under the Apache License, Version 2.0 (the "License");
      5  * you may not use this file except in compliance with the License.
      6  * You may obtain a copy of the License at
      7  *
      8  *   http://www.apache.org/licenses/LICENSE-2.0
      9  *
     10  * Unless required by applicable law or agreed to in writing, software
     11  * distributed under the License is distributed on an "AS IS" BASIS,
     12  * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
     13  * See the License for the specific language governing permissions and
     14  * limitations under the License.
     15  ******************************************************************************/
     16 
     17 package com.badlogic.gdx.utils;
     18 
     19 import java.io.File;
     20 import java.io.FileInputStream;
     21 import java.io.FileNotFoundException;
     22 import java.io.FileOutputStream;
     23 import java.io.IOException;
     24 import java.io.InputStream;
     25 import java.lang.reflect.Method;
     26 import java.util.HashSet;
     27 import java.util.UUID;
     28 import java.util.zip.CRC32;
     29 import java.util.zip.ZipEntry;
     30 import java.util.zip.ZipFile;
     31 
     32 /** Loads shared libraries from a natives jar file (desktop) or arm folders (Android). For desktop projects, have the natives jar
     33  * in the classpath, for Android projects put the shared libraries in the libs/armeabi and libs/armeabi-v7a folders.
     34  * @author mzechner
     35  * @author Nathan Sweet */
     36 public class SharedLibraryLoader {
     37 	static public boolean isWindows = System.getProperty("os.name").contains("Windows");
     38 	static public boolean isLinux = System.getProperty("os.name").contains("Linux");
     39 	static public boolean isMac = System.getProperty("os.name").contains("Mac");
     40 	static public boolean isIos = false;
     41 	static public boolean isAndroid = false;
     42 	static public boolean isARM = System.getProperty("os.arch").startsWith("arm");
     43 	static public boolean is64Bit = System.getProperty("os.arch").equals("amd64")
     44 		|| System.getProperty("os.arch").equals("x86_64");
     45 
     46 	// JDK 8 only.
     47 	static public String abi = (System.getProperty("sun.arch.abi") != null ? System.getProperty("sun.arch.abi") : "");
     48 
     49 	static {
     50 		String vm = System.getProperty("java.runtime.name");
     51 		if (vm != null && vm.contains("Android Runtime")) {
     52 			isAndroid = true;
     53 			isWindows = false;
     54 			isLinux = false;
     55 			isMac = false;
     56 			is64Bit = false;
     57 		}
     58 		if (!isAndroid && !isWindows && !isLinux && !isMac) {
     59 			isIos = true;
     60 			is64Bit = false;
     61 		}
     62 	}
     63 
     64 	static private final HashSet<String> loadedLibraries = new HashSet();
     65 
     66 	private String nativesJar;
     67 
     68 	public SharedLibraryLoader () {
     69 	}
     70 
     71 	/** Fetches the natives from the given natives jar file. Used for testing a shared lib on the fly.
     72 	 * @param nativesJar */
     73 	public SharedLibraryLoader (String nativesJar) {
     74 		this.nativesJar = nativesJar;
     75 	}
     76 
     77 	/** Returns a CRC of the remaining bytes in the stream. */
     78 	public String crc (InputStream input) {
     79 		if (input == null) throw new IllegalArgumentException("input cannot be null.");
     80 		CRC32 crc = new CRC32();
     81 		byte[] buffer = new byte[4096];
     82 		try {
     83 			while (true) {
     84 				int length = input.read(buffer);
     85 				if (length == -1) break;
     86 				crc.update(buffer, 0, length);
     87 			}
     88 		} catch (Exception ex) {
     89 			StreamUtils.closeQuietly(input);
     90 		}
     91 		return Long.toString(crc.getValue(), 16);
     92 	}
     93 
     94 	/** Maps a platform independent library name to a platform dependent name. */
     95 	public String mapLibraryName (String libraryName) {
     96 		if (isWindows) return libraryName + (is64Bit ? "64.dll" : ".dll");
     97 		if (isLinux) return "lib" + libraryName + (isARM ? "arm" + abi : "") + (is64Bit ? "64.so" : ".so");
     98 		if (isMac) return "lib" + libraryName + (is64Bit ? "64.dylib" : ".dylib");
     99 		return libraryName;
    100 	}
    101 
    102 	/** Loads a shared library for the platform the application is running on.
    103 	 * @param libraryName The platform independent library name. If not contain a prefix (eg lib) or suffix (eg .dll). */
    104 	public synchronized void load (String libraryName) {
    105 		// in case of iOS, things have been linked statically to the executable, bail out.
    106 		if (isIos) return;
    107 
    108 		libraryName = mapLibraryName(libraryName);
    109 		if (loadedLibraries.contains(libraryName)) return;
    110 
    111 		try {
    112 			if (isAndroid)
    113 				System.loadLibrary(libraryName);
    114 			else
    115 				loadFile(libraryName);
    116 		} catch (Throwable ex) {
    117 			throw new GdxRuntimeException("Couldn't load shared library '" + libraryName + "' for target: "
    118 				+ System.getProperty("os.name") + (is64Bit ? ", 64-bit" : ", 32-bit"), ex);
    119 		}
    120 		loadedLibraries.add(libraryName);
    121 	}
    122 
    123 	private InputStream readFile (String path) {
    124 		if (nativesJar == null) {
    125 			InputStream input = SharedLibraryLoader.class.getResourceAsStream("/" + path);
    126 			if (input == null) throw new GdxRuntimeException("Unable to read file for extraction: " + path);
    127 			return input;
    128 		}
    129 
    130 		// Read from JAR.
    131 		try {
    132 			ZipFile file = new ZipFile(nativesJar);
    133 			ZipEntry entry = file.getEntry(path);
    134 			if (entry == null) throw new GdxRuntimeException("Couldn't find '" + path + "' in JAR: " + nativesJar);
    135 			return file.getInputStream(entry);
    136 		} catch (IOException ex) {
    137 			throw new GdxRuntimeException("Error reading '" + path + "' in JAR: " + nativesJar, ex);
    138 		}
    139 	}
    140 
    141 	/** Extracts the specified file to the specified directory if it does not already exist or the CRC does not match. If file
    142 	 * extraction fails and the file exists at java.library.path, that file is returned.
    143 	 * @param sourcePath The file to extract from the classpath or JAR.
    144 	 * @param dirName The name of the subdirectory where the file will be extracted. If null, the file's CRC will be used.
    145 	 * @return The extracted file. */
    146 	public File extractFile (String sourcePath, String dirName) throws IOException {
    147 		try {
    148 			String sourceCrc = crc(readFile(sourcePath));
    149 			if (dirName == null) dirName = sourceCrc;
    150 
    151 			File extractedFile = getExtractedFile(dirName, new File(sourcePath).getName());
    152 			if (extractedFile == null) {
    153 				extractedFile = getExtractedFile(UUID.randomUUID().toString(), new File(sourcePath).getName());
    154 				if (extractedFile == null) throw new GdxRuntimeException(
    155 					"Unable to find writable path to extract file. Is the user home directory writable?");
    156 			}
    157 			return extractFile(sourcePath, sourceCrc, extractedFile);
    158 		} catch (RuntimeException ex) {
    159 			// Fallback to file at java.library.path location, eg for applets.
    160 			File file = new File(System.getProperty("java.library.path"), sourcePath);
    161 			if (file.exists()) return file;
    162 			throw ex;
    163 		}
    164 	}
    165 
    166 	/** Extracts the specified file into the temp directory if it does not already exist or the CRC does not match. If file
    167 	 * extraction fails and the file exists at java.library.path, that file is returned.
    168 	 * @param sourcePath The file to extract from the classpath or JAR.
    169 	 * @param dir The location where the extracted file will be written. */
    170 	public void extractFileTo (String sourcePath, File dir) throws IOException {
    171 		extractFile(sourcePath, crc(readFile(sourcePath)), new File(dir, new File(sourcePath).getName()));
    172 	}
    173 
    174 	/** Returns a path to a file that can be written. Tries multiple locations and verifies writing succeeds.
    175 	 * @return null if a writable path could not be found. */
    176 	private File getExtractedFile (String dirName, String fileName) {
    177 		// Temp directory with username in path.
    178 		File idealFile = new File(
    179 			System.getProperty("java.io.tmpdir") + "/libgdx" + System.getProperty("user.name") + "/" + dirName, fileName);
    180 		if (canWrite(idealFile)) return idealFile;
    181 
    182 		// System provided temp directory.
    183 		try {
    184 			File file = File.createTempFile(dirName, null);
    185 			if (file.delete()) {
    186 				file = new File(file, fileName);
    187 				if (canWrite(file)) return file;
    188 			}
    189 		} catch (IOException ignored) {
    190 		}
    191 
    192 		// User home.
    193 		File file = new File(System.getProperty("user.home") + "/.libgdx/" + dirName, fileName);
    194 		if (canWrite(file)) return file;
    195 
    196 		// Relative directory.
    197 		file = new File(".temp/" + dirName, fileName);
    198 		if (canWrite(file)) return file;
    199 
    200 		// We are running in the OS X sandbox.
    201 		if (System.getenv("APP_SANDBOX_CONTAINER_ID") != null) return idealFile;
    202 
    203 		return null;
    204 	}
    205 
    206 	/** Returns true if the parent directories of the file can be created and the file can be written. */
    207 	private boolean canWrite (File file) {
    208 		File parent = file.getParentFile();
    209 		File testFile;
    210 		if (file.exists()) {
    211 			if (!file.canWrite() || !canExecute(file)) return false;
    212 			// Don't overwrite existing file just to check if we can write to directory.
    213 			testFile = new File(parent, UUID.randomUUID().toString());
    214 		} else {
    215 			parent.mkdirs();
    216 			if (!parent.isDirectory()) return false;
    217 			testFile = file;
    218 		}
    219 		try {
    220 			new FileOutputStream(testFile).close();
    221 			if (!canExecute(testFile)) return false;
    222 			return true;
    223 		} catch (Throwable ex) {
    224 			return false;
    225 		} finally {
    226 			testFile.delete();
    227 		}
    228 	}
    229 
    230 	private boolean canExecute (File file) {
    231 		try {
    232 			Method canExecute = File.class.getMethod("canExecute");
    233 			if ((Boolean)canExecute.invoke(file)) return true;
    234 
    235 			Method setExecutable = File.class.getMethod("setExecutable", boolean.class, boolean.class);
    236 			setExecutable.invoke(file, true, false);
    237 
    238 			return (Boolean)canExecute.invoke(file);
    239 		} catch (Exception ignored) {
    240 		}
    241 		return false;
    242 	}
    243 
    244 	private File extractFile (String sourcePath, String sourceCrc, File extractedFile) throws IOException {
    245 		String extractedCrc = null;
    246 		if (extractedFile.exists()) {
    247 			try {
    248 				extractedCrc = crc(new FileInputStream(extractedFile));
    249 			} catch (FileNotFoundException ignored) {
    250 			}
    251 		}
    252 
    253 		// If file doesn't exist or the CRC doesn't match, extract it to the temp dir.
    254 		if (extractedCrc == null || !extractedCrc.equals(sourceCrc)) {
    255 			try {
    256 				InputStream input = readFile(sourcePath);
    257 				extractedFile.getParentFile().mkdirs();
    258 				FileOutputStream output = new FileOutputStream(extractedFile);
    259 				byte[] buffer = new byte[4096];
    260 				while (true) {
    261 					int length = input.read(buffer);
    262 					if (length == -1) break;
    263 					output.write(buffer, 0, length);
    264 				}
    265 				input.close();
    266 				output.close();
    267 			} catch (IOException ex) {
    268 				throw new GdxRuntimeException("Error extracting file: " + sourcePath + "\nTo: " + extractedFile.getAbsolutePath(),
    269 					ex);
    270 			}
    271 		}
    272 
    273 		return extractedFile;
    274 	}
    275 
    276 	/** Extracts the source file and calls System.load. Attemps to extract and load from multiple locations. Throws runtime
    277 	 * exception if all fail. */
    278 	private void loadFile (String sourcePath) {
    279 		String sourceCrc = crc(readFile(sourcePath));
    280 
    281 		String fileName = new File(sourcePath).getName();
    282 
    283 		// Temp directory with username in path.
    284 		File file = new File(System.getProperty("java.io.tmpdir") + "/libgdx" + System.getProperty("user.name") + "/" + sourceCrc,
    285 			fileName);
    286 		Throwable ex = loadFile(sourcePath, sourceCrc, file);
    287 		if (ex == null) return;
    288 
    289 		// System provided temp directory.
    290 		try {
    291 			file = File.createTempFile(sourceCrc, null);
    292 			if (file.delete() && loadFile(sourcePath, sourceCrc, file) == null) return;
    293 		} catch (Throwable ignored) {
    294 		}
    295 
    296 		// User home.
    297 		file = new File(System.getProperty("user.home") + "/.libgdx/" + sourceCrc, fileName);
    298 		if (loadFile(sourcePath, sourceCrc, file) == null) return;
    299 
    300 		// Relative directory.
    301 		file = new File(".temp/" + sourceCrc, fileName);
    302 		if (loadFile(sourcePath, sourceCrc, file) == null) return;
    303 
    304 		// Fallback to java.library.path location, eg for applets.
    305 		file = new File(System.getProperty("java.library.path"), sourcePath);
    306 		if (file.exists()) {
    307 			System.load(file.getAbsolutePath());
    308 			return;
    309 		}
    310 
    311 		throw new GdxRuntimeException(ex);
    312 	}
    313 
    314 	/** @return null if the file was extracted and loaded. */
    315 	private Throwable loadFile (String sourcePath, String sourceCrc, File extractedFile) {
    316 		try {
    317 			System.load(extractFile(sourcePath, sourceCrc, extractedFile).getAbsolutePath());
    318 			return null;
    319 		} catch (Throwable ex) {
    320 			return ex;
    321 		}
    322 	}
    323 }
    324