Skip to content

Commit 8c61ad4

Browse files
ctruedenclaude
andcommitted
Support multiple pixi envs w/ Environment.activate
Replaces the builder-level .environment() method (which baked a single pixi environment name into the built Environment) with a runtime activate(name) method on Environment itself. This better reflects the pixi data model: a pixi project directory is a single unit that can house multiple named environments, all sharing one build step. env.activate("foo") eagerly runs `pixi install --environment foo` and returns a new, ready Environment targeting that prefix. The default environment is installed as usual during build(); non-default ones are installed on demand via activate(). The base Environment.activate() throws UnsupportedOperationException so other environment types stay clean. As per apposed/appose-python@59e6d94. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
1 parent 8401825 commit 8c61ad4

4 files changed

Lines changed: 72 additions & 41 deletions

File tree

src/main/java/org/apposed/appose/Environment.java

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -74,6 +74,26 @@ default String type() {
7474
return builder().envType();
7575
}
7676

77+
/**
78+
* Returns a new Environment targeting a named sub-environment.
79+
* <p>
80+
* For pixi projects this installs (if needed) and activates the named
81+
* pixi environment, equivalent to {@code pixi run --environment <name>}.
82+
* The returned Environment is fully ready to launch services.
83+
* </p>
84+
*
85+
* @param name The sub-environment name (e.g. {@code "gpu"}, {@code "shiny"}).
86+
* @return A new Environment configured for the named sub-environment.
87+
* @throws BuildException If something goes wrong during activation.
88+
* @throws UnsupportedOperationException If this environment type does not
89+
* support named sub-environments.
90+
*/
91+
default Environment activate(String name) throws BuildException {
92+
throw new UnsupportedOperationException(
93+
getClass().getSimpleName() + " does not support named sub-environments"
94+
);
95+
}
96+
7797
/**
7898
* Rebuilds this environment from scratch.
7999
* This deletes the existing environment directory and rebuilds it using the

src/main/java/org/apposed/appose/builder/BaseBuilder.java

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -232,13 +232,35 @@ protected Scheme resolveScheme() {
232232
throw new IllegalStateException("Cannot determine scheme: neither scheme nor content is set");
233233
}
234234

235+
/** Functional interface for activating a named sub-environment. */
236+
@FunctionalInterface
237+
public interface Activator {
238+
Environment activate(String name) throws BuildException;
239+
}
240+
235241
protected Environment createEnv(String base, List<String> binPaths, List<String> launchArgs) {
242+
return createEnv(base, binPaths, launchArgs, null);
243+
}
244+
245+
protected Environment createEnv( String base, List<String> binPaths,
246+
List<String> launchArgs, Activator activator)
247+
{
236248
return new Environment() {
237249
@Override public String base() { return base; }
238250
@Override public List<String> binPaths() { return binPaths; }
239251
@Override public List<String> launchArgs() { return launchArgs; }
240252
@Override public Map<String, String> envVars() { return envVars; }
241253
@Override public Builder<?> builder() { return BaseBuilder.this; }
254+
255+
@Override
256+
public Environment activate(String name) throws BuildException {
257+
if (activator == null) {
258+
throw new UnsupportedOperationException(
259+
getClass().getSimpleName() + " does not support named sub-environments"
260+
);
261+
}
262+
return activator.activate(name);
263+
}
242264
};
243265
}
244266

src/main/java/org/apposed/appose/builder/PixiBuilder.java

Lines changed: 24 additions & 35 deletions
Original file line numberDiff line numberDiff line change
@@ -54,24 +54,9 @@ public final class PixiBuilder extends BaseBuilder<PixiBuilder> {
5454

5555
private final List<String> condaPackages = new ArrayList<>();
5656
private final List<String> pypiPackages = new ArrayList<>();
57-
private String pixiEnvironment;
5857

5958
// -- PixiBuilder methods --
6059

61-
/**
62-
* Selects which pixi environment to activate within the manifest.
63-
* Pixi supports multiple named environments in a single {@code pixi.toml};
64-
* use this method to target one other than {@code "default"}.
65-
* Maps to {@code pixi run --environment <name>}.
66-
*
67-
* @param name The pixi environment name (e.g. {@code "cuda"}, {@code "cpu"}).
68-
* @return This builder instance, for fluent-style programming.
69-
*/
70-
public PixiBuilder environment(String name) {
71-
this.pixiEnvironment = name;
72-
return this;
73-
}
74-
7560
/**
7661
* Adds conda packages to the environment.
7762
*
@@ -106,7 +91,6 @@ protected void addStateFields(Map<String, Object> state) {
10691
super.addStateFields(state);
10792
state.put("condaPackages", condaPackages);
10893
state.put("pypiPackages", pypiPackages);
109-
state.put("pixiEnvironment", pixiEnvironment);
11094
}
11195

11296
@Override
@@ -289,22 +273,14 @@ private void runPixiInstall(Pixi pixi, File envDir) throws IOException, Interrup
289273
pixi.setFlags(withFlag(flags, "-vv"));
290274
}
291275

292-
String envName = pixiEnvironment != null ? pixiEnvironment : "default";
293-
monitor = new PixiInstallMonitor(envDir, envName, progressSubscribers,
276+
monitor = new PixiInstallMonitor(envDir, "default", progressSubscribers,
294277
msg -> errorSubscribers.forEach(sub -> sub.accept(msg)));
295278
pixi.setErrorConsumer(monitor::intercept);
296279
}
297280

298281
// Ensure the pixi environment is fully installed.
299-
List<String> installCmd = new ArrayList<>(Arrays.asList(
300-
"install", "--manifest-path", manifestFile.getAbsolutePath()
301-
));
302-
if (pixiEnvironment != null) {
303-
installCmd.add("--environment");
304-
installCmd.add(pixiEnvironment);
305-
}
306282
try {
307-
pixi.exec(installCmd.toArray(new String[0]));
283+
pixi.exec("install", "--manifest-path", manifestFile.getAbsolutePath());
308284
}
309285
finally {
310286
if (monitor != null) {
@@ -322,18 +298,31 @@ private Environment buildPixiEnvironment(Pixi pixi, File envDir) {
322298
if (!manifestFile.exists()) manifestFile = new File(envDir, "pixi.toml");
323299

324300
String base = envDir.getAbsolutePath();
325-
String envName = pixiEnvironment != null ? pixiEnvironment : "default";
326301
List<String> launchArgs = new ArrayList<>(Arrays.asList(
327-
pixi.command, "run", "--manifest-path",
328-
manifestFile.getAbsolutePath()
302+
pixi.command, "run", "--manifest-path", manifestFile.getAbsolutePath()
329303
));
330-
if (pixiEnvironment != null) {
331-
launchArgs.add("--environment");
332-
launchArgs.add(pixiEnvironment);
333-
}
334304
List<String> binPaths = Collections.singletonList(
335-
envDir.toPath().resolve(".pixi").resolve("envs").resolve(envName).resolve("bin").toString()
305+
envDir.toPath().resolve(".pixi").resolve("envs").resolve("default").resolve("bin").toString()
336306
);
337-
return createEnv(base, binPaths, launchArgs);
307+
308+
final File manifestFileFinal = manifestFile;
309+
Activator activator = name -> {
310+
try {
311+
pixi.exec("install", "--manifest-path", manifestFileFinal.getAbsolutePath(),
312+
"--environment", name);
313+
}
314+
catch (IOException | InterruptedException e) {
315+
throw new org.apposed.appose.BuildException(this, e);
316+
}
317+
List<String> activatedLaunchArgs = new ArrayList<>(launchArgs);
318+
activatedLaunchArgs.add("--environment");
319+
activatedLaunchArgs.add(name);
320+
List<String> activatedBinPaths = Collections.singletonList(
321+
envDir.toPath().resolve(".pixi").resolve("envs").resolve(name).resolve("bin").toString()
322+
);
323+
return createEnv(base, activatedBinPaths, activatedLaunchArgs);
324+
};
325+
326+
return createEnv(base, binPaths, launchArgs, activator);
338327
}
339328
}

src/test/java/org/apposed/appose/builder/PixiBuilderTest.java

Lines changed: 6 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -196,25 +196,25 @@ public void testContentPixiToml() throws Exception {
196196
cowsayAndAssert(env, "toml!");
197197
}
198198

199-
/** Tests that {@code .environment()} selects a non-default pixi environment. */
199+
/** Tests that {@code env.activate()} launches a service in a non-default pixi environment. */
200200
@Test
201-
public void testPixiEnvironmentSelection() throws Exception {
201+
public void testPixiActivate() throws Exception {
202202
Environment env = Appose
203203
.pixi("src/test/resources/envs/cowsay-multi-env.toml")
204204
.base("target/envs/pixi-multi-env")
205-
.environment("alt")
206205
.logDebug()
207206
.build();
208207
assertInstanceOf(PixiBuilder.class, env.builder());
208+
Environment altEnv = env.activate("alt");
209209
// Verify launch args include --environment alt.
210-
List<String> launchArgs = env.launchArgs();
210+
List<String> launchArgs = altEnv.launchArgs();
211211
int idx = launchArgs.indexOf("--environment");
212212
assertTrue(idx >= 0, "launchArgs should contain --environment");
213213
assertEquals("alt", launchArgs.get(idx + 1));
214214
// Verify bin path resolves to the alt environment directory.
215-
assertTrue(env.binPaths().get(0).contains(File.separator + "alt" + File.separator),
215+
assertTrue(altEnv.binPaths().get(0).contains(File.separator + "alt" + File.separator),
216216
"binPaths should reference the alt environment");
217-
cowsayAndAssert(env, "multi-env");
217+
cowsayAndAssert(altEnv, "multi-env");
218218
}
219219

220220
/**

0 commit comments

Comments
 (0)