Skip to content

Commit a2ab8d2

Browse files
committed
Add an options dialog to choose Python environment
1 parent 45cec0f commit a2ab8d2

File tree

3 files changed

+325
-1
lines changed

3 files changed

+325
-1
lines changed

pom.xml

Lines changed: 12 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,7 @@
1010
</parent>
1111

1212
<artifactId>scripting-python</artifactId>
13-
<version>0.3.1-SNAPSHOT</version>
13+
<version>0.4.0-SNAPSHOT</version>
1414

1515
<name>SciJava Scripting: Python</name>
1616
<description>Python scripting language plugin to be used via scyjava.</description>
@@ -87,10 +87,16 @@
8787

8888
<!-- NB: Deploy releases to the SciJava Maven repository. -->
8989
<releaseProfiles>sign,deploy-to-scijava</releaseProfiles>
90+
91+
<appose.version>0.3.0</appose.version>
9092
</properties>
9193

9294
<dependencies>
9395
<!-- SciJava dependencies -->
96+
<dependency>
97+
<groupId>org.scijava</groupId>
98+
<artifactId>app-launcher</artifactId>
99+
</dependency>
94100
<dependency>
95101
<groupId>org.scijava</groupId>
96102
<artifactId>scijava-common</artifactId>
@@ -105,6 +111,11 @@
105111
<groupId>com.fifesoft</groupId>
106112
<artifactId>rsyntaxtextarea</artifactId>
107113
</dependency>
114+
<dependency>
115+
<groupId>org.apposed</groupId>
116+
<artifactId>appose</artifactId>
117+
<version>${appose.version}</version>
118+
</dependency>
108119

109120
<!-- Test dependencies -->
110121
<dependency>
Lines changed: 202 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,202 @@
1+
/*-
2+
* #%L
3+
* Python scripting language plugin to be used via scyjava.
4+
* %%
5+
* Copyright (C) 2021 - 2025 SciJava developers.
6+
* %%
7+
* Redistribution and use in source and binary forms, with or without
8+
* modification, are permitted provided that the following conditions are met:
9+
*
10+
* 1. Redistributions of source code must retain the above copyright notice,
11+
* this list of conditions and the following disclaimer.
12+
* 2. Redistributions in binary form must reproduce the above copyright notice,
13+
* this list of conditions and the following disclaimer in the documentation
14+
* and/or other materials provided with the distribution.
15+
*
16+
* THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
17+
* AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
18+
* IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
19+
* ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDERS OR CONTRIBUTORS BE
20+
* LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
21+
* CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
22+
* SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
23+
* INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
24+
* CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
25+
* ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
26+
* POSSIBILITY OF SUCH DAMAGE.
27+
* #L%
28+
*/
29+
30+
package org.scijava.plugins.scripting.python;
31+
32+
import org.scijava.app.AppService;
33+
import org.scijava.command.CommandService;
34+
import org.scijava.launcher.Config;
35+
import org.scijava.log.LogService;
36+
import org.scijava.menu.MenuConstants;
37+
import org.scijava.options.OptionsPlugin;
38+
import org.scijava.plugin.Menu;
39+
import org.scijava.plugin.Parameter;
40+
import org.scijava.plugin.Plugin;
41+
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;
49+
50+
/**
51+
* Options for configuring the Python environment.
52+
*
53+
* @author Curtis Rueden
54+
*/
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+
})
62+
public class OptionsPython extends OptionsPlugin {
63+
64+
@Parameter
65+
private AppService appService;
66+
67+
@Parameter
68+
private CommandService commandService;
69+
70+
@Parameter
71+
private LogService log;
72+
73+
@Parameter(label = "Python environment directory", persist = false)
74+
private File pythonDir;
75+
76+
@Parameter(label = "Rebuild Python environment", callback = "rebuildEnv")
77+
private Button rebuildEnvironment;
78+
79+
@Parameter(label = "Launch in Python mode", callback = "updatePythonConfig", persist = false)
80+
private boolean pythonMode;
81+
82+
// -- OptionsPython methods --
83+
84+
public File getPythonDir() {
85+
return pythonDir;
86+
}
87+
88+
public boolean isPythonMode() {
89+
return pythonMode;
90+
}
91+
92+
public void setPythonDir(final File pythonDir) {
93+
this.pythonDir = pythonDir;
94+
}
95+
96+
public void setPythonMode(final boolean pythonMode) {
97+
this.pythonMode = pythonMode;
98+
}
99+
100+
// -- Callback methods --
101+
102+
@Override
103+
public void load() {
104+
// Read python-dir and launch-mode from app config file.
105+
String configFileProp = System.getProperty("scijava.app.config-file");
106+
File configFile = configFileProp == null ? null : new File(configFileProp);
107+
if (configFile != null && configFile.canRead()) {
108+
try {
109+
final Map<String, String> config = Config.load(configFile);
110+
111+
final String cfgPythonDir = config.get("python-dir");
112+
if (cfgPythonDir != null) {
113+
final Path appPath = appService.getApp().getBaseDirectory().toPath();
114+
pythonDir = stringToFile(appPath, cfgPythonDir);
115+
}
116+
117+
final String cfgLaunchMode = config.get("launch-mode");
118+
if (cfgLaunchMode != null) pythonMode = cfgLaunchMode.equals("PYTHON");
119+
}
120+
catch (IOException e) {
121+
// Proceed gracefully if config file is not accessible.
122+
log.debug(e);
123+
}
124+
}
125+
126+
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);
132+
pythonDir = pythonPath.toFile();
133+
}
134+
}
135+
136+
public void rebuildEnv() {
137+
// Use scijava.app.python-env-file system property if present.
138+
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");
141+
if (pythonEnvFileProp != null) {
142+
environmentYaml = OptionsPython.stringToFile(appPath, pythonEnvFileProp);
143+
}
144+
145+
commandService.run(RebuildEnvironment.class, true,
146+
"environmentYaml", environmentYaml,
147+
"targetDir", pythonDir
148+
);
149+
}
150+
151+
@Override
152+
public void save() {
153+
// Write python-dir and launch-mode values to app config file.
154+
final String configFileProp = System.getProperty("scijava.app.config-file");
155+
if (configFileProp == null) return; // No config file to update.
156+
final File configFile = new File(configFileProp);
157+
Map<String, String> config = null;
158+
if (configFile.isFile()) {
159+
try {
160+
config = Config.load(configFile);
161+
}
162+
catch (IOException exc) {
163+
// Proceed gracefully if config file is not accessible.
164+
log.debug(exc);
165+
}
166+
}
167+
if (config == null) config = new LinkedHashMap<>();
168+
final Path appPath = appService.getApp().getBaseDirectory().toPath();
169+
config.put("python-dir", fileToString(appPath, pythonDir));
170+
config.put("launch-mode", pythonMode ? "PYTHON" : "JVM");
171+
try {
172+
Config.save(configFile, config);
173+
}
174+
catch (IOException exc) {
175+
// Proceed gracefully if config file cannot be written.
176+
log.debug(exc);
177+
}
178+
}
179+
180+
// -- Utility methods --
181+
182+
/**
183+
* Converts a path string to a file, treating relative path expressions as
184+
* relative to the given base directory, not the current working directory.
185+
*/
186+
static File stringToFile(Path baseDir, String value) {
187+
final Path path = Paths.get(value);
188+
final Path absPath = path.isAbsolute() ? path : baseDir.resolve(path);
189+
return absPath.toFile();
190+
}
191+
192+
/**
193+
* Converts a file to a path string, which in the case of a file beneath the
194+
* given base directory, will be a path expression relative to that base.
195+
*/
196+
static String fileToString(Path baseDir, File file) {
197+
Path filePath = file.toPath();
198+
Path relPath = filePath.startsWith(baseDir) ?
199+
baseDir.relativize(filePath) : filePath.toAbsolutePath();
200+
return relPath.toString();
201+
}
202+
}
Lines changed: 111 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,111 @@
1+
/*-
2+
* #%L
3+
* Python scripting language plugin to be used via scyjava.
4+
* %%
5+
* Copyright (C) 2021 - 2025 SciJava developers.
6+
* %%
7+
* Redistribution and use in source and binary forms, with or without
8+
* modification, are permitted provided that the following conditions are met:
9+
*
10+
* 1. Redistributions of source code must retain the above copyright notice,
11+
* this list of conditions and the following disclaimer.
12+
* 2. Redistributions in binary form must reproduce the above copyright notice,
13+
* this list of conditions and the following disclaimer in the documentation
14+
* and/or other materials provided with the distribution.
15+
*
16+
* THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
17+
* AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
18+
* IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
19+
* ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDERS OR CONTRIBUTORS BE
20+
* LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
21+
* CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
22+
* SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
23+
* INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
24+
* CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
25+
* ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
26+
* POSSIBILITY OF SUCH DAMAGE.
27+
* #L%
28+
*/
29+
30+
package org.scijava.plugins.scripting.python;
31+
32+
import org.apposed.appose.Appose;
33+
import org.apposed.appose.Builder;
34+
import org.scijava.app.AppService;
35+
import org.scijava.command.Command;
36+
import org.scijava.launcher.Splash;
37+
import org.scijava.log.Logger;
38+
import org.scijava.plugin.Parameter;
39+
import org.scijava.plugin.Plugin;
40+
41+
import java.io.File;
42+
import java.io.IOException;
43+
import java.nio.file.Files;
44+
import java.nio.file.Path;
45+
import java.util.Comparator;
46+
import java.util.stream.Stream;
47+
48+
/**
49+
* SciJava command wrapper to build a Python environment.
50+
*
51+
* @author Curtis Rueden
52+
*/
53+
@Plugin(type = Command.class, label = "Rebuild Python environment")
54+
public class RebuildEnvironment implements Command {
55+
56+
@Parameter
57+
private AppService appService;
58+
59+
@Parameter
60+
private Logger log;
61+
62+
@Parameter(label = "environment definition file")
63+
private File environmentYaml;
64+
65+
@Parameter(label = "Target directory")
66+
private File targetDir;
67+
68+
// -- OptionsPython methods --
69+
70+
@Override
71+
public void run() {
72+
final File backupDir = new File(targetDir.getPath() + ".old");
73+
if (backupDir.exists()) {
74+
// Delete the previous backup environment recursively.
75+
try (Stream<Path> x = Files.walk(backupDir.toPath())) {
76+
x.sorted(Comparator.reverseOrder()).forEach(p -> {
77+
try {
78+
Files.delete(p);
79+
}
80+
catch (IOException exc) {
81+
log.error(exc);
82+
}
83+
});
84+
}
85+
catch (IOException exc) {
86+
log.error(exc);
87+
}
88+
}
89+
// Rename the old environment to a backup directory.
90+
if (targetDir.exists()) targetDir.renameTo(backupDir);
91+
// Build the new environment.
92+
try {
93+
Builder builder = Appose
94+
.file(environmentYaml, "environment.yml")
95+
.subscribeOutput(this::report)
96+
.subscribeError(this::report)
97+
.subscribeProgress((msg, cur, max) -> Splash.update(msg, (double) cur / max));
98+
System.err.println("Building Python environment"); // HACK: stderr stream triggers console window show.
99+
Splash.show();
100+
builder.build(targetDir);
101+
}
102+
catch (IOException exc) {
103+
log.error("Failed to build Python environment", exc);
104+
}
105+
}
106+
107+
private void report(String s) {
108+
if (s.isEmpty()) System.err.print(".");
109+
else System.err.print(s);
110+
}
111+
}

0 commit comments

Comments
 (0)