Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
22 changes: 22 additions & 0 deletions src/main/groovy/io/seqera/wave/util/ContainerHelper.groovy
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,7 @@ import io.seqera.wave.api.PackagesSpec
import io.seqera.wave.api.SubmitContainerTokenRequest
import io.seqera.wave.api.SubmitContainerTokenResponse
import io.seqera.wave.config.CondaOpts
import io.seqera.wave.config.CranOpts
import io.seqera.wave.core.ContainerPlatform
import io.seqera.wave.exception.BadRequestException
import io.seqera.wave.service.builder.BuildFormat
Expand All @@ -41,6 +42,10 @@ import static io.seqera.wave.util.DockerHelper.condaFileToSingularityFile
import static io.seqera.wave.util.DockerHelper.condaPackagesToCondaYaml
import static io.seqera.wave.util.DockerHelper.condaPackagesToDockerFile
import static io.seqera.wave.util.DockerHelper.condaPackagesToSingularityFile
import static io.seqera.wave.util.CranHelper.cranPackagesToDockerFile
import static io.seqera.wave.util.CranHelper.cranPackagesToSingularityFile
import static io.seqera.wave.util.CranHelper.cranFileToDockerFile
import static io.seqera.wave.util.CranHelper.cranFileToSingularityFile
/**
* Container helper methods
*
Expand Down Expand Up @@ -78,6 +83,23 @@ class ContainerHelper {
return result
}

if( spec.type == PackagesSpec.Type.CRAN ) {
if( !spec.cranOpts )
spec.cranOpts = new CranOpts()
def result
if ( spec.entries ) {
final String packages = spec.entries.join(' ')
result = formatSingularity
? cranPackagesToSingularityFile(packages, spec.channels, spec.cranOpts)
: cranPackagesToDockerFile(packages, spec.channels, spec.cranOpts)
} else {
result = formatSingularity
? cranFileToSingularityFile(spec.cranOpts)
: cranFileToDockerFile(spec.cranOpts)
}
return result
}

throw new BadRequestException("Unexpected packages spec type: $spec.type")
}

Expand Down
70 changes: 70 additions & 0 deletions src/test/groovy/io/seqera/wave/util/ContainerHelperTest.groovy
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,7 @@ import io.seqera.wave.api.ImageNameStrategy
import io.seqera.wave.api.PackagesSpec
import io.seqera.wave.api.SubmitContainerTokenRequest
import io.seqera.wave.config.CondaOpts
import io.seqera.wave.config.CranOpts
import io.seqera.wave.exception.BadRequestException
import io.seqera.wave.service.request.ContainerRequest
import io.seqera.wave.service.builder.BuildFormat
Expand Down Expand Up @@ -603,4 +604,73 @@ class ContainerHelperTest extends Specification {

}

def 'should create cran docker file with packages'() {
given:
def REPOSITORIES = ['cran', 'bioconductor']
def CRAN_OPTS = new CranOpts([rImage: 'rocker/r-ver:4.4.1', basePackages: 'littler r-cran-docopt'])
def PACKAGES = ['dplyr', 'ggplot2', 'bioc::GenomicRanges']
def packages = new PackagesSpec(type: PackagesSpec.Type.CRAN, entries: PACKAGES, channels: REPOSITORIES, cranOpts: CRAN_OPTS)

when:
def result = ContainerHelper.containerFileFromPackages(packages, false)

then:
result.contains('FROM rocker/r-ver:4.4.1')
result.contains('install2.r')
result.contains("'dplyr' 'ggplot2' BiocManager::install('GenomicRanges')")
result.contains('R_LIBS_USER="/usr/local/lib/R/site-library"')
}

def 'should create cran singularity file with packages'() {
given:
def REPOSITORIES = ['cran']
def CRAN_OPTS = new CranOpts([rImage: 'rocker/r-ver:4.4.1'])
def PACKAGES = ['tidyverse', 'data.table']
def packages = new PackagesSpec(type: PackagesSpec.Type.CRAN, entries: PACKAGES, channels: REPOSITORIES, cranOpts: CRAN_OPTS)

when:
def result = ContainerHelper.containerFileFromPackages(packages, true)

then:
result.contains('BootStrap: docker')
result.contains('From: rocker/r-ver:4.4.1')
result.contains('install2.r')
result.contains("'tidyverse' 'data.table'")
result.contains('export R_LIBS_USER="/usr/local/lib/R/site-library"')
}

def 'should create cran docker file without packages (file mode)'() {
given:
def CRAN_OPTS = new CranOpts([rImage: 'rocker/r-ver:4.4.1'])
def packages = new PackagesSpec(type: PackagesSpec.Type.CRAN, cranOpts: CRAN_OPTS)

when:
def result = ContainerHelper.containerFileFromPackages(packages, false)

then:
result.contains('FROM rocker/r-ver:4.4.1')
result.contains('COPY --from=wave_context_dir . .')
result.contains('renv.lock')
result.contains('install.R')
result.contains('R_LIBS_USER="/usr/local/lib/R/site-library"')
}

def 'should create cran singularity file without packages (file mode)'() {
given:
def CRAN_OPTS = new CranOpts([rImage: 'rocker/r-ver:4.4.1', basePackages: 'build-essential'])
def packages = new PackagesSpec(type: PackagesSpec.Type.CRAN, cranOpts: CRAN_OPTS)

when:
def result = ContainerHelper.containerFileFromPackages(packages, true)

then:
result.contains('BootStrap: docker')
result.contains('From: rocker/r-ver:4.4.1')
result.contains('%files')
result.contains('/opt/wave_context_dir')
result.contains('build-essential')
result.contains('renv.lock')
result.contains('export R_LIBS_USER="/usr/local/lib/R/site-library"')
}

}
14 changes: 0 additions & 14 deletions typespec/models/CondaPackages.tsp

This file was deleted.

6 changes: 3 additions & 3 deletions typespec/models/ContainerRequest.tsp
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import "./ContainerConfig.tsp";
import "./CondaPackages.tsp";
import "./PackagesSpec.tsp";
import "./ScanMode.tsp";
import "./ScanLevel.tsp";

Expand Down Expand Up @@ -39,8 +39,8 @@ model ContainerRequest {
@doc("The name strategy to be used to create the name of the container built by Wave. Its values can be `none`, `tagPrefix`, or `imageSuffix`. ")
nameStrategy?: "none" | "tagPrefix" | "imageSuffix";
mirror?: boolean;
@doc("Conda packages to be installed in the container.")
packages?: CondaPackages;
@doc("Packages to be installed in the container.")
packages?: PackagesSpec;
scanMode?: ScanMode;
scanLevels?: ScanLevel[];
@doc("Request submission timestamp using ISO-8601.")
Expand Down
14 changes: 14 additions & 0 deletions typespec/models/CranOpts.tsp
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
@doc("Options for CRAN/R environments.")
@example(#{
rImage: "rocker/r-ver:4.4.1",
basePackages: "littler r-cran-docopt",
commands: #["apt-get update", "apt-get install -y libcurl4-openssl-dev"]
})
model CranOpts {
@doc("Name of the R Docker image used to build CRAN containers.")
rImage: string;
@doc("Names of base system packages to install.")
basePackages: string;
@doc("Commands to be included in the container.")
commands: string[];
}
23 changes: 23 additions & 0 deletions typespec/models/PackagesSpec.tsp
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
import "./CondaOpts.tsp";
import "./CranOpts.tsp";

@doc("Specification for packages to be installed in a container.")
@example(#{
type: "CONDA",
entries: #["salmon", "bwa"],
channels: #["conda-forge", "bioconda"]
})
model PackagesSpec {
@doc("The type of package manager: CONDA or CRAN.")
type: "CONDA" | "CRAN";
@doc("The package environment file encoded as a base64 string.")
environment?: string;
@doc("A list of one or more package names.")
entries?: string[];
@doc("Conda build options (when type is CONDA).")
condaOpts?: CondaOpts;
@doc("CRAN build options (when type is CRAN).")
cranOpts?: CranOpts;
@doc("Channels or repositories used for downloading packages.")
channels?: string[];
}
17 changes: 15 additions & 2 deletions wave-api/src/main/java/io/seqera/wave/api/PackagesSpec.java
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@
import java.util.Objects;

import io.seqera.wave.config.CondaOpts;
import io.seqera.wave.config.CranOpts;

/**
* Model a Package environment requirements
Expand All @@ -29,7 +30,7 @@
*/
public class PackagesSpec {

public enum Type { CONDA }
public enum Type { CONDA, CRAN }

public Type type;

Expand All @@ -48,6 +49,11 @@ public enum Type { CONDA }
*/
public CondaOpts condaOpts;

/**
* CRAN build options
*/
public CranOpts cranOpts;

/**
* channels used for downloading packages
*/
Expand All @@ -62,12 +68,13 @@ public boolean equals(Object object) {
&& Objects.equals(environment, that.environment)
&& Objects.equals(entries, that.entries)
&& Objects.equals(condaOpts, that.condaOpts)
&& Objects.equals(cranOpts, that.cranOpts)
&& Objects.equals(channels, that.channels);
}

@Override
public int hashCode() {
return Objects.hash(type, environment, entries, condaOpts, channels);
return Objects.hash(type, environment, entries, condaOpts, cranOpts, channels);
}

@Override
Expand All @@ -77,6 +84,7 @@ public String toString() {
", envFile='" + environment + '\'' +
", packages=" + entries +
", condaOpts=" + condaOpts +
", cranOpts=" + cranOpts +
", channels=" + ObjectUtils.toString(channels) +
'}';
}
Expand Down Expand Up @@ -106,4 +114,9 @@ public PackagesSpec withCondaOpts(CondaOpts opts) {
return this;
}

public PackagesSpec withCranOpts(CranOpts opts) {
this.cranOpts = opts;
return this;
}

}
83 changes: 83 additions & 0 deletions wave-api/src/main/java/io/seqera/wave/config/CranOpts.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,83 @@
/*
* Copyright 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.config;

import java.util.List;
import java.util.Map;
import java.util.Objects;

/**
* CRAN/R build options
*
* @author Paolo Di Tommaso <[email protected]>
*/
public class CranOpts {
final public static String DEFAULT_R_IMAGE = "rocker/r-ver:4.4.1";
final public static String DEFAULT_PACKAGES = "littler r-cran-docopt";

public String rImage;
public List<String> commands;
public String basePackages;

public CranOpts() {
this(Map.of());
}

public CranOpts(Map<String,?> opts) {
this.rImage = opts.containsKey("rImage") ? opts.get("rImage").toString(): DEFAULT_R_IMAGE;
this.commands = opts.containsKey("commands") ? (List<String>)opts.get("commands") : null;
this.basePackages = opts.containsKey("basePackages") ? (String)opts.get("basePackages") : DEFAULT_PACKAGES;
}

public CranOpts withRImage(String value) {
this.rImage = value;
return this;
}

public CranOpts withCommands(List<String> value) {
this.commands = value;
return this;
}

public CranOpts withBasePackages(String value) {
this.basePackages = value;
return this;
}

@Override
public String toString() {
return String.format("CranOpts(rImage=%s; basePackages=%s, commands=%s)",
rImage,
basePackages,
commands != null ? String.join(",", commands) : "null"
);
}

@Override
public boolean equals(Object object) {
if (this == object) return true;
if (object == null || getClass() != object.getClass()) return false;
CranOpts cranOpts = (CranOpts) object;
return Objects.equals(rImage, cranOpts.rImage) && Objects.equals(commands, cranOpts.commands) && Objects.equals(basePackages, cranOpts.basePackages);
}

@Override
public int hashCode() {
return Objects.hash(rImage, commands, basePackages);
}
}
2 changes: 1 addition & 1 deletion wave-utils/src/main/java/io/seqera/wave/util/Base32.java
Original file line number Diff line number Diff line change
Expand Up @@ -32,7 +32,7 @@ public final class Base32 {
-1, 0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, // `abcdefghijklmno
15, 16, 17, 18, 19, 20, 21, 22, 23, 24, 25 // pqrstuvwxyz
};
/* Messsages for Illegal Parameter Exceptions in decode() */
/* Messages for Illegal Parameter Exceptions in decode() */
private static final String errorCanonicalLength = "non canonical Base32 string length";
private static final String errorCanonicalEnd = "non canonical bits at end of Base32 string";
private static final String errorInvalidChar = "invalid character in Base32 string";
Expand Down
Loading