• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
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