Skip to content
Open
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
15 changes: 15 additions & 0 deletions src/main/groovy/io/seqera/wave/util/ContainerHelper.groovy
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,8 @@ import org.yaml.snakeyaml.Yaml
import static io.seqera.wave.api.BuildTemplate.CONDA_MICROMAMBA_V1
import static io.seqera.wave.api.BuildTemplate.CONDA_MICROMAMBA_V2
import static io.seqera.wave.api.BuildTemplate.CONDA_PIXI_V1
import static io.seqera.wave.api.BuildTemplate.CONDA_PIXI_LOCK_V1
import static io.seqera.wave.api.BuildTemplate.CONDA_PIXI_TOML_V1
import static io.seqera.wave.api.BuildTemplate.CRAN_INSTALLR_V1
import static io.seqera.wave.service.builder.BuildFormat.SINGULARITY
import static DockerHelper.condaEnvironmentToCondaYaml
Expand Down Expand Up @@ -70,6 +72,12 @@ class ContainerHelper {
if( req.buildTemplate == CONDA_PIXI_V1 ) {
return PixiHelper.containerFile(spec, req.containerImage, singularity)
}
if( req.buildTemplate == CONDA_PIXI_LOCK_V1 ) {
return PixiLockHelper.containerFile(spec, req.containerImage, singularity)
}
if( req.buildTemplate == CONDA_PIXI_TOML_V1 ) {
return PixiTomlHelper.containerFile(spec, req.containerImage, singularity)
}
if( spec.type == PackagesSpec.Type.CRAN && (!req.buildTemplate || req.buildTemplate == CRAN_INSTALLR_V1) ) {
return CranHelper.containerFile(spec, singularity)
}
Expand All @@ -84,6 +92,13 @@ class ContainerHelper {
if( req.packages.type != PackagesSpec.Type.CONDA )
return null

if( req.buildTemplate == CONDA_PIXI_LOCK_V1 || req.buildTemplate == CONDA_PIXI_TOML_V1 ) {
if( req.packages.environment ) {
return decodeBase64OrFail(req.packages.environment, 'packages.environment')
}
return null
}

if( req.packages.environment ) {
// parse the attribute as a conda file path *and* append the base packages if any
// note 'channel' is null, because they are expected to be provided in the conda file
Expand Down
59 changes: 59 additions & 0 deletions src/main/groovy/io/seqera/wave/util/PixiLockHelper.groovy
Original file line number Diff line number Diff line change
@@ -0,0 +1,59 @@
/*
* Wave, containers provisioning service
* Copyright (c) 2023-2024, Seqera Labs
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/

package io.seqera.wave.util

import groovy.transform.CompileStatic
import io.seqera.wave.api.PackagesSpec
import io.seqera.wave.config.PixiOpts
import io.seqera.wave.exception.BadRequestException

import static TemplateUtils.pixiLockFileToDockerFile
import static TemplateUtils.pixiLockFileToSingularityFile
import static TemplateUtils.pixiLockUrlToDockerFile
import static TemplateUtils.pixiLockUrlToSingularityFile

/**
* Helper class for Pixi lock file-based container builds (CONDA_PIXI_LOCK_V1 template).
*
* @author Julianus Pfeuffer <8102638+jpfeuffer@users.noreply.github.com>
*/
@CompileStatic
class PixiLockHelper {

static String containerFile(PackagesSpec spec, String containerImage, boolean singularity) {
if( spec.type != PackagesSpec.Type.CONDA ) {
throw new BadRequestException("Package type '${spec.type}' not supported by 'conda/pixi-lock:v1' build template")
}

final lockFileUrl = CondaHelper.tryGetLockFile(spec.entries)
final opts = spec.pixiOpts ?: new PixiOpts()
if( containerImage )
opts.baseImage = containerImage

if( lockFileUrl ) {
return singularity
? pixiLockUrlToSingularityFile(lockFileUrl, opts)
: pixiLockUrlToDockerFile(lockFileUrl, opts)
}

return singularity
? pixiLockFileToSingularityFile(opts)
: pixiLockFileToDockerFile(opts)
}
}
61 changes: 61 additions & 0 deletions src/main/groovy/io/seqera/wave/util/PixiTomlHelper.groovy
Original file line number Diff line number Diff line change
@@ -0,0 +1,61 @@
/*
* Wave, containers provisioning service
* Copyright (c) 2023-2024, Seqera Labs
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/

package io.seqera.wave.util

import groovy.transform.CompileStatic
import io.seqera.wave.api.PackagesSpec
import io.seqera.wave.config.PixiOpts
import io.seqera.wave.exception.BadRequestException

import static TemplateUtils.pixiTomlFileToDockerFile
import static TemplateUtils.pixiTomlFileToSingularityFile
import static TemplateUtils.pixiTomlUrlToDockerFile
import static TemplateUtils.pixiTomlUrlToSingularityFile

/**
* Helper class for Pixi manifest (pixi.toml) file-based container builds (CONDA_PIXI_TOML_V1 template).
* Unlike the lock-file path, this runs {@code pixi install} with online solving, making it
* equivalent to providing a conda.yml but using the Pixi toolchain.
*
* @author Julianus Pfeuffer <8102638+jpfeuffer@users.noreply.github.com>
*/
@CompileStatic
class PixiTomlHelper {

static String containerFile(PackagesSpec spec, String containerImage, boolean singularity) {
if( spec.type != PackagesSpec.Type.CONDA ) {
throw new BadRequestException("Package type '${spec.type}' not supported by 'conda/pixi-toml:v1' build template")
}

final tomlFileUrl = CondaHelper.tryGetLockFile(spec.entries)
final opts = spec.pixiOpts ?: new PixiOpts()
if( containerImage )
opts.baseImage = containerImage

if( tomlFileUrl ) {
return singularity
? pixiTomlUrlToSingularityFile(tomlFileUrl, opts)
: pixiTomlUrlToDockerFile(tomlFileUrl, opts)
}

return singularity
? pixiTomlFileToSingularityFile(opts)
: pixiTomlFileToDockerFile(opts)
}
}
125 changes: 124 additions & 1 deletion src/main/java/io/seqera/wave/util/TemplateUtils.java
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@
import java.io.IOException;
import java.io.InputStream;
import java.net.URL;
import java.util.Base64;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
Expand All @@ -32,7 +33,7 @@
/**
* Helper class to create Dockerfile for Conda package manager
*
* @author Paolo Di Tommaso <paolo.ditommaso@gmail.com>
* @author Julianus Pfeuffer <8102638+jpfeuffer@users.noreply.github.com>
*/
public class TemplateUtils {

Expand Down Expand Up @@ -113,6 +114,22 @@ static public String condaFileToSingularityFileUsingPixi(PixiOpts opts) {
return condaFileTemplate1("/templates/conda-pixi-v1/singularityfile-conda-file.txt", opts);
}

static public String pixiLockUrlToDockerFile(String lockUrl, PixiOpts opts) {
return pixiLockUrlTemplate("/templates/conda-pixi-lock-v1/dockerfile-pixi-lock-url.txt", lockUrl, opts);
}

static public String pixiLockUrlToSingularityFile(String lockUrl, PixiOpts opts) {
return pixiLockUrlTemplate("/templates/conda-pixi-lock-v1/singularityfile-pixi-lock-url.txt", lockUrl, opts);
}

static public String pixiLockFileToDockerFile(PixiOpts opts) {
return pixiLockFileTemplate("/templates/conda-pixi-lock-v1/dockerfile-pixi-lock-file.txt", opts);
}

static public String pixiLockFileToSingularityFile(PixiOpts opts) {
return pixiLockFileTemplate("/templates/conda-pixi-lock-v1/singularityfile-pixi-lock-file.txt", opts);
}

static public String condaPackagesToDockerFileUsingV2(String packages, List<String> condaChannels, CondaOpts opts) {
return condaPackagesTemplate1(
"/templates/conda-micromamba-v2/dockerfile-conda-packages.txt",
Expand Down Expand Up @@ -164,6 +181,96 @@ static protected String condaFileTemplate1(String template, PixiOpts opts) {
return addCommands(result, opts.commands, singularity);
}

static public String pixiTomlFileToDockerFile(PixiOpts opts) {
return pixiTomlFileTemplate("/templates/conda-pixi-toml-v1/dockerfile-pixi-toml-file.txt", opts);
}

static public String pixiTomlUrlToDockerFile(String tomlUrl, PixiOpts opts) {
return pixiTomlUrlTemplate("/templates/conda-pixi-toml-v1/dockerfile-pixi-toml-url.txt", tomlUrl, opts);
}

static public String pixiTomlFileToSingularityFile(PixiOpts opts) {
return pixiTomlFileTemplate("/templates/conda-pixi-toml-v1/singularityfile-pixi-toml-file.txt", opts);
}

static public String pixiTomlUrlToSingularityFile(String tomlUrl, PixiOpts opts) {
return pixiTomlUrlTemplate("/templates/conda-pixi-toml-v1/singularityfile-pixi-toml-url.txt", tomlUrl, opts);
}

static protected String pixiTomlFileTemplate(String template, PixiOpts opts) {
final boolean singularity = template.contains("/singularityfile");
final Map<String,String> binding = new HashMap<>();
binding.put("base_image", opts.baseImage);
binding.put("pixi_image", opts.pixiImage);

final String result = renderTemplate0(template, binding, List.of("wave_context_dir"));
return addCommands(result, opts.commands, singularity);
}

static protected String pixiTomlUrlTemplate(String template, String tomlUrl, PixiOpts opts) {
final boolean singularity = template.contains("/singularityfile");
final Map<String,String> binding = new HashMap<>();
binding.put("base_image", opts.baseImage);
binding.put("pixi_image", opts.pixiImage);
binding.put("toml_url", tomlUrl);

final String result = renderTemplate0(template, binding);
return addCommands(result, opts.commands, singularity);
}

static protected String pixiLockUrlTemplate(String template, String lockUrl, PixiOpts opts) {
final boolean singularity = template.contains("/singularityfile");
final Map<String,String> binding = new HashMap<>();
binding.put("base_image", opts.baseImage);
binding.put("pixi_image", opts.pixiImage);
binding.put("lock_url", lockUrl);
binding.put("manifest_generate", pixiManifestGenerate(opts.manifest, singularity));
binding.put("base_packages", pixiGlobalInstallBasePackage0(opts.basePackages, singularity));

final String result = renderTemplate0(template, binding);
return addCommands(result, opts.commands, singularity);
}

static protected String pixiLockFileTemplate(String template, PixiOpts opts) {
final boolean singularity = template.contains("/singularityfile");
final Map<String,String> binding = new HashMap<>();
binding.put("base_image", opts.baseImage);
binding.put("pixi_image", opts.pixiImage);
binding.put("manifest_generate", pixiManifestGenerate(opts.manifest, singularity));
binding.put("base_packages", pixiGlobalInstallBasePackage0(opts.basePackages, singularity));

final String result = renderTemplate0(template, binding, List.of("wave_context_dir"));
return addCommands(result, opts.commands, singularity);
}

/**
* Generate the shell commands to create pixi.toml from either a provided manifest
* or by extracting metadata from the lock file.
*/
private static String pixiManifestGenerate(String manifest, boolean singularity) {
if (manifest != null && !manifest.isEmpty()) {
// Manifest provided: decode from base64 at build time
final String encoded = Base64.getEncoder().encodeToString(manifest.getBytes());
if (singularity) {
return "printf '%s' '" + encoded + "' | base64 -d > pixi.toml";
} else {
return "printf '%s' '" + encoded + "' | base64 -d > pixi.toml \\";
}
}
// No manifest: generate from lock file by extracting channels, platforms, and deps
final String awkDeps = "awk '/- conda:/{n=split($NF,u,\"/\");f=u[n];sub(/\\.(conda|tar\\.bz2)$/,\"\",f);m=split(f,s,\"-\");v=0;for(i=2;i<=m;i++)if(s[i]~/^[0-9]/&&index(s[i],\".\")>0){v=i;break};if(!v)for(i=2;i<=m;i++)if(s[i]~/^[0-9]/){v=i;break};nm=\"\";for(i=1;i<(v?v:m+1);i++){if(nm)nm=nm\"-\";nm=nm s[i]};if(nm)print nm\" = \\\\\"*\\\\\"\"}' pixi.lock | sort -u > /tmp/deps.toml";
final String awkPlatforms = "PLATFORMS=$(awk '/- conda:/{n=split($NF,u,\"/\");p=u[n-1];if(p!=\"noarch\")print p}' pixi.lock | sort -u | awk '{printf \"\\\\\"%s\\\\\", \", $0}' | sed 's/, $//')";
final String awkChannels = "CHANNELS=$(awk '/^ channels:/{ch=1;next} ch&&/- url:/{gsub(/^ *- url: */,\"\");gsub(/https:\\/\\/conda\\.anaconda\\.org\\//,\"\");gsub(/\\/$/,\"\");printf \"\\\\\"%s\\\\\", \",$0;next} ch&&!/^ -/{exit}' pixi.lock | sed 's/, $//')";
final String printfToml = "printf \"[workspace]\\nname = \\\"wave-env\\\"\\nchannels = [%s]\\nplatforms = [%s]\\n\\n[dependencies]\\n\" \"$CHANNELS\" \"$PLATFORMS\" > pixi.toml";
final String catDeps = "cat /tmp/deps.toml >> pixi.toml";

if (singularity) {
return awkPlatforms + "\n " + awkChannels + "\n " + awkDeps + "\n " + printfToml + "\n " + catDeps;
} else {
return awkPlatforms + " \\\n && " + awkChannels + " \\\n && " + awkDeps + " \\\n && " + printfToml + " \\\n && " + catDeps + " \\";
}
}

static private String renderTemplate0(String templatePath, Map<String,String> binding) {
return renderTemplate0(templatePath, binding, List.of());
}
Expand Down Expand Up @@ -201,6 +308,22 @@ private static String pixiAddBasePackage0(String basePackages, boolean singulari
: "&& " + result + " \\";
}

/**
* Install base packages in an isolated pixi global prefix so that the project
* environment installed via {@code pixi install --frozen} (and its lock file) is
* left untouched. This is used by the pixi lock build templates to add runtime
* tools such as {@code procps-ng} (providing the {@code ps} command) without
* re-solving the locked environment.
*/
private static String pixiGlobalInstallBasePackage0(String basePackages, boolean singularity) {
String result = !StringUtils.isEmpty(basePackages)
? String.format("pixi global install %s", basePackages)
: null;
return result==null || singularity
? result
: "&& " + result + " \\";
}

static private String addCommands(String result, List<String> commands, boolean singularity) {
if( commands==null || commands.isEmpty() )
return result;
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
FROM {{pixi_image}} AS build

COPY conda.yml /opt/wave/pixi.lock
WORKDIR /opt/wave
RUN {{manifest_generate}}
&& pixi install --frozen \
&& mkdir -p /root/.pixi/bin \
{{base_packages}}
&& pixi shell-hook > /shell-hook.sh \
&& echo 'exec "$@"' >> /shell-hook.sh \
&& echo ">> CONDA_LOCK_START" \
&& cat /opt/wave/pixi.lock \
&& echo "<< CONDA_LOCK_END"

FROM {{base_image}} AS final

COPY --from=build /opt/wave/.pixi/envs/default /opt/wave/.pixi/envs/default
COPY --from=build /root/.pixi /root/.pixi
COPY --from=build /shell-hook.sh /shell-hook.sh

USER root
ENV USER=root
ENV PATH=/root/.pixi/bin:$PATH

ENTRYPOINT ["/bin/bash", "/shell-hook.sh"]
CMD ["/bin/bash"]
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
FROM {{pixi_image}} AS build

WORKDIR /opt/wave
RUN curl -fsSL {{lock_url}} -o pixi.lock \
&& {{manifest_generate}}
&& pixi install --frozen \
&& mkdir -p /root/.pixi/bin \
{{base_packages}}
&& pixi shell-hook > /shell-hook.sh \
&& echo 'exec "$@"' >> /shell-hook.sh \
&& echo ">> CONDA_LOCK_START" \
&& cat /opt/wave/pixi.lock \
&& echo "<< CONDA_LOCK_END"

FROM {{base_image}} AS final

COPY --from=build /opt/wave/.pixi/envs/default /opt/wave/.pixi/envs/default
COPY --from=build /root/.pixi /root/.pixi
COPY --from=build /shell-hook.sh /shell-hook.sh

USER root
ENV USER=root
ENV PATH=/root/.pixi/bin:$PATH

ENTRYPOINT ["/bin/bash", "/shell-hook.sh"]
CMD ["/bin/bash"]
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
BootStrap: docker
From: {{pixi_image}}
%files
{{wave_context_dir}}/conda.yml /opt/wave/pixi.lock
%post
cd /opt/wave
{{manifest_generate}}
pixi install --frozen
{{base_packages}}
pixi shell-hook > /shell-hook.sh
echo ">> CONDA_LOCK_START"
cat /opt/wave/pixi.lock
echo "<< CONDA_LOCK_END"
%environment
export PATH=/root/.pixi/bin:$PATH
. /shell-hook.sh
Loading