1
1
package rprocessing .mode .library ;
2
2
3
3
import java .io .File ;
4
+ import java .io .FileFilter ;
5
+ import java .io .FileReader ;
4
6
import java .io .IOException ;
7
+ import java .lang .reflect .Field ;
5
8
import java .lang .reflect .InvocationTargetException ;
6
9
import java .lang .reflect .Method ;
7
10
import java .net .MalformedURLException ;
8
11
import java .net .URL ;
9
12
import java .net .URLClassLoader ;
13
+ import java .util .ArrayList ;
10
14
import java .util .Arrays ;
11
15
import java .util .Enumeration ;
16
+ import java .util .HashMap ;
12
17
import java .util .List ;
13
- import java .util .Set ;
18
+ import java .util .Map ;
19
+ import java .util .Properties ;
14
20
import java .util .zip .ZipEntry ;
15
21
import java .util .zip .ZipFile ;
16
22
20
26
21
27
import com .google .common .base .Joiner ;
22
28
29
+ import processing .core .PApplet ;
30
+ import rprocessing .Runner ;
31
+ import rprocessing .exception .REvalException ;
32
+ import rprocessing .util .RScriptReader ;
33
+
23
34
public class LibraryImporter {
24
35
25
36
/*
@@ -31,35 +42,241 @@ public class LibraryImporter {
31
42
32
43
private static final boolean VERBOSE = Boolean .parseBoolean (System .getenv ("VERBOSE_RLANG_MODE" ));
33
44
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
+
34
51
private static void log (final String msg ) {
35
52
if (VERBOSE ) {
36
53
System .err .println (LibraryImporter .class .getSimpleName () + ": " + msg );
37
54
}
38
55
}
39
56
40
- public LibraryImporter (final List <File > libdirs , final RenjinScriptEngine renjinScriptEngine ) {
57
+ public LibraryImporter (final List <File > libdirs , final RenjinScriptEngine renjinScriptEngine )
58
+ throws ScriptException {
41
59
this .libSearchPath = libdirs ;
42
60
this .renjinScriptEngine = renjinScriptEngine ;
43
61
}
44
62
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
+
49
67
}
50
68
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 ;
52
72
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" );
56
75
if (handRolledJar .exists ()) {
57
76
log ("Found hand-rolled jar lib " + handRolledJar );
58
77
addJarToClassLoader (handRolledJar );
59
78
importPublicClassesFromJar (handRolledJar );
60
79
return ;
61
80
}
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 );
62
278
}
279
+ return exportTable ;
63
280
}
64
281
65
282
/**
0 commit comments