11package rprocessing .mode .library ;
22
33import java .io .File ;
4+ import java .io .FileFilter ;
5+ import java .io .FileReader ;
46import java .io .IOException ;
7+ import java .lang .reflect .Field ;
58import java .lang .reflect .InvocationTargetException ;
69import java .lang .reflect .Method ;
710import java .net .MalformedURLException ;
811import java .net .URL ;
912import java .net .URLClassLoader ;
13+ import java .util .ArrayList ;
1014import java .util .Arrays ;
1115import java .util .Enumeration ;
16+ import java .util .HashMap ;
1217import java .util .List ;
13- import java .util .Set ;
18+ import java .util .Map ;
19+ import java .util .Properties ;
1420import java .util .zip .ZipEntry ;
1521import java .util .zip .ZipFile ;
1622
2026
2127import 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+
2334public 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 /**
0 commit comments