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 SharedLibraryLoader()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 */ SharedLibraryLoader(String nativesJar)73 public SharedLibraryLoader (String nativesJar) { 74 this.nativesJar = nativesJar; 75 } 76 77 /** Returns a CRC of the remaining bytes in the stream. */ crc(InputStream input)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. */ mapLibraryName(String libraryName)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). */ load(String libraryName)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 readFile(String path)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. */ extractFile(String sourcePath, String dirName)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. */ extractFileTo(String sourcePath, File dir)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. */ getExtractedFile(String dirName, String fileName)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. */ canWrite(File file)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 canExecute(File file)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 extractFile(String sourcePath, String sourceCrc, File extractedFile)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. */ loadFile(String sourcePath)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. */ loadFile(String sourcePath, String sourceCrc, File extractedFile)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