Skip to content

Commit e795c0d

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

File tree

3 files changed

+303
-1
lines changed

3 files changed

+303
-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: 89 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,89 @@
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+
import org.scijava.util.FileUtils;
41+
42+
import java.io.File;
43+
import java.io.IOException;
44+
45+
/**
46+
* SciJava command wrapper to build a Python environment.
47+
*
48+
* @author Curtis Rueden
49+
*/
50+
@Plugin(type = Command.class, label = "Create Python environment")
51+
public class CreateEnvironment implements Command {
52+
53+
@Parameter
54+
private AppService appService;
55+
56+
@Parameter
57+
private Logger log;
58+
59+
@Parameter(label = "environment definition file")
60+
private File environmentYaml;
61+
62+
@Parameter(label = "Target directory")
63+
private File targetDir;
64+
65+
// -- OptionsPython methods --
66+
67+
@Override
68+
public void run() {
69+
FileUtils.deleteRecursively(targetDir);
70+
try {
71+
Builder builder = Appose
72+
.file(environmentYaml, "environment.yml")
73+
.subscribeOutput(this::report)
74+
.subscribeError(this::report)
75+
.subscribeProgress((msg, cur, max) -> Splash.update(msg, (double) cur / max));
76+
System.err.println("Creating Python environment"); // HACK: stderr stream triggers console window show.
77+
Splash.show();
78+
builder.build(targetDir);
79+
}
80+
catch (IOException exc) {
81+
log.error("Failed to create Python environment", exc);
82+
}
83+
}
84+
85+
private void report(String s) {
86+
if (s.isEmpty()) System.err.print(".");
87+
else System.err.print(s);
88+
}
89+
}
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 = "Create Python environment", callback = "createEnv")
77+
private Button createEnvironment;
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 python-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 cfgPythonMode = config.get("python-mode");
118+
if (cfgPythonMode != null) pythonMode = cfgPythonMode.equals("true");
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 createEnv() {
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(CreateEnvironment.class, true,
146+
"environmentYaml", environmentYaml,
147+
"targetDir", pythonDir
148+
);
149+
}
150+
151+
@Override
152+
public void save() {
153+
// Write python-dir and python-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("python-mode", pythonMode ? "true" : "false");
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+
}

0 commit comments

Comments
 (0)