From 9797b10cf278f50c2a4e943231ecaa1d25a98f3f Mon Sep 17 00:00:00 2001 From: Paolo Di Tommaso Date: Wed, 17 Dec 2025 19:55:50 +0100 Subject: [PATCH 1/2] Add Pixi-specific CLI options for container builds MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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 --- app/build.gradle | 4 +- app/src/main/java/io/seqera/wave/cli/App.java | 24 ++ .../io/seqera/wave/cli/AppPixiOptsTest.groovy | 208 ++++++++++++++++++ 3 files changed, 234 insertions(+), 2 deletions(-) create mode 100644 app/src/test/groovy/io/seqera/wave/cli/AppPixiOptsTest.groovy diff --git a/app/build.gradle b/app/build.gradle index e63c7bc..a9c17f9 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -19,8 +19,8 @@ repositories { } dependencies { - implementation 'io.seqera:wave-api:1.31.0' - implementation 'io.seqera:wave-utils:1.31.0' + implementation 'io.seqera:wave-api:1.31.2' + implementation 'io.seqera:wave-utils:1.31.2' implementation 'info.picocli:picocli:4.6.1' implementation 'com.squareup.moshi:moshi:1.15.2' implementation 'com.squareup.moshi:moshi-adapters:1.15.2' diff --git a/app/src/main/java/io/seqera/wave/cli/App.java b/app/src/main/java/io/seqera/wave/cli/App.java index 7c8e0d6..2e1041f 100644 --- a/app/src/main/java/io/seqera/wave/cli/App.java +++ b/app/src/main/java/io/seqera/wave/cli/App.java @@ -66,6 +66,7 @@ import io.seqera.wave.cli.util.YamlHelper; import io.seqera.wave.config.CondaOpts; import io.seqera.wave.config.CranOpts; +import io.seqera.wave.config.PixiOpts; import io.seqera.wave.util.DockerIgnoreFilter; import io.seqera.wave.util.Packer; import org.apache.commons.lang3.StringUtils; @@ -176,6 +177,18 @@ public class App implements Runnable { @Option(names = {"--cran-run-command"}, paramLabel = "''", description = "Dockerfile RUN commands used to build the container.") private List cranRunCommands; + @Option(names = {"--pixi-build-image"}, paramLabel = "''", description = "Pixi builder image used to build the container (default: ${DEFAULT-VALUE}).") + private String pixiBuildImage = PixiOpts.DEFAULT_PIXI_IMAGE; + + @Option(names = {"--pixi-base-image"}, paramLabel = "''", description = "Base image for the final Pixi container (default: ${DEFAULT-VALUE}).") + private String pixiBaseImage = PixiOpts.DEFAULT_BASE_IMAGE; + + @Option(names = {"--pixi-base-packages"}, paramLabel = "''", description = "Base packages to be installed in the Pixi container (default: ${DEFAULT-VALUE}).") + private String pixiBasePackages = PixiOpts.DEFAULT_PACKAGES; + + @Option(names = {"--pixi-run-command"}, paramLabel = "''", description = "Dockerfile RUN commands used to build the container.") + private List pixiRunCommands; + @Option(names = {"--log-level"}, paramLabel = "''", description = "Set the application log level. One of: OFF, ERROR, WARN, INFO, DEBUG, TRACE and ALL") private String logLevel; @@ -631,6 +644,15 @@ private CranOpts cranOpts() { ; } + private PixiOpts pixiOpts() { + return new PixiOpts() + .withPixiImage(pixiBuildImage) + .withBaseImage(pixiBaseImage) + .withBasePackages(pixiBasePackages) + .withCommands(pixiRunCommands) + ; + } + protected String containerFileBase64() { return !isEmpty(containerFile) ? encodePathBase64(containerFile) @@ -642,6 +664,7 @@ protected PackagesSpec packagesSpec() { return new PackagesSpec() .withType(PackagesSpec.Type.CONDA) .withCondaOpts(condaOpts()) + .withPixiOpts(pixiOpts()) .withEnvironment(encodePathBase64(condaFile)) .withChannels(condaChannels()) ; @@ -651,6 +674,7 @@ protected PackagesSpec packagesSpec() { return new PackagesSpec() .withType(PackagesSpec.Type.CONDA) .withCondaOpts(condaOpts()) + .withPixiOpts(pixiOpts()) .withEntries(condaPackages) .withChannels(condaChannels()) ; diff --git a/app/src/test/groovy/io/seqera/wave/cli/AppPixiOptsTest.groovy b/app/src/test/groovy/io/seqera/wave/cli/AppPixiOptsTest.groovy new file mode 100644 index 0000000..572e4a1 --- /dev/null +++ b/app/src/test/groovy/io/seqera/wave/cli/AppPixiOptsTest.groovy @@ -0,0 +1,208 @@ +/* + * Copyright 2023-2025, Seqera Labs + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + */ + +package io.seqera.wave.cli + +import java.nio.file.Files + +import io.seqera.wave.api.PackagesSpec +import io.seqera.wave.config.PixiOpts +import picocli.CommandLine +import spock.lang.Specification +/** + * Test App Pixi prefixed options + * + * @author Paolo Di Tommaso + */ +class AppPixiOptsTest extends Specification { + + def 'should include pixi opts with conda package' () { + given: + def app = new App() + String[] args = ["--conda-package", "foo"] + + when: + new CommandLine(app).parseArgs(args) + and: + def req = app.createRequest() + then: + req.packages.type == PackagesSpec.Type.CONDA + req.packages.entries == ['foo'] + and: + req.packages.pixiOpts == new PixiOpts( + pixiImage: PixiOpts.DEFAULT_PIXI_IMAGE, + baseImage: PixiOpts.DEFAULT_BASE_IMAGE, + basePackages: PixiOpts.DEFAULT_PACKAGES + ) + } + + def 'should include pixi opts with conda file' () { + given: + def CONDA_RECIPE = ''' + name: my-recipe + dependencies: + - one=1.0 + - two:2.0 + '''.stripIndent(true) + and: + def folder = Files.createTempDirectory('test') + def condaFile = folder.resolve('conda.yml'); + condaFile.text = CONDA_RECIPE + and: + def app = new App() + String[] args = ["--conda-file", condaFile.toString()] + + when: + new CommandLine(app).parseArgs(args) + and: + def req = app.createRequest() + then: + req.packages.type == PackagesSpec.Type.CONDA + and: + req.packages.pixiOpts == new PixiOpts( + pixiImage: PixiOpts.DEFAULT_PIXI_IMAGE, + baseImage: PixiOpts.DEFAULT_BASE_IMAGE, + basePackages: PixiOpts.DEFAULT_PACKAGES + ) + + cleanup: + folder?.deleteDir() + } + + def 'should include custom pixi build image with conda package' () { + given: + def app = new App() + String[] args = [ + "--conda-package", "foo", + "--pixi-build-image", "my/pixi:latest" + ] + + when: + new CommandLine(app).parseArgs(args) + and: + def req = app.createRequest() + then: + req.packages.type == PackagesSpec.Type.CONDA + req.packages.entries == ['foo'] + and: + req.packages.pixiOpts == new PixiOpts( + pixiImage: 'my/pixi:latest', + baseImage: PixiOpts.DEFAULT_BASE_IMAGE, + basePackages: PixiOpts.DEFAULT_PACKAGES + ) + } + + def 'should include custom pixi base image with conda package' () { + given: + def app = new App() + String[] args = [ + "--conda-package", "foo", + "--pixi-base-image", "ubuntu:22.04" + ] + + when: + new CommandLine(app).parseArgs(args) + and: + def req = app.createRequest() + then: + req.packages.type == PackagesSpec.Type.CONDA + req.packages.entries == ['foo'] + and: + req.packages.pixiOpts == new PixiOpts( + pixiImage: PixiOpts.DEFAULT_PIXI_IMAGE, + baseImage: 'ubuntu:22.04', + basePackages: PixiOpts.DEFAULT_PACKAGES + ) + } + + def 'should include custom pixi base packages with conda package' () { + given: + def app = new App() + String[] args = [ + "--conda-package", "foo", + "--pixi-base-packages", "conda-forge::curl" + ] + + when: + new CommandLine(app).parseArgs(args) + and: + def req = app.createRequest() + then: + req.packages.type == PackagesSpec.Type.CONDA + req.packages.entries == ['foo'] + and: + req.packages.pixiOpts == new PixiOpts( + pixiImage: PixiOpts.DEFAULT_PIXI_IMAGE, + baseImage: PixiOpts.DEFAULT_BASE_IMAGE, + basePackages: 'conda-forge::curl' + ) + } + + def 'should include pixi run commands with conda package' () { + given: + def app = new App() + String[] args = [ + "--conda-package", "foo", + "--pixi-run-command", "RUN apt-get update", + "--pixi-run-command", "RUN apt-get install -y curl" + ] + + when: + new CommandLine(app).parseArgs(args) + and: + def req = app.createRequest() + then: + req.packages.type == PackagesSpec.Type.CONDA + req.packages.entries == ['foo'] + and: + req.packages.pixiOpts == new PixiOpts( + pixiImage: PixiOpts.DEFAULT_PIXI_IMAGE, + baseImage: PixiOpts.DEFAULT_BASE_IMAGE, + basePackages: PixiOpts.DEFAULT_PACKAGES, + commands: ['RUN apt-get update', 'RUN apt-get install -y curl'] + ) + } + + def 'should include all pixi options' () { + given: + def app = new App() + String[] args = [ + "--conda-package", "foo", + "--conda-package", "bar", + "--pixi-build-image", "custom/pixi:v1", + "--pixi-base-image", "debian:12", + "--pixi-base-packages", "conda-forge::wget", + "--pixi-run-command", "RUN one", + "--pixi-run-command", "RUN two" + ] + + when: + new CommandLine(app).parseArgs(args) + and: + def req = app.createRequest() + then: + req.packages.type == PackagesSpec.Type.CONDA + req.packages.entries == ['foo', 'bar'] + and: + req.packages.pixiOpts == new PixiOpts( + pixiImage: 'custom/pixi:v1', + baseImage: 'debian:12', + basePackages: 'conda-forge::wget', + commands: ['RUN one', 'RUN two'] + ) + } +} \ No newline at end of file From 75ef0ac514d87defe081d8437618d11b09707dbb Mon Sep 17 00:00:00 2001 From: Paolo Di Tommaso Date: Wed, 17 Dec 2025 20:11:34 +0100 Subject: [PATCH 2/2] Minor changes Signed-off-by: Paolo Di Tommaso --- app/src/main/java/io/seqera/wave/cli/App.java | 6 +++--- .../test/groovy/io/seqera/wave/cli/AppPixiOptsTest.groovy | 6 +++--- 2 files changed, 6 insertions(+), 6 deletions(-) diff --git a/app/src/main/java/io/seqera/wave/cli/App.java b/app/src/main/java/io/seqera/wave/cli/App.java index 2e1041f..c7976f9 100644 --- a/app/src/main/java/io/seqera/wave/cli/App.java +++ b/app/src/main/java/io/seqera/wave/cli/App.java @@ -177,8 +177,8 @@ public class App implements Runnable { @Option(names = {"--cran-run-command"}, paramLabel = "''", description = "Dockerfile RUN commands used to build the container.") private List cranRunCommands; - @Option(names = {"--pixi-build-image"}, paramLabel = "''", description = "Pixi builder image used to build the container (default: ${DEFAULT-VALUE}).") - private String pixiBuildImage = PixiOpts.DEFAULT_PIXI_IMAGE; + @Option(names = {"--pixi-image"}, paramLabel = "''", description = "Pixi builder image used to build the container (default: ${DEFAULT-VALUE}).") + private String pixiImage = PixiOpts.DEFAULT_PIXI_IMAGE; @Option(names = {"--pixi-base-image"}, paramLabel = "''", description = "Base image for the final Pixi container (default: ${DEFAULT-VALUE}).") private String pixiBaseImage = PixiOpts.DEFAULT_BASE_IMAGE; @@ -646,7 +646,7 @@ private CranOpts cranOpts() { private PixiOpts pixiOpts() { return new PixiOpts() - .withPixiImage(pixiBuildImage) + .withPixiImage(pixiImage) .withBaseImage(pixiBaseImage) .withBasePackages(pixiBasePackages) .withCommands(pixiRunCommands) diff --git a/app/src/test/groovy/io/seqera/wave/cli/AppPixiOptsTest.groovy b/app/src/test/groovy/io/seqera/wave/cli/AppPixiOptsTest.groovy index 572e4a1..396d710 100644 --- a/app/src/test/groovy/io/seqera/wave/cli/AppPixiOptsTest.groovy +++ b/app/src/test/groovy/io/seqera/wave/cli/AppPixiOptsTest.groovy @@ -88,7 +88,7 @@ class AppPixiOptsTest extends Specification { def app = new App() String[] args = [ "--conda-package", "foo", - "--pixi-build-image", "my/pixi:latest" + "--pixi-image", "my/pixi:latest" ] when: @@ -183,7 +183,7 @@ class AppPixiOptsTest extends Specification { String[] args = [ "--conda-package", "foo", "--conda-package", "bar", - "--pixi-build-image", "custom/pixi:v1", + "--pixi-image", "custom/pixi:v1", "--pixi-base-image", "debian:12", "--pixi-base-packages", "conda-forge::wget", "--pixi-run-command", "RUN one", @@ -205,4 +205,4 @@ class AppPixiOptsTest extends Specification { commands: ['RUN one', 'RUN two'] ) } -} \ No newline at end of file +}