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