Skip to content

Commit 9797b10

Browse files
pditommasoclaude
andcommitted
Add Pixi-specific CLI options for container builds
Add support for Pixi build customization when building conda-based containers: - --pixi-build-image: Pixi builder image for package installation - --pixi-base-image: Base image for the final container - --pixi-base-packages: Base packages to include - --pixi-run-command: Custom Dockerfile RUN commands These options are passed via PixiOpts when building with conda packages or conda files, allowing users to customize Pixi-based container builds. 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
1 parent 8c85b26 commit 9797b10

3 files changed

Lines changed: 234 additions & 2 deletions

File tree

app/build.gradle

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -19,8 +19,8 @@ repositories {
1919
}
2020

2121
dependencies {
22-
implementation 'io.seqera:wave-api:1.31.0'
23-
implementation 'io.seqera:wave-utils:1.31.0'
22+
implementation 'io.seqera:wave-api:1.31.2'
23+
implementation 'io.seqera:wave-utils:1.31.2'
2424
implementation 'info.picocli:picocli:4.6.1'
2525
implementation 'com.squareup.moshi:moshi:1.15.2'
2626
implementation 'com.squareup.moshi:moshi-adapters:1.15.2'

app/src/main/java/io/seqera/wave/cli/App.java

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -66,6 +66,7 @@
6666
import io.seqera.wave.cli.util.YamlHelper;
6767
import io.seqera.wave.config.CondaOpts;
6868
import io.seqera.wave.config.CranOpts;
69+
import io.seqera.wave.config.PixiOpts;
6970
import io.seqera.wave.util.DockerIgnoreFilter;
7071
import io.seqera.wave.util.Packer;
7172
import org.apache.commons.lang3.StringUtils;
@@ -176,6 +177,18 @@ public class App implements Runnable {
176177
@Option(names = {"--cran-run-command"}, paramLabel = "''", description = "Dockerfile RUN commands used to build the container.")
177178
private List<String> cranRunCommands;
178179

180+
@Option(names = {"--pixi-build-image"}, paramLabel = "''", description = "Pixi builder image used to build the container (default: ${DEFAULT-VALUE}).")
181+
private String pixiBuildImage = PixiOpts.DEFAULT_PIXI_IMAGE;
182+
183+
@Option(names = {"--pixi-base-image"}, paramLabel = "''", description = "Base image for the final Pixi container (default: ${DEFAULT-VALUE}).")
184+
private String pixiBaseImage = PixiOpts.DEFAULT_BASE_IMAGE;
185+
186+
@Option(names = {"--pixi-base-packages"}, paramLabel = "''", description = "Base packages to be installed in the Pixi container (default: ${DEFAULT-VALUE}).")
187+
private String pixiBasePackages = PixiOpts.DEFAULT_PACKAGES;
188+
189+
@Option(names = {"--pixi-run-command"}, paramLabel = "''", description = "Dockerfile RUN commands used to build the container.")
190+
private List<String> pixiRunCommands;
191+
179192
@Option(names = {"--log-level"}, paramLabel = "''", description = "Set the application log level. One of: OFF, ERROR, WARN, INFO, DEBUG, TRACE and ALL")
180193
private String logLevel;
181194

@@ -631,6 +644,15 @@ private CranOpts cranOpts() {
631644
;
632645
}
633646

647+
private PixiOpts pixiOpts() {
648+
return new PixiOpts()
649+
.withPixiImage(pixiBuildImage)
650+
.withBaseImage(pixiBaseImage)
651+
.withBasePackages(pixiBasePackages)
652+
.withCommands(pixiRunCommands)
653+
;
654+
}
655+
634656
protected String containerFileBase64() {
635657
return !isEmpty(containerFile)
636658
? encodePathBase64(containerFile)
@@ -642,6 +664,7 @@ protected PackagesSpec packagesSpec() {
642664
return new PackagesSpec()
643665
.withType(PackagesSpec.Type.CONDA)
644666
.withCondaOpts(condaOpts())
667+
.withPixiOpts(pixiOpts())
645668
.withEnvironment(encodePathBase64(condaFile))
646669
.withChannels(condaChannels())
647670
;
@@ -651,6 +674,7 @@ protected PackagesSpec packagesSpec() {
651674
return new PackagesSpec()
652675
.withType(PackagesSpec.Type.CONDA)
653676
.withCondaOpts(condaOpts())
677+
.withPixiOpts(pixiOpts())
654678
.withEntries(condaPackages)
655679
.withChannels(condaChannels())
656680
;
Lines changed: 208 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,208 @@
1+
/*
2+
* Copyright 2023-2025, Seqera Labs
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* http://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*
16+
*/
17+
18+
package io.seqera.wave.cli
19+
20+
import java.nio.file.Files
21+
22+
import io.seqera.wave.api.PackagesSpec
23+
import io.seqera.wave.config.PixiOpts
24+
import picocli.CommandLine
25+
import spock.lang.Specification
26+
/**
27+
* Test App Pixi prefixed options
28+
*
29+
* @author Paolo Di Tommaso <paolo.ditommaso@gmail.com>
30+
*/
31+
class AppPixiOptsTest extends Specification {
32+
33+
def 'should include pixi opts with conda package' () {
34+
given:
35+
def app = new App()
36+
String[] args = ["--conda-package", "foo"]
37+
38+
when:
39+
new CommandLine(app).parseArgs(args)
40+
and:
41+
def req = app.createRequest()
42+
then:
43+
req.packages.type == PackagesSpec.Type.CONDA
44+
req.packages.entries == ['foo']
45+
and:
46+
req.packages.pixiOpts == new PixiOpts(
47+
pixiImage: PixiOpts.DEFAULT_PIXI_IMAGE,
48+
baseImage: PixiOpts.DEFAULT_BASE_IMAGE,
49+
basePackages: PixiOpts.DEFAULT_PACKAGES
50+
)
51+
}
52+
53+
def 'should include pixi opts with conda file' () {
54+
given:
55+
def CONDA_RECIPE = '''
56+
name: my-recipe
57+
dependencies:
58+
- one=1.0
59+
- two:2.0
60+
'''.stripIndent(true)
61+
and:
62+
def folder = Files.createTempDirectory('test')
63+
def condaFile = folder.resolve('conda.yml');
64+
condaFile.text = CONDA_RECIPE
65+
and:
66+
def app = new App()
67+
String[] args = ["--conda-file", condaFile.toString()]
68+
69+
when:
70+
new CommandLine(app).parseArgs(args)
71+
and:
72+
def req = app.createRequest()
73+
then:
74+
req.packages.type == PackagesSpec.Type.CONDA
75+
and:
76+
req.packages.pixiOpts == new PixiOpts(
77+
pixiImage: PixiOpts.DEFAULT_PIXI_IMAGE,
78+
baseImage: PixiOpts.DEFAULT_BASE_IMAGE,
79+
basePackages: PixiOpts.DEFAULT_PACKAGES
80+
)
81+
82+
cleanup:
83+
folder?.deleteDir()
84+
}
85+
86+
def 'should include custom pixi build image with conda package' () {
87+
given:
88+
def app = new App()
89+
String[] args = [
90+
"--conda-package", "foo",
91+
"--pixi-build-image", "my/pixi:latest"
92+
]
93+
94+
when:
95+
new CommandLine(app).parseArgs(args)
96+
and:
97+
def req = app.createRequest()
98+
then:
99+
req.packages.type == PackagesSpec.Type.CONDA
100+
req.packages.entries == ['foo']
101+
and:
102+
req.packages.pixiOpts == new PixiOpts(
103+
pixiImage: 'my/pixi:latest',
104+
baseImage: PixiOpts.DEFAULT_BASE_IMAGE,
105+
basePackages: PixiOpts.DEFAULT_PACKAGES
106+
)
107+
}
108+
109+
def 'should include custom pixi base image with conda package' () {
110+
given:
111+
def app = new App()
112+
String[] args = [
113+
"--conda-package", "foo",
114+
"--pixi-base-image", "ubuntu:22.04"
115+
]
116+
117+
when:
118+
new CommandLine(app).parseArgs(args)
119+
and:
120+
def req = app.createRequest()
121+
then:
122+
req.packages.type == PackagesSpec.Type.CONDA
123+
req.packages.entries == ['foo']
124+
and:
125+
req.packages.pixiOpts == new PixiOpts(
126+
pixiImage: PixiOpts.DEFAULT_PIXI_IMAGE,
127+
baseImage: 'ubuntu:22.04',
128+
basePackages: PixiOpts.DEFAULT_PACKAGES
129+
)
130+
}
131+
132+
def 'should include custom pixi base packages with conda package' () {
133+
given:
134+
def app = new App()
135+
String[] args = [
136+
"--conda-package", "foo",
137+
"--pixi-base-packages", "conda-forge::curl"
138+
]
139+
140+
when:
141+
new CommandLine(app).parseArgs(args)
142+
and:
143+
def req = app.createRequest()
144+
then:
145+
req.packages.type == PackagesSpec.Type.CONDA
146+
req.packages.entries == ['foo']
147+
and:
148+
req.packages.pixiOpts == new PixiOpts(
149+
pixiImage: PixiOpts.DEFAULT_PIXI_IMAGE,
150+
baseImage: PixiOpts.DEFAULT_BASE_IMAGE,
151+
basePackages: 'conda-forge::curl'
152+
)
153+
}
154+
155+
def 'should include pixi run commands with conda package' () {
156+
given:
157+
def app = new App()
158+
String[] args = [
159+
"--conda-package", "foo",
160+
"--pixi-run-command", "RUN apt-get update",
161+
"--pixi-run-command", "RUN apt-get install -y curl"
162+
]
163+
164+
when:
165+
new CommandLine(app).parseArgs(args)
166+
and:
167+
def req = app.createRequest()
168+
then:
169+
req.packages.type == PackagesSpec.Type.CONDA
170+
req.packages.entries == ['foo']
171+
and:
172+
req.packages.pixiOpts == new PixiOpts(
173+
pixiImage: PixiOpts.DEFAULT_PIXI_IMAGE,
174+
baseImage: PixiOpts.DEFAULT_BASE_IMAGE,
175+
basePackages: PixiOpts.DEFAULT_PACKAGES,
176+
commands: ['RUN apt-get update', 'RUN apt-get install -y curl']
177+
)
178+
}
179+
180+
def 'should include all pixi options' () {
181+
given:
182+
def app = new App()
183+
String[] args = [
184+
"--conda-package", "foo",
185+
"--conda-package", "bar",
186+
"--pixi-build-image", "custom/pixi:v1",
187+
"--pixi-base-image", "debian:12",
188+
"--pixi-base-packages", "conda-forge::wget",
189+
"--pixi-run-command", "RUN one",
190+
"--pixi-run-command", "RUN two"
191+
]
192+
193+
when:
194+
new CommandLine(app).parseArgs(args)
195+
and:
196+
def req = app.createRequest()
197+
then:
198+
req.packages.type == PackagesSpec.Type.CONDA
199+
req.packages.entries == ['foo', 'bar']
200+
and:
201+
req.packages.pixiOpts == new PixiOpts(
202+
pixiImage: 'custom/pixi:v1',
203+
baseImage: 'debian:12',
204+
basePackages: 'conda-forge::wget',
205+
commands: ['RUN one', 'RUN two']
206+
)
207+
}
208+
}

0 commit comments

Comments
 (0)