Skip to content

Commit 61c5e83

Browse files
authored
LibraryImporter: Fix ad-hoc code (#137)
Signed-off-by: Ce Gao <[email protected]>
1 parent eab8aee commit 61c5e83

File tree

3 files changed

+235
-15
lines changed

3 files changed

+235
-15
lines changed

src/rprocessing/Runner.java

+5-5
Original file line numberDiff line numberDiff line change
@@ -100,7 +100,7 @@ public static synchronized void runSketchBlocking(final RunnableSketch sketch,
100100
for (final File dir : libDirs) {
101101
searchForExtraStuff(dir, libs);
102102
}
103-
libraryImporter.loadLibraries(libs);
103+
libraryImporter.injectIntoScope();
104104
}
105105

106106
try {
@@ -127,7 +127,7 @@ private static void searchForExtraStuff(final File dir, final Set<String> entrie
127127
return;
128128
}
129129

130-
log("Searching: ", dir);
130+
// log("Searching: ", dir);
131131

132132
final File[] dlls = dir.listFiles(new FilenameFilter() {
133133
@Override
@@ -138,7 +138,7 @@ public boolean accept(final File dir, final String name) {
138138
if (dlls != null && dlls.length > 0) {
139139
entries.add(dir.getAbsolutePath());
140140
} else {
141-
log("No DLLs in ", dir);
141+
// log("No DLLs in ", dir);
142142
}
143143

144144
final File[] jars = dir.listFiles(new FilenameFilter() {
@@ -152,7 +152,7 @@ public boolean accept(final File dir, final String name) {
152152
entries.add(jar.getAbsolutePath());
153153
}
154154
} else {
155-
log("No JARs in ", dir);
155+
// log("No JARs in ", dir);
156156
}
157157

158158
final File[] dirs = dir.listFiles(new FileFilter() {
@@ -166,7 +166,7 @@ public boolean accept(final File f) {
166166
searchForExtraStuff(d, entries);
167167
}
168168
} else {
169-
log("No dirs in ", dir);
169+
// log("No dirs in ", dir);
170170
}
171171
}
172172
}

src/rprocessing/mode/library/LibraryImporter.java

+227-10
Original file line numberDiff line numberDiff line change
@@ -1,16 +1,22 @@
11
package rprocessing.mode.library;
22

33
import java.io.File;
4+
import java.io.FileFilter;
5+
import java.io.FileReader;
46
import java.io.IOException;
7+
import java.lang.reflect.Field;
58
import java.lang.reflect.InvocationTargetException;
69
import java.lang.reflect.Method;
710
import java.net.MalformedURLException;
811
import java.net.URL;
912
import java.net.URLClassLoader;
13+
import java.util.ArrayList;
1014
import java.util.Arrays;
1115
import java.util.Enumeration;
16+
import java.util.HashMap;
1217
import java.util.List;
13-
import java.util.Set;
18+
import java.util.Map;
19+
import java.util.Properties;
1420
import java.util.zip.ZipEntry;
1521
import java.util.zip.ZipFile;
1622

@@ -20,6 +26,11 @@
2026

2127
import com.google.common.base.Joiner;
2228

29+
import processing.core.PApplet;
30+
import rprocessing.Runner;
31+
import rprocessing.exception.REvalException;
32+
import rprocessing.util.RScriptReader;
33+
2334
public class LibraryImporter {
2435

2536
/*
@@ -31,35 +42,241 @@ public class LibraryImporter {
3142

3243
private static final boolean VERBOSE = Boolean.parseBoolean(System.getenv("VERBOSE_RLANG_MODE"));
3344

45+
private static final String PLATFORM = PApplet.platformNames[PApplet.platform];
46+
private static final String BITS = System.getProperty("os.arch").contains("64") ? "64" : "32";
47+
48+
private static final String IMPORT_TEXT =
49+
RScriptReader.readResourceAsText(Runner.class, "r/import_library.R");
50+
3451
private static void log(final String msg) {
3552
if (VERBOSE) {
3653
System.err.println(LibraryImporter.class.getSimpleName() + ": " + msg);
3754
}
3855
}
3956

40-
public LibraryImporter(final List<File> libdirs, final RenjinScriptEngine renjinScriptEngine) {
57+
public LibraryImporter(final List<File> libdirs, final RenjinScriptEngine renjinScriptEngine)
58+
throws ScriptException {
4159
this.libSearchPath = libdirs;
4260
this.renjinScriptEngine = renjinScriptEngine;
4361
}
4462

45-
public void loadLibraries(final Set<String> libs) throws ScriptException {
46-
for (String lib : libs) {
47-
this.loadLibrary(lib);
48-
}
63+
public void injectIntoScope() throws ScriptException {
64+
this.renjinScriptEngine.put("defaultLibraryImporter", this);
65+
this.renjinScriptEngine.eval(IMPORT_TEXT);
66+
4967
}
5068

51-
private void loadLibrary(final String libName) throws ScriptException {
69+
public void importProcessingLibrary(final String libName) throws ScriptException, REvalException {
70+
log("Import Processing library:" + libName);
71+
File libDir = null;
5272
for (final File searchDir : libSearchPath) {
53-
log(searchDir.getAbsolutePath());
54-
final File handRolledJar =
55-
new File("/home/ist/sketchbook/libraries/peasycam/library/peasycam.jar");
73+
// Permit hand-rolled single-jar libraries:
74+
final File handRolledJar = new File(searchDir.getAbsoluteFile(), libName + ".jar");
5675
if (handRolledJar.exists()) {
5776
log("Found hand-rolled jar lib " + handRolledJar);
5877
addJarToClassLoader(handRolledJar);
5978
importPublicClassesFromJar(handRolledJar);
6079
return;
6180
}
81+
82+
final File potentialDir = new File(searchDir.getAbsoluteFile(), libName);
83+
if (potentialDir.exists()) {
84+
if (libDir == null) {
85+
libDir = potentialDir;
86+
} else {
87+
System.err.println("Multiple libraries could be " + libName + ";");
88+
System.err.println("Picking " + libDir + " over " + potentialDir);
89+
}
90+
}
91+
}
92+
93+
if (libDir == null) {
94+
throw new REvalException("This sketch requires the " + libName + "library.");
95+
}
96+
final File contentsDir = new File(libDir, "library");
97+
if (!contentsDir.exists()) {
98+
throw new REvalException("The library " + libName + " is malformed and won't import.");
99+
}
100+
final File mainJar = new File(contentsDir, libName + ".jar");
101+
102+
final List<File> resources = findResources(contentsDir);
103+
for (final File resource : resources) {
104+
final String name = resource.getName();
105+
if (name.endsWith(".jar") || name.endsWith(".zip")) {
106+
// Contains stuff we want
107+
addJarToClassLoader(resource.getAbsoluteFile());
108+
109+
log("Appending " + resource.getAbsolutePath() + " to sys.path.");
110+
111+
// Are we missing any extensions?
112+
} else if (name.matches("^.*\\.(so|dll|dylib|jnilib)$")) {
113+
// Add *containing directory* to native search path
114+
addDirectoryToNativeSearchPath(resource.getAbsoluteFile().getParentFile());
115+
}
116+
}
117+
118+
importPublicClassesFromJar(mainJar);
119+
}
120+
121+
/**
122+
* Find all of the resources a library requires on this platform. See
123+
* https://github.com/processing/processing/wiki/Library-Basics.
124+
*
125+
* First, finds the library. Second, tries to parse export.txt, and follow its instructions.
126+
* Third, tries to understand folder structure, and export according to that.
127+
*
128+
* @param libName The name of the library to add.
129+
* @return The list of files we need to import.
130+
*/
131+
protected List<File> findResources(final File contentsDir) {
132+
log("Exploring " + contentsDir + " for resources.");
133+
List<File> resources;
134+
resources = findResourcesFromExportTxt(contentsDir);
135+
if (resources == null) {
136+
log("Falling back to directory structure.");
137+
resources = findResourcesFromDirectoryStructure(contentsDir);
138+
}
139+
return resources;
140+
}
141+
142+
private List<File> findResourcesFromExportTxt(final File contentsDir) {
143+
final File exportTxt = new File(contentsDir, "export.txt");
144+
if (!exportTxt.exists()) {
145+
log("No export.txt in " + contentsDir.getAbsolutePath());
146+
return null;
147+
}
148+
final Map<String, String[]> exportTable;
149+
try {
150+
exportTable = parseExportTxt(exportTxt);
151+
} catch (final Exception e) {
152+
log("Couldn't parse export.txt: " + e.getMessage());
153+
return null;
154+
}
155+
156+
final String[] resourceNames;
157+
158+
// Check from most-specific to least-specific:
159+
if (exportTable.containsKey("application." + PLATFORM + BITS)) {
160+
log("Found 'application." + PLATFORM + BITS + "' in export.txt");
161+
resourceNames = exportTable.get("application." + PLATFORM + BITS);
162+
} else if (exportTable.containsKey("application." + PLATFORM)) {
163+
log("Found 'application." + PLATFORM + "' in export.txt");
164+
resourceNames = exportTable.get("application." + PLATFORM);
165+
} else if (exportTable.containsKey("application")) {
166+
log("Found 'application' in export.txt");
167+
resourceNames = exportTable.get("application");
168+
} else {
169+
log("No matching platform in " + exportTxt.getAbsolutePath());
170+
return null;
171+
}
172+
final List<File> resources = new ArrayList<>();
173+
for (final String resourceName : resourceNames) {
174+
final File resource = new File(contentsDir, resourceName);
175+
if (resource.exists()) {
176+
resources.add(resource);
177+
} else {
178+
log(resourceName + " is mentioned in " + exportTxt.getAbsolutePath()
179+
+ "but doesn't actually exist. Moving on.");
180+
continue;
181+
}
182+
}
183+
return resources;
184+
}
185+
186+
private List<File> findResourcesFromDirectoryStructure(final File contentsDir) {
187+
final List<String> childNames = Arrays.asList(contentsDir.list());
188+
final List<File> resources = new ArrayList<File>();
189+
190+
// Find platform-specific stuff
191+
File platformDir = null;
192+
if (childNames.contains(PLATFORM + BITS)) {
193+
final File potentialPlatformDir = new File(contentsDir, PLATFORM + BITS);
194+
if (potentialPlatformDir.isDirectory()) {
195+
platformDir = potentialPlatformDir;
196+
}
197+
}
198+
if (platformDir == null && childNames.contains(PLATFORM)) {
199+
final File potentialPlatformDir = new File(contentsDir, PLATFORM);
200+
if (potentialPlatformDir.isDirectory()) {
201+
platformDir = potentialPlatformDir;
202+
}
203+
}
204+
if (platformDir != null) {
205+
log("Found platform-specific directory " + platformDir.getAbsolutePath());
206+
for (final File resource : platformDir.listFiles()) {
207+
resources.add(resource);
208+
}
209+
}
210+
211+
// Find multi-platform stuff; always do this
212+
final File[] commonResources = contentsDir.listFiles(new FileFilter() {
213+
@Override
214+
public boolean accept(final File file) {
215+
return !file.isDirectory();
216+
}
217+
});
218+
for (final File resource : commonResources) {
219+
resources.add(resource);
220+
}
221+
return resources;
222+
}
223+
224+
/**
225+
* Add the given path to the list of paths searched for DLLs (as in those loaded by loadLibrary).
226+
* A hack, which depends on the presence of a particular field in ClassLoader. Known to work on
227+
* all recent Sun JVMs and OS X.
228+
*
229+
* <p>
230+
* See <a href="http://forums.sun.com/thread.jspa?threadID=707176">this thread</a>.
231+
*/
232+
private void addDirectoryToNativeSearchPath(final File dllDir) {
233+
final String newPath = dllDir.getAbsolutePath();
234+
try {
235+
final Field field = ClassLoader.class.getDeclaredField("usr_paths");
236+
field.setAccessible(true);
237+
final String[] paths = (String[]) field.get(null);
238+
for (final String path : paths) {
239+
if (newPath.equals(path)) {
240+
return;
241+
}
242+
}
243+
final String[] tmp = Arrays.copyOf(paths, paths.length + 1);
244+
tmp[paths.length] = newPath;
245+
field.set(null, tmp);
246+
log("Added " + newPath + " to java.library.path.");
247+
} catch (final Exception e) {
248+
System.err.println(
249+
"While attempting to add " + newPath + " to the processing.py library search path: "
250+
+ e.getClass().getSimpleName() + "--" + e.getMessage());
251+
}
252+
}
253+
254+
/**
255+
* Parse an export.txt file to figure out what we need to load for this platform. This is all
256+
* duplicated from processing.app.Library / processing.app.Base, but we don't have the PDE around
257+
* at runtime so we can't use them.
258+
*
259+
* @param exportTxt The export.txt file; must exist.
260+
*/
261+
private Map<String, String[]> parseExportTxt(final File exportTxt) throws Exception {
262+
log("Parsing " + exportTxt.getAbsolutePath());
263+
264+
final Properties exportProps = new Properties();
265+
try (final FileReader in = new FileReader(exportTxt)) {
266+
exportProps.load(in);
267+
}
268+
269+
final Map<String, String[]> exportTable = new HashMap<>();
270+
271+
for (final String platform : exportProps.stringPropertyNames()) {
272+
final String exportCSV = exportProps.getProperty(platform);
273+
final String[] exports = PApplet.splitTokens(exportCSV, ",");
274+
for (int i = 0; i < exports.length; i++) {
275+
exports[i] = exports[i].trim();
276+
}
277+
exportTable.put(platform, exports);
62278
}
279+
return exportTable;
63280
}
64281

65282
/**

src/rprocessing/r/import_library.R

+3
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
importLibrary <- function(lib) {
2+
defaultLibraryImporter$importProcessingLibrary(lib)
3+
}

0 commit comments

Comments
 (0)