Skip to content

Commit edea652

Browse files
author
Jonathan Feinberg
committed
Merge pull request #105 from kazimuth/goingnative
Only import the parts of libraries we need.
2 parents f39f4cf + bcdbdaa commit edea652

File tree

1 file changed

+190
-35
lines changed

1 file changed

+190
-35
lines changed

runtime/src/jycessing/LibraryImporter.java

Lines changed: 190 additions & 35 deletions
Original file line numberDiff line numberDiff line change
@@ -3,21 +3,30 @@
33
import org.python.core.Py;
44
import org.python.core.PyObject;
55
import org.python.core.PyStringMap;
6+
import org.python.core.PySystemState;
67
import org.python.google.common.base.Joiner;
78
import org.python.util.InteractiveConsole;
89

10+
import processing.core.PApplet;
11+
912
import java.io.File;
13+
import java.io.FileFilter;
14+
import java.io.FileReader;
1015
import java.io.IOException;
1116
import java.lang.reflect.Field;
1217
import java.lang.reflect.InvocationTargetException;
1318
import java.lang.reflect.Method;
1419
import java.net.MalformedURLException;
1520
import java.net.URL;
1621
import java.net.URLClassLoader;
22+
import java.util.ArrayList;
1723
import java.util.Arrays;
1824
import java.util.Enumeration;
25+
import java.util.Map;
26+
import java.util.HashMap;
1927
import java.util.HashSet;
2028
import java.util.List;
29+
import java.util.Properties;
2130
import java.util.Set;
2231
import java.util.regex.Pattern;
2332
import java.util.zip.ZipEntry;
@@ -43,6 +52,9 @@ private static void log(final String msg) {
4352
}
4453
}
4554

55+
private static final String PLATFORM = PApplet.platformNames[PApplet.platform];
56+
private static final String BITS = System.getProperty("os.arch").contains("64") ? "64" : "32";
57+
4658
/*
4759
* Directories where libraries may be found.
4860
*/
@@ -73,68 +85,210 @@ public PyObject __call__(final PyObject[] args, final String[] kws) {
7385
}
7486
});
7587
}
76-
88+
89+
/**
90+
* Locate the library in the library folder "libName", find what
91+
* it exports for the current platform, and add exports to the
92+
* system classpath, the system native library path, and jython's
93+
* sys.path.
94+
*
95+
* Then, go through the main jar file of the library and import
96+
* all of its publicly exposed classes.
97+
*
98+
* @param libName The name of the library to import
99+
*/
77100
protected void addLibrary(final String libName) {
78101
// Don't double-load anything.
79102
if (loadedLibs.contains(libName)) {
103+
log("...never mind, we already did");
80104
return;
81105
}
82106
loadedLibs.add(libName);
107+
83108

84-
// Find a directory with the given library name in the libSearchPath.
85-
File libNameDir = null;
86-
for (final File libDir : libSearchPath) {
87-
libNameDir = new File(String.format("%s/%s", libDir.getAbsolutePath(), libName));
88-
if (libNameDir.exists()) {
89-
break;
109+
110+
File libDir = null;
111+
for (final File searchDir : libSearchPath) {
112+
final File potentialDir = new File(searchDir.getAbsoluteFile(), libName);
113+
if (potentialDir.exists()) {
114+
if (libDir == null) {
115+
libDir = potentialDir;
116+
} else {
117+
System.err.println("Multiple libraries could be " + libName + ";");
118+
System.err.println("Picking " + libDir + " over " + potentialDir);
119+
}
90120
}
91121
}
92-
if (libNameDir == null || !libNameDir.exists()) {
93-
// Raise the exception in the interpreter, which will give us nice line numbers
94-
// when the error is reported in the PDE editor.
122+
123+
if (libDir == null) {
95124
interp.exec("raise Exception('This sketch requires the \"" + libName + "\" library.')");
96125
}
126+
final File contentsDir = new File(libDir, "library");
127+
if (!contentsDir.exists()) {
128+
interp.exec("raise Exception('The library " + libName + " is malformed and won't import.')");
129+
}
130+
final File mainJar = new File(contentsDir, libName + ".jar");
131+
132+
final List<File> resources = findResources(contentsDir);
133+
final PySystemState sys = Py.getSystemState();
134+
for (final File resource : resources) {
135+
final String name = resource.getName();
136+
if (name.endsWith(".jar") || name.endsWith(".zip")) {
137+
// Contains stuff we want
138+
addJarToClassLoader(resource.getAbsoluteFile());
139+
140+
log("Appending " + resource.getAbsolutePath() + " to sys.path.");
141+
sys.path.append(Py.newString(resource.getAbsolutePath()));
142+
143+
// Are we missing any extensions?
144+
} else if (name.endsWith(".so") || name.endsWith(".dll") || name.endsWith(".dylib") || name.endsWith(".jnilib")) {
145+
// Add *containing directory* to native search path
146+
addDirectoryToNativeSearchPath(resource.getAbsoluteFile().getParentFile());
147+
}
148+
}
97149

98-
final File library = new File(libNameDir, "library");
99-
100-
addToRuntime(library);
101-
102-
// Find the main library jar file, which can be found at libname/library/libname.jar.
103-
final String jarPath = String.format("%s/%s.jar", library.getAbsolutePath(), libName);
104150
try {
105-
importPublicClassesFromJar(jarPath);
106-
} catch (final IOException e) {
151+
importPublicClassesFromJar(mainJar);
152+
} catch (final Exception e) {
107153
throw new RuntimeException("While trying to add " + libName + " library:", e);
108154
}
109155
}
110156

111157
/**
112-
* Recursively add the given file to the system classloader, the native lib
113-
* search path, and the Jython sys.path.
158+
* Find all of the resources a library requires on this platform.
159+
* See https://github.com/processing/processing/wiki/Library-Basics.
114160
*
115-
* <p>The given file should be either a directory or a jar file.
161+
* First, finds the library.
162+
* Second, tries to parse export.txt, and follow its instructions.
163+
* Third, tries to understand folder structure, and export according to that.
116164
*
117-
* @param file The directory or jar file to make available to sketch runtime.
165+
* @param libName The name of the library to add.
166+
* @return The list of files we need to import.
118167
*/
119-
private void addToRuntime(final File file) {
120-
addJarToClassLoader(file);
121-
if (file.isDirectory()) {
122-
addDirectoryToNativeSearchPath(file);
123-
}
124-
Py.getSystemState().path.insert(0, Py.newString(file.getAbsolutePath()));
125-
if (file.isDirectory()) {
126-
for (final File f : file.listFiles()) {
127-
if (f.isDirectory() || f.getName().endsWith(".jar")) {
128-
addToRuntime(f);
129-
}
168+
protected List<File> findResources(final File contentsDir) {
169+
log("Exploring " + contentsDir + " for resources.");
170+
List<File> resources;
171+
resources = findResourcesFromExportTxt(contentsDir);
172+
if (resources == null) {
173+
log("Falling back to directory structure.");
174+
resources = findResourcesFromDirectoryStructure(contentsDir);
175+
}
176+
return resources;
177+
}
178+
179+
private List<File> findResourcesFromExportTxt(final File contentsDir) {
180+
final File exportTxt = new File(contentsDir, "export.txt");
181+
if (!exportTxt.exists()) {
182+
log("No export.txt in " + contentsDir.getAbsolutePath());
183+
return null;
184+
}
185+
final Map<String, String[]> exportTable;
186+
try {
187+
exportTable = parseExportTxt(exportTxt);
188+
} catch (Exception e) {
189+
log("Couldn't parse export.txt: " + e.getMessage());
190+
return null;
191+
}
192+
193+
final String[] resourceNames;
194+
195+
// Check from most-specific to least-specific:
196+
if (exportTable.containsKey("application." + PLATFORM + BITS)) {
197+
log("Found 'application." + PLATFORM + BITS + "' in export.txt");
198+
resourceNames = exportTable.get("application." + PLATFORM + BITS);
199+
} else if (exportTable.containsKey("application." + PLATFORM)) {
200+
log("Found 'application." + PLATFORM + "' in export.txt");
201+
resourceNames = exportTable.get("application." + PLATFORM);
202+
} else if (exportTable.containsKey("application")) {
203+
log("Found 'application' in export.txt");
204+
resourceNames = exportTable.get("application");
205+
} else {
206+
log("No matching platform in " + exportTxt.getAbsolutePath());
207+
return null;
208+
}
209+
final List<File> resources = new ArrayList<>();
210+
for (final String resourceName : resourceNames) {
211+
final File resource = new File(contentsDir, resourceName);
212+
if (resource.exists()) {
213+
resources.add(resource);
214+
} else {
215+
log(resourceName + " is mentioned in " + exportTxt.getAbsolutePath() + "but doesn't actually exist. Moving on.");
216+
continue;
217+
}
218+
}
219+
return resources;
220+
}
221+
222+
private List<File> findResourcesFromDirectoryStructure(final File contentsDir) {
223+
final List<String> childNames = Arrays.asList(contentsDir.list());
224+
final List<File> resources = new ArrayList<File>();
225+
226+
// Find platform-specific stuff
227+
File platformDir = null;
228+
if (childNames.contains(PLATFORM + BITS)) {
229+
final File potentialPlatformDir = new File(contentsDir, PLATFORM + BITS);
230+
if (potentialPlatformDir.isDirectory()) {
231+
platformDir = potentialPlatformDir;
232+
}
233+
}
234+
if (platformDir == null && childNames.contains(PLATFORM)) {
235+
final File potentialPlatformDir = new File(contentsDir, PLATFORM + BITS);
236+
if (potentialPlatformDir.isDirectory()) {
237+
platformDir = potentialPlatformDir;
238+
}
239+
}
240+
if (platformDir != null) {
241+
log("Found platform-specific directory " + platformDir.getAbsolutePath());
242+
for (final File resource : platformDir.listFiles()) {
243+
resources.add(resource);
244+
}
245+
}
246+
247+
// Find multi-platform stuff; always do this
248+
final File[] commonResources = contentsDir.listFiles(new FileFilter() {
249+
@Override
250+
public boolean accept(File file) {
251+
return !file.isDirectory();
252+
}
253+
});
254+
for (final File resource : commonResources) {
255+
resources.add(resource);
256+
}
257+
return resources;
258+
}
259+
260+
/**
261+
* Parse an export.txt file to figure out what we need to load for this platform.
262+
* This is all duplicated from processing.app.Library / processing.app.Base,
263+
* but we don't have the PDE around at runtime so we can't use them.
264+
*
265+
* @param exportTxt The export.txt file; must exist.
266+
*/
267+
private Map<String, String[]> parseExportTxt(final File exportTxt) throws Exception {
268+
log("Parsing " + exportTxt.getAbsolutePath());
269+
270+
final Properties exportProps = new Properties();
271+
try (final FileReader in = new FileReader(exportTxt)) {
272+
exportProps.load(in);
273+
}
274+
275+
final Map<String, String[]> exportTable = new HashMap<>();
276+
277+
for (final String platform : exportProps.stringPropertyNames()) {
278+
final String exportCSV = exportProps.getProperty(platform);
279+
final String[] exports = PApplet.splitTokens(exportCSV, ",");
280+
for (int i = 0; i < exports.length; i++) {
281+
exports[i] = exports[i].trim();
130282
}
283+
exportTable.put(platform, exports);
131284
}
285+
return exportTable;
132286
}
133287

134288
/**
135289
* Use a brittle and egregious hack to forcibly add the given jar file to the
136290
* system classloader.
137-
* @param jar The jar to add to the system clsasloader.
291+
* @param jar The jar to add to the system classloader.
138292
*/
139293
private void addJarToClassLoader(final File jar) {
140294
try {
@@ -205,7 +359,8 @@ private void addDirectoryToNativeSearchPath(final File dllDir) {
205359
from com.foo import Banana
206360
from com.bar import Kiwi
207361
*/
208-
private void importPublicClassesFromJar(final String jarPath) throws IOException {
362+
private void importPublicClassesFromJar(final File jarPath) throws IOException {
363+
log("Importing public classes from " + jarPath.getAbsolutePath());
209364
try (final ZipFile file = new ZipFile(jarPath)) {
210365
final Enumeration<? extends ZipEntry> entries = file.entries();
211366
while (entries.hasMoreElements()) {

0 commit comments

Comments
 (0)