6
6
* %%
7
7
* Redistribution and use in source and binary forms, with or without
8
8
* modification, are permitted provided that the following conditions are met:
9
- *
9
+ *
10
10
* 1. Redistributions of source code must retain the above copyright notice,
11
11
* this list of conditions and the following disclaimer.
12
12
* 2. Redistributions in binary form must reproduce the above copyright notice,
13
13
* this list of conditions and the following disclaimer in the documentation
14
14
* and/or other materials provided with the distribution.
15
- *
15
+ *
16
16
* THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
17
17
* AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
18
18
* IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
29
29
30
30
package org .scijava .plugins .scripting .python ;
31
31
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
+
32
40
import org .scijava .app .AppService ;
33
41
import org .scijava .command .CommandService ;
34
42
import org .scijava .launcher .Config ;
38
46
import org .scijava .plugin .Menu ;
39
47
import org .scijava .plugin .Parameter ;
40
48
import org .scijava .plugin .Plugin ;
49
+ import org .scijava .ui .DialogPrompt ;
50
+ import org .scijava .ui .UIService ;
41
51
import 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 ;
49
53
50
54
/**
51
55
* Options for configuring the Python environment.
52
- *
56
+ *
53
57
* @author Curtis Rueden
54
58
*/
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 ), })
62
63
public class OptionsPython extends OptionsPlugin {
63
64
64
65
@ Parameter
@@ -73,12 +74,28 @@ public class OptionsPython extends OptionsPlugin {
73
74
@ Parameter (label = "Python environment directory" , persist = false )
74
75
private File pythonDir ;
75
76
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" )
77
86
private Button rebuildEnvironment ;
78
87
79
- @ Parameter (label = "Launch in Python mode" , callback = "updatePythonConfig" , persist = false )
88
+ @ Parameter (label = "Launch in Python mode" , callback = "updatePythonConfig" ,
89
+ persist = false )
80
90
private boolean pythonMode ;
81
91
92
+ @ Parameter (required = false )
93
+ private UIService uiService ;
94
+
95
+ private boolean initialPythonMode = false ;
96
+ private String initialCondaDependencies ;
97
+ private String initialPipDependencies ;
98
+
82
99
// -- OptionsPython methods --
83
100
84
101
public File getPythonDir () {
@@ -124,28 +141,100 @@ public void load() {
124
141
}
125
142
126
143
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 );
132
152
pythonDir = pythonPath .toFile ();
133
153
}
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
+ }
134
217
}
135
218
136
219
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 () {
138
229
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" );
141
234
if (pythonEnvFileProp != null ) {
142
- environmentYaml = OptionsPython . stringToFile (appPath , pythonEnvFileProp );
235
+ environmentYaml = stringToFile (appPath , pythonEnvFileProp );
143
236
}
144
-
145
- commandService .run (RebuildEnvironment .class , true ,
146
- "environmentYaml" , environmentYaml ,
147
- "targetDir" , pythonDir
148
- );
237
+ return environmentYaml ;
149
238
}
150
239
151
240
@ Override
@@ -175,6 +264,66 @@ public void save() {
175
264
// Proceed gracefully if config file cannot be written.
176
265
log .debug (exc );
177
266
}
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 ;
178
327
}
179
328
180
329
// -- Utility methods --
@@ -195,8 +344,8 @@ static File stringToFile(Path baseDir, String value) {
195
344
*/
196
345
static String fileToString (Path baseDir , File file ) {
197
346
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 ();
200
349
return relPath .toString ();
201
350
}
202
351
}
0 commit comments