66 * %% 
77 * Redistribution and use in source and binary forms, with or without 
88 * modification, are permitted provided that the following conditions are met: 
9-  *   
9+  * 
1010 * 1. Redistributions of source code must retain the above copyright notice, 
1111 *    this list of conditions and the following disclaimer. 
1212 * 2. Redistributions in binary form must reproduce the above copyright notice, 
1313 *    this list of conditions and the following disclaimer in the documentation 
1414 *    and/or other materials provided with the distribution. 
15-  *   
15+  * 
1616 * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" 
1717 * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE 
1818 * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE 
2929
3030package  org .scijava .plugins .scripting .python ;
3131
32+ import  java .io .File ;
33+ import  java .io .IOException ;
34+ import  java .nio .file .Path ;
35+ import  java .nio .file .Paths ;
36+ import  java .util .LinkedHashMap ;
37+ import  java .util .Map ;
38+ import  java .util .StringJoiner ;
39+ 
3240import  org .scijava .app .AppService ;
3341import  org .scijava .command .CommandService ;
3442import  org .scijava .launcher .Config ;
3846import  org .scijava .plugin .Menu ;
3947import  org .scijava .plugin .Parameter ;
4048import  org .scijava .plugin .Plugin ;
49+ import  org .scijava .ui .DialogPrompt ;
50+ import  org .scijava .ui .UIService ;
4151import  org .scijava .widget .Button ;
42- 
43- import  java .io .File ;
44- import  java .io .IOException ;
45- import  java .nio .file .Path ;
46- import  java .nio .file .Paths ;
47- import  java .util .LinkedHashMap ;
48- import  java .util .Map ;
52+ import  org .scijava .widget .TextWidget ;
4953
5054/** 
5155 * Options for configuring the Python environment. 
52-  *   
56+  * 
5357 * @author Curtis Rueden 
5458 */ 
55- @ Plugin (type  = OptionsPlugin .class , menu  = {
56- 	@ Menu (label  = MenuConstants .EDIT_LABEL ,
57- 		weight  = MenuConstants .EDIT_WEIGHT ,
58- 		mnemonic  = MenuConstants .EDIT_MNEMONIC ),
59- 	@ Menu (label  = "Options" , mnemonic  = 'o' ),
60- 	@ Menu (label  = "Python..." , weight  = 10 ),
61- })
59+ @ Plugin (type  = OptionsPlugin .class , menu  = { @ Menu (
60+ 	label  = MenuConstants .EDIT_LABEL , weight  = MenuConstants .EDIT_WEIGHT ,
61+ 	mnemonic  = MenuConstants .EDIT_MNEMONIC ), @ Menu (label  = "Options" ,
62+ 		mnemonic  = 'o' ), @ Menu (label  = "Python..." , weight  = 10 ), })
6263public  class  OptionsPython  extends  OptionsPlugin  {
6364
6465	@ Parameter 
@@ -73,12 +74,28 @@ public class OptionsPython extends OptionsPlugin {
7374	@ Parameter (label  = "Python environment directory" , persist  = false )
7475	private  File  pythonDir ;
7576
76- 	@ Parameter (label  = "Rebuild Python environment" , callback  = "rebuildEnv" )
77+ 	@ Parameter (label  = "Conda dependencies" , style  = TextWidget .AREA_STYLE ,
78+ 		persist  = false )
79+ 	private  String  condaDependencies ;
80+ 
81+ 	@ Parameter (label  = "Pip dependencies" , style  = TextWidget .AREA_STYLE ,
82+ 		persist  = false )
83+ 	private  String  pipDependencies ;
84+ 
85+ 	@ Parameter (label  = "Build Python environment" , callback  = "rebuildEnv" )
7786	private  Button  rebuildEnvironment ;
7887
79- 	@ Parameter (label  = "Launch in Python mode" , callback  = "updatePythonConfig" , persist  = false )
88+ 	@ Parameter (label  = "Launch in Python mode" , callback  = "updatePythonConfig" ,
89+ 		persist  = false )
8090	private  boolean  pythonMode ;
8191
92+ 	@ Parameter (required  = false )
93+ 	private  UIService  uiService ;
94+ 
95+ 	private  boolean  initialPythonMode  = false ;
96+ 	private  String  initialCondaDependencies ;
97+ 	private  String  initialPipDependencies ;
98+ 
8299	// -- OptionsPython methods -- 
83100
84101	public  File  getPythonDir () {
@@ -124,28 +141,100 @@ public void load() {
124141		}
125142
126143		if  (pythonDir  == null ) {
127- 			// For the default Python directory, try to match the platform string used for Java installations. 
128- 			final  String  javaPlatform  = System .getProperty ("scijava.app.java-platform" );
129- 			final  String  platform  = javaPlatform  != null  ? javaPlatform  :
130- 				System .getProperty ("os.name" ) + "-"  + System .getProperty ("os.arch" );
131- 			final  Path  pythonPath  = appService .getApp ().getBaseDirectory ().toPath ().resolve ("python" ).resolve (platform );
144+ 			// For the default Python directory, try to match the platform 
145+ 			// string used for Java installations. 
146+ 			final  String  javaPlatform  = System .getProperty (
147+ 				"scijava.app.java-platform" );
148+ 			final  String  platform  = javaPlatform  != null  ? javaPlatform  : System 
149+ 				.getProperty ("os.name" ) + "-"  + System .getProperty ("os.arch" );
150+ 			final  Path  pythonPath  = appService .getApp ().getBaseDirectory ().toPath ()
151+ 				.resolve ("python" ).resolve (platform );
132152			pythonDir  = pythonPath .toFile ();
133153		}
154+ 
155+ 		// Store the initial value of pythonMode for later comparison 
156+ 		initialPythonMode  = pythonMode ;
157+ 
158+ 		// Populate condaDependencies and pipDependencies from environment.yml 
159+ 		condaDependencies  = "" ;
160+ 		pipDependencies  = "" ;
161+ 		java .util .Set <String > pipBlacklist  = new  java .util .HashSet <>();
162+ 		pipBlacklist .add ("appose-python" );
163+ 		pipBlacklist .add ("pyimagej" );
164+ 		File  envFile  = getEnvironmentYamlFile ();
165+ 		if  (envFile .exists ()) {
166+ 			try  {
167+ 				java .util .List <String > lines  = java .nio .file .Files .readAllLines (envFile 
168+ 					.toPath ());
169+ 				boolean  inDeps  = false , inPip  = false ;
170+ 				StringJoiner  condaDeps  = new  StringJoiner ("\n " );
171+ 				StringJoiner  pipDeps  = new  StringJoiner ("\n " );
172+ 				for  (String  line  : lines ) {
173+ 					String  trimmed  = line .trim ();
174+ 					if  (trimmed .startsWith ("#" ) || trimmed .isEmpty ()) {
175+ 						// Ignore empty and comment lines 
176+ 						continue ;
177+ 					}
178+ 					if  (trimmed .startsWith ("dependencies:" )) {
179+ 						inDeps  = true ;
180+ 						continue ;
181+ 					}
182+ 					if  (inDeps  && trimmed .startsWith ("- pip" )) {
183+ 						inPip  = true ;
184+ 						continue ;
185+ 					}
186+ 					if  (inDeps  && trimmed .startsWith ("- " ) && !inPip ) {
187+ 						String  dep  = trimmed .substring (2 ).trim ();
188+ 						if  (!dep .equals ("pip" )) condaDeps .add (dep );
189+ 						continue ;
190+ 					}
191+ 					if  (inPip  && trimmed .startsWith ("- " )) {
192+ 						String  pipDep  = trimmed .substring (2 ).trim ();
193+ 						boolean  blacklisted  = false ;
194+ 						for  (String  bad  : pipBlacklist ) {
195+ 							if  (pipDep .contains (bad )) {
196+ 								blacklisted  = true ;
197+ 								break ;
198+ 							}
199+ 						}
200+ 						if  (!blacklisted ) pipDeps .add (pipDep );
201+ 						continue ;
202+ 					}
203+ 					if  (inDeps  && !trimmed .startsWith ("- " ) && !trimmed .isEmpty ())
204+ 						inDeps  = false ;
205+ 					if  (inPip  && (!trimmed .startsWith ("- " ) || trimmed .isEmpty ())) inPip  =
206+ 						false ;
207+ 				}
208+ 				condaDependencies  = condaDeps .toString ().trim ();
209+ 				pipDependencies  = pipDeps .toString ().trim ();
210+ 				initialCondaDependencies  = condaDependencies ;
211+ 				initialPipDependencies  = pipDependencies ;
212+ 			}
213+ 			catch  (Exception  e ) {
214+ 				log .debug ("Could not read environment.yml: "  + e .getMessage ());
215+ 			}
216+ 		}
134217	}
135218
136219	public  void  rebuildEnv () {
137- 		// Use scijava.app.python-env-file system property if present. 
220+ 		File  environmentYaml  = writeEnvironmentYaml ();
221+ 		commandService .run (RebuildEnvironment .class , true , "environmentYaml" ,
222+ 			environmentYaml , "targetDir" , pythonDir );
223+ 	}
224+ 
225+ 	/** 
226+ 	 * Returns the File for the environment.yml, using the system property if set. 
227+ 	 */ 
228+ 	private  File  getEnvironmentYamlFile () {
138229		final  Path  appPath  = appService .getApp ().getBaseDirectory ().toPath ();
139- 		File  environmentYaml  = appPath .resolve ("config" ).resolve ("environment.yml" ).toFile ();
140- 		final  String  pythonEnvFileProp  = System .getProperty ("scijava.app.python-env-file" );
230+ 		File  environmentYaml  = appPath .resolve ("config" ).resolve ("environment.yml" )
231+ 			.toFile ();
232+ 		final  String  pythonEnvFileProp  = System .getProperty (
233+ 			"scijava.app.python-env-file" );
141234		if  (pythonEnvFileProp  != null ) {
142- 			environmentYaml  = OptionsPython . stringToFile (appPath , pythonEnvFileProp );
235+ 			environmentYaml  = stringToFile (appPath , pythonEnvFileProp );
143236		}
144- 
145- 		commandService .run (RebuildEnvironment .class , true ,
146- 			"environmentYaml" , environmentYaml ,
147- 			"targetDir" , pythonDir 
148- 		);
237+ 		return  environmentYaml ;
149238	}
150239
151240	@ Override 
@@ -175,6 +264,66 @@ public void save() {
175264			// Proceed gracefully if config file cannot be written. 
176265			log .debug (exc );
177266		}
267+ 
268+ 		if  (pythonMode  && (pythonDir  == null  || !pythonDir .exists ())) {
269+ 			rebuildEnv ();
270+ 		}
271+ 		else  {
272+ 			writeEnvironmentYaml ();
273+ 		}
274+ 		// Warn the user if pythonMode was just enabled and wasn't before 
275+ 		if  (!initialPythonMode  && pythonMode  && uiService  != null ) {
276+ 			String  msg  =
277+ 				"You have just enabled Python mode. Please restart for these changes to take effect! (after your python environment initializes, if needed)\n \n "  +
278+ 					"If Fiji fails to start, try deleting your configuration file and restarting.\n \n Configuration file: "  +
279+ 					configFile ;
280+ 			uiService .showDialog (msg , "Python Mode Enabled" ,
281+ 				DialogPrompt .MessageType .WARNING_MESSAGE );
282+ 		}
283+ 	}
284+ 
285+ 	private  File  writeEnvironmentYaml () {
286+ 		File  envFile  = getEnvironmentYamlFile ();
287+ 
288+ 		// skip writing if nothing has changed 
289+ 		if  (initialCondaDependencies .equals (condaDependencies ) &&
290+ 			initialPipDependencies .equals (pipDependencies )) return  envFile ;
291+ 
292+ 		// Update initial dependencies to detect future changes 
293+ 		initialCondaDependencies  = condaDependencies ;
294+ 		initialPipDependencies  = pipDependencies ;
295+ 
296+ 		// Write environment.yml from condaDependencies and pipDependencies 
297+ 		try  {
298+ 			String  name  = "fiji" ;
299+ 			String [] channels  = { "conda-forge"  };
300+ 			String  pyimagej  = "pyimagej>=1.7.0" ;
301+ 			String  apposePython  =
302+ 				"git+https://github.com/apposed/appose-python.git@efe6dadb2242ca45820fcbb7aeea2096f99f9cb2" ;
303+ 			StringBuilder  yml  = new  StringBuilder ();
304+ 			yml .append ("name: " ).append (name ).append ("\n channels:\n " );
305+ 			for  (String  ch  : channels )
306+ 				yml .append ("  - " ).append (ch ).append ("\n " );
307+ 			yml .append ("dependencies:\n " );
308+ 			for  (String  dep  : condaDependencies .split ("\n " )) {
309+ 				String  trimmed  = dep .trim ();
310+ 				if  (!trimmed .isEmpty ()) yml .append ("  - " ).append (trimmed ).append ("\n " );
311+ 			}
312+ 			yml .append ("  - pip\n " );
313+ 			yml .append ("  - pip:\n " );
314+ 			for  (String  dep  : pipDependencies .split ("\n " )) {
315+ 				String  trimmed  = dep .trim ();
316+ 				if  (!trimmed .isEmpty ()) yml .append ("    - " ).append (trimmed ).append (
317+ 					"\n " );
318+ 			}
319+ 			yml .append ("    - " ).append (pyimagej ).append ("\n " );
320+ 			yml .append ("    - " ).append (apposePython ).append ("\n " );
321+ 			java .nio .file .Files .write (envFile .toPath (), yml .toString ().getBytes ());
322+ 		}
323+ 		catch  (Exception  e ) {
324+ 			log .debug ("Could not write environment.yml: "  + e .getMessage ());
325+ 		}
326+ 		return  envFile ;
178327	}
179328
180329	// -- Utility methods -- 
@@ -195,8 +344,8 @@ static File stringToFile(Path baseDir, String value) {
195344	 */ 
196345	static  String  fileToString (Path  baseDir , File  file ) {
197346		Path  filePath  = file .toPath ();
198- 		Path  relPath  = filePath .startsWith (baseDir ) ?
199- 			baseDir . relativize ( filePath )  : filePath .toAbsolutePath ();
347+ 		Path  relPath  = filePath .startsWith (baseDir ) ?  baseDir . relativize ( filePath ) 
348+ 			: filePath .toAbsolutePath ();
200349		return  relPath .toString ();
201350	}
202351}
0 commit comments