diff --git a/docs/reference/config.md b/docs/reference/config.md index f8e678fa45..bc9e7757d5 100644 --- a/docs/reference/config.md +++ b/docs/reference/config.md @@ -1587,6 +1587,11 @@ The following settings are available: ::: : Forcefully apply compression option to all layers, including already existing layers (default: `false`). +`wave.build.conda.baseImage` +: :::{versionadded} 25.12.0-edge + ::: +: The base image for the final stage in multi-stage Conda container builds (default: `ubuntu:24.04`). This option only applies when using `wave.build.template` set to `conda/micromamba:v2` or `conda/pixi:v1`. + `wave.build.conda.basePackages` : One or more Conda packages to be always added in the resulting container (default: `conda-forge::procps-ng`). @@ -1600,6 +1605,16 @@ The following settings are available: : The container repository where images built by Wave are uploaded. : The corresponding credentials must be provided in your Seqera Platform account. +`wave.build.template` +: :::{versionadded} 25.12.0-edge + ::: +: The build template to use for container builds (default: `conda/micromamba:v1`). Supported values: +: - `conda/micromamba:v1`: Standard Micromamba 1.x single-stage build. Default when unspecified. +: - `conda/micromamba:v2`: Micromamba 2.x with multi-stage builds. +: - `conda/pixi:v1`: Pixi package manager with multi-stage builds for optimized image sizes. +: - `cran/installr:v1`: R/CRAN packages using installr. +: Multi-stage templates produce smaller images by excluding build tools from the final image. + `wave.httpClient.connectTimeout` : :::{versionadded} 22.06.0-edge ::: diff --git a/docs/wave.md b/docs/wave.md index 8b53cfa772..a4b2bd874f 100644 --- a/docs/wave.md +++ b/docs/wave.md @@ -92,6 +92,40 @@ conda.channels = 'conda-forge,bioconda' Packages from the [Python Package Index](https://pypi.org/) can also be added to a Conda `environment.yml` file. See {ref}`Conda and PyPI ` for more information. +(wave-build-templates)= + +### Build templates + +:::{versionadded} 25.12.0-edge +::: + +Wave supports different build templates for creating container images from Conda packages. Build templates control how packages are installed and how the final container image is structured. + +Multi-stage build templates (`conda/pixi:v1` and `conda/micromamba:v2`) offer several advantages: + +- **Smaller images**: Build tools and package managers are excluded from the final image (30-50% size reduction typical). +- **Reproducibility**: Lock files are generated for exact package version tracking. +- **Security**: Fewer binaries in the final image reduces the attack surface. + +To use a specific build template, add the following to your configuration: + +```groovy +wave.enabled = true +wave.strategy = ['conda'] +wave.build.template = 'conda/pixi:v1' +``` + +Available templates: + +| Template | Description | +|----------|-------------| +| `conda/micromamba:v1` | Standard Micromamba 1.x single-stage build. The final image includes the package manager. Default when unspecified. | +| `conda/micromamba:v2` | Multi-stage build using Micromamba 2.x. Produces smaller images without the package manager. | +| `conda/pixi:v1` | Multi-stage build using [Pixi](https://pixi.sh/) package manager. Produces smaller images with faster dependency resolution. | +| `cran/installr:v1` | Build template for R/CRAN packages using installr. | + +When `wave.build.template` is not specified, Wave uses the standard `conda/micromamba:v1` template. + (wave-singularity)= ### Build Singularity native images diff --git a/plugins/nf-wave/build.gradle b/plugins/nf-wave/build.gradle index bf517d02d8..541c38f4e6 100644 --- a/plugins/nf-wave/build.gradle +++ b/plugins/nf-wave/build.gradle @@ -56,8 +56,8 @@ dependencies { api 'org.apache.commons:commons-compress:1.26.1' api 'com.google.code.gson:gson:2.13.1' api 'org.yaml:snakeyaml:2.2' - api 'io.seqera:wave-api:0.16.0' - api 'io.seqera:wave-utils:0.15.1' + api 'io.seqera:wave-api:1.31.1' + api 'io.seqera:wave-utils:1.31.1' testImplementation(testFixtures(project(":nextflow"))) testImplementation "org.apache.groovy:groovy:4.0.29" diff --git a/plugins/nf-wave/src/main/io/seqera/wave/plugin/SubmitContainerTokenRequest.groovy b/plugins/nf-wave/src/main/io/seqera/wave/plugin/SubmitContainerTokenRequest.groovy index c0d66d22fa..409f3a97de 100644 --- a/plugins/nf-wave/src/main/io/seqera/wave/plugin/SubmitContainerTokenRequest.groovy +++ b/plugins/nf-wave/src/main/io/seqera/wave/plugin/SubmitContainerTokenRequest.groovy @@ -154,4 +154,9 @@ class SubmitContainerTokenRequest { */ BuildCompression buildCompression + /** + * The build template to use for container builds + */ + String buildTemplate + } diff --git a/plugins/nf-wave/src/main/io/seqera/wave/plugin/WaveClient.groovy b/plugins/nf-wave/src/main/io/seqera/wave/plugin/WaveClient.groovy index e6f88161b5..e0b7ac5934 100644 --- a/plugins/nf-wave/src/main/io/seqera/wave/plugin/WaveClient.groovy +++ b/plugins/nf-wave/src/main/io/seqera/wave/plugin/WaveClient.groovy @@ -226,7 +226,8 @@ class WaveClient { mirror: config.mirrorMode(), scanMode: config.scanMode(), scanLevels: config.scanAllowedLevels(), - buildCompression: config.buildCompression() + buildCompression: config.buildCompression(), + buildTemplate: config.buildTemplate() ) } @@ -254,7 +255,8 @@ class WaveClient { mirror: config.mirrorMode(), scanMode: config.scanMode(), scanLevels: config.scanAllowedLevels(), - buildCompression: config.buildCompression() + buildCompression: config.buildCompression(), + buildTemplate: config.buildTemplate() ) return sendRequest(request) } diff --git a/plugins/nf-wave/src/main/io/seqera/wave/plugin/config/WaveConfig.groovy b/plugins/nf-wave/src/main/io/seqera/wave/plugin/config/WaveConfig.groovy index 7e070fd2be..786c999ac8 100644 --- a/plugins/nf-wave/src/main/io/seqera/wave/plugin/config/WaveConfig.groovy +++ b/plugins/nf-wave/src/main/io/seqera/wave/plugin/config/WaveConfig.groovy @@ -139,6 +139,8 @@ class WaveConfig implements ConfigScope { String cacheRepository() { build.cacheRepository } + String buildTemplate() { build.template } + Duration buildMaxDuration() { build.maxDuration } BuildCompression buildCompression() { build.compression } @@ -275,6 +277,12 @@ class BuildOpts implements ConfigScope { """) final String cacheRepository + @ConfigOption + @Description(""" + The build template to use for container builds. Supported values: `conda/pixi:v1` (Pixi with multi-stage builds), `conda/micromamba:v2` (Micromamba 2.x with multi-stage builds), `cran/installr:v1` (R/CRAN packages). Default: standard conda/micromamba:v1 template. + """) + final String template + final CondaOpts conda final BuildCompression compression @@ -287,6 +295,7 @@ class BuildOpts implements ConfigScope { BuildOpts(Map opts) { repository = opts.repository cacheRepository = opts.cacheRepository + template = opts.template conda = new CondaOpts(opts.conda as Map ?: Collections.emptyMap()) compression = parseCompression(opts.compression as Map) maxDuration = opts.maxDuration as Duration ?: Duration.of('40m') diff --git a/plugins/nf-wave/src/test/io/seqera/wave/plugin/config/WaveConfigTest.groovy b/plugins/nf-wave/src/test/io/seqera/wave/plugin/config/WaveConfigTest.groovy index 011ad42e04..e6940ac764 100644 --- a/plugins/nf-wave/src/test/io/seqera/wave/plugin/config/WaveConfigTest.groovy +++ b/plugins/nf-wave/src/test/io/seqera/wave/plugin/config/WaveConfigTest.groovy @@ -89,6 +89,7 @@ class WaveConfigTest extends Specification { def opts = new WaveConfig([:]) then: opts.condaOpts().mambaImage == 'mambaorg/micromamba:1.5.10-noble' + opts.condaOpts().baseImage == 'ubuntu:24.04' opts.condaOpts().commands == null when: @@ -96,7 +97,13 @@ class WaveConfigTest extends Specification { then: opts.condaOpts().mambaImage == 'mambaorg/foo:1' opts.condaOpts().commands == ['USER hola'] - + + when: + opts = new WaveConfig([build:[conda:[baseImage:'debian:12', mambaImage:'mambaorg/micromamba:2-amazon2023']]]) + then: + opts.condaOpts().baseImage == 'debian:12' + opts.condaOpts().mambaImage == 'mambaorg/micromamba:2-amazon2023' + } def 'should get build and cache repos' () { @@ -113,6 +120,18 @@ class WaveConfigTest extends Specification { opts.cacheRepository() == 'some/cache' } + def 'should get build template' () { + when: + def opts = new WaveConfig([:]) + then: + opts.buildTemplate() == null + + when: + opts = new WaveConfig([build:[template:'conda/pixi:v1']]) + then: + opts.buildTemplate() == 'conda/pixi:v1' + } + @Unroll def 'should set strategy' () { when: @@ -185,7 +204,7 @@ class WaveConfigTest extends Specification { given: def config = new WaveConfig([enabled: true]) expect: - config.toString() == 'WaveConfig(build:BuildOpts(repository:null, cacheRepository:null, conda:CondaOpts(mambaImage=mambaorg/micromamba:1.5.10-noble; basePackages=conda-forge::procps-ng, commands=null), compression:null, maxDuration:40m), enabled:true, endpoint:https://wave.seqera.io, freeze:false, httpClient:HttpOpts(), mirror:false, retryPolicy:RetryOpts(delay:450ms, maxDelay:1m 30s, maxAttempts:5, jitter:0.25, multiplier:2.0, delayAsDuration:PT0.45S, maxDelayAsDuration:PT1M30S), scan:ScanOpts(allowedLevels:null, mode:null), strategy:[container, dockerfile, conda], bundleProjectResources:null, containerConfigUrl:[], preserveFileTimestamp:null, tokensCacheMaxDuration:30m)' + config.toString() == 'WaveConfig(build:BuildOpts(repository:null, cacheRepository:null, template:null, conda:CondaOpts(mambaImage=mambaorg/micromamba:1.5.10-noble; basePackages=conda-forge::procps-ng, commands=null, baseImage=ubuntu:24.04), compression:null, maxDuration:40m), enabled:true, endpoint:https://wave.seqera.io, freeze:false, httpClient:HttpOpts(), mirror:false, retryPolicy:RetryOpts(delay:450ms, maxDelay:1m 30s, maxAttempts:5, jitter:0.25, multiplier:2.0, delayAsDuration:PT0.45S, maxDelayAsDuration:PT1M30S), scan:ScanOpts(allowedLevels:null, mode:null), strategy:[container, dockerfile, conda], bundleProjectResources:null, containerConfigUrl:[], preserveFileTimestamp:null, tokensCacheMaxDuration:30m)' } def 'should not allow invalid setting' () { diff --git a/validation/wave-tests/example6/nextflow.config b/validation/wave-tests/example6/nextflow.config index c757b3bb7b..b203bbe466 100644 --- a/validation/wave-tests/example6/nextflow.config +++ b/validation/wave-tests/example6/nextflow.config @@ -7,3 +7,7 @@ fusion { enabled = true } +wave { + build.template = 'conda/pixi:v1' +} +