33import org .python .core .Py ;
44import org .python .core .PyObject ;
55import org .python .core .PyStringMap ;
6+ import org .python .core .PySystemState ;
67import org .python .google .common .base .Joiner ;
78import org .python .util .InteractiveConsole ;
89
10+ import processing .core .PApplet ;
11+
912import java .io .File ;
13+ import java .io .FileFilter ;
14+ import java .io .FileReader ;
1015import java .io .IOException ;
1116import java .lang .reflect .Field ;
1217import java .lang .reflect .InvocationTargetException ;
1318import java .lang .reflect .Method ;
1419import java .net .MalformedURLException ;
1520import java .net .URL ;
1621import java .net .URLClassLoader ;
22+ import java .util .ArrayList ;
1723import java .util .Arrays ;
1824import java .util .Enumeration ;
25+ import java .util .Map ;
26+ import java .util .HashMap ;
1927import java .util .HashSet ;
2028import java .util .List ;
29+ import java .util .Properties ;
2130import java .util .Set ;
2231import java .util .regex .Pattern ;
2332import 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