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..c7976f9 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-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; + + @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(pixiImage) + .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..396d710 --- /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-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-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'] + ) + } +}