Skip to content

Commit 413d730

Browse files
committed
Initial pixi support
Signed-off-by: Paolo Di Tommaso <paolo.ditommaso@gmail.com>
1 parent 9637ab1 commit 413d730

File tree

4 files changed

+124
-4
lines changed

4 files changed

+124
-4
lines changed

settings.gradle

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -10,4 +10,4 @@ rootProject.name="wave"
1010
// only for development
1111
// clone https://github.com/seqeralabs/libseqera
1212
// in a sibling directory and include the build below
13-
//includeBuild('../libseqera')
13+
includeBuild('../libseqera')

src/main/groovy/io/seqera/wave/controller/ContainerController.groovy

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -85,7 +85,7 @@ import static io.seqera.wave.service.builder.BuildFormat.SINGULARITY
8585
import static io.seqera.wave.service.pairing.PairingService.TOWER_SERVICE
8686
import static io.seqera.wave.util.ContainerHelper.checkContainerSpec
8787
import static io.seqera.wave.util.ContainerHelper.condaFileFromRequest
88-
import static io.seqera.wave.util.ContainerHelper.containerFileFromPackages
88+
import static io.seqera.wave.util.ContainerHelper.containerFileFromRequest
8989
import static io.seqera.wave.util.ContainerHelper.decodeBase64OrFail
9090
import static io.seqera.wave.util.ContainerHelper.makeContainerId
9191
import static io.seqera.wave.util.ContainerHelper.makeResponseV1
@@ -248,7 +248,7 @@ class ContainerController {
248248

249249
if( v2 && req.packages ) {
250250
// generate the container file required to assemble the container
251-
final generated = containerFileFromPackages(req.packages, req.formatSingularity())
251+
final generated = containerFileFromRequest(req)
252252
req = req.copyWith(containerFile: generated.bytes.encodeBase64().toString())
253253
}
254254

src/main/groovy/io/seqera/wave/util/ContainerHelper.groovy

Lines changed: 27 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,7 @@ import io.seqera.wave.api.PackagesSpec
2727
import io.seqera.wave.api.SubmitContainerTokenRequest
2828
import io.seqera.wave.api.SubmitContainerTokenResponse
2929
import io.seqera.wave.config.CondaOpts
30+
import io.seqera.wave.config.PixiOpts
3031
import io.seqera.wave.core.ContainerPlatform
3132
import io.seqera.wave.exception.BadRequestException
3233
import io.seqera.wave.service.builder.BuildFormat
@@ -36,6 +37,7 @@ import org.yaml.snakeyaml.Yaml
3637
import static io.seqera.wave.service.builder.BuildFormat.SINGULARITY
3738
import static io.seqera.wave.util.DockerHelper.condaEnvironmentToCondaYaml
3839
import static io.seqera.wave.util.DockerHelper.condaFileToDockerFile
40+
import static io.seqera.wave.util.DockerHelper.condaFileToDockerFileUsingPixi
3941
import static io.seqera.wave.util.DockerHelper.condaFileToSingularityFile
4042
import static io.seqera.wave.util.DockerHelper.condaPackagesToCondaYaml
4143
import static io.seqera.wave.util.DockerHelper.condaPackagesToDockerFile
@@ -76,10 +78,34 @@ class ContainerHelper {
7678
}
7779
return result
7880
}
79-
8081
throw new BadRequestException("Unexpected packages spec type: $spec.type")
8182
}
8283

84+
static String containerFileFromRequest(SubmitContainerTokenRequest req) {
85+
if( !req.buildTemplate )
86+
return containerFileFromPackages(req.packages, req.formatSingularity())
87+
// build the container using the pixi template
88+
if( req.buildTemplate=='pixi/v1') {
89+
// check the type of the packages and apply
90+
if( req.packages.type == PackagesSpec.Type.CONDA ) {
91+
final lockFile = condaLockFile(req.packages.entries)
92+
final opts = req.packages.pixiOpts ?: new PixiOpts()
93+
if( req.containerImage )
94+
opts.baseImage = req.containerImage
95+
if( lockFile )
96+
throw new BadRequestException("Conda lock file is not supported by '${req}' template")
97+
if( req.formatSingularity() )
98+
throw new BadRequestException("Singularity is not supported by '${req.buildTemplate}' template")
99+
final result = condaFileToDockerFileUsingPixi(opts)
100+
return result
101+
}
102+
else
103+
throw new BadRequestException("Package type '${req.packages.type}' not supported by build template: ${req.buildTemplate}")
104+
105+
}
106+
throw new BadRequestException("Unexpected build template: ${req.buildTemplate}")
107+
}
108+
83109
static String condaFileFromRequest(SubmitContainerTokenRequest req) {
84110
if( !req.packages )
85111
return decodeBase64OrFail(req.condaFile, 'condaFile')

src/test/groovy/io/seqera/wave/util/ContainerHelperTest.groovy

Lines changed: 94 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,7 @@ import io.seqera.wave.api.ImageNameStrategy
2828
import io.seqera.wave.api.PackagesSpec
2929
import io.seqera.wave.api.SubmitContainerTokenRequest
3030
import io.seqera.wave.config.CondaOpts
31+
import io.seqera.wave.config.PixiOpts
3132
import io.seqera.wave.exception.BadRequestException
3233
import io.seqera.wave.service.request.ContainerRequest
3334
import io.seqera.wave.service.builder.BuildFormat
@@ -603,4 +604,97 @@ class ContainerHelperTest extends Specification {
603604

604605
}
605606

607+
608+
// === build with pixi tests
609+
610+
def 'should create conda docker file with packages and pixi'() {
611+
given:
612+
def CHANNELS = ['conda-forge', 'defaults']
613+
def PACKAGES = ['bwa=0.7.15', 'salmon=1.1.1']
614+
def packages = new PackagesSpec(
615+
type: PackagesSpec.Type.CONDA,
616+
entries: PACKAGES,
617+
channels: CHANNELS)
618+
and:
619+
def req = new SubmitContainerTokenRequest(packages:packages, buildTemplate: 'pixi/v1')
620+
when:
621+
def result = ContainerHelper.containerFileFromRequest(req)
622+
623+
then:
624+
result =='''\
625+
FROM ghcr.io/prefix-dev/pixi:latest AS build
626+
627+
COPY conda.yml /opt/wave/conda.yml
628+
WORKDIR /opt/wave
629+
630+
RUN pixi init --import /opt/wave/conda.yml \\
631+
&& pixi add conda-forge::procps-ng \\
632+
&& pixi shell-hook > /shell-hook.sh \\
633+
&& echo 'exec "$@"' >> /shell-hook.sh \\
634+
&& echo ">> CONDA_LOCK_START" \\
635+
&& cat /opt/wave/pixi.lock \\
636+
&& echo "<< CONDA_LOCK_END"
637+
638+
FROM ubuntu:24.04 AS prod
639+
640+
# copy the pixi environment in the final container
641+
COPY --from=build /opt/wave/.pixi/envs/default /opt/wave/.pixi/envs/default
642+
COPY --from=build /shell-hook.sh /shell-hook.sh
643+
644+
# set the entrypoint to the shell-hook script (activate the environment and run the command)
645+
# no more pixi needed in the prod container
646+
ENTRYPOINT ["/bin/bash", "/shell-hook.sh"]
647+
648+
# Default command for "docker run"
649+
CMD ["/bin/bash"]
650+
'''.stripIndent()
651+
}
652+
653+
def 'should create conda docker file with packages and pixi custom image'() {
654+
given:
655+
def CHANNELS = ['conda-forge', 'defaults']
656+
def PIXI_OPTS = new PixiOpts(
657+
basePackages: 'foo::one bar::two',
658+
baseImage: 'base/image',
659+
pixiImage: 'ghcr.io/prefix-dev/pixi:0.47.0-jammy-cuda-12.8.1')
660+
def PACKAGES = ['bwa=0.7.15', 'salmon=1.1.1']
661+
def packages = new PackagesSpec(
662+
type: PackagesSpec.Type.CONDA,
663+
entries: PACKAGES,
664+
channels: CHANNELS,
665+
pixiOpts: PIXI_OPTS)
666+
and:
667+
def req = new SubmitContainerTokenRequest(packages:packages, buildTemplate: 'pixi/v1')
668+
when:
669+
def result = ContainerHelper.containerFileFromRequest(req)
670+
671+
then:
672+
result =='''\
673+
FROM ghcr.io/prefix-dev/pixi:0.47.0-jammy-cuda-12.8.1 AS build
674+
675+
COPY conda.yml /opt/wave/conda.yml
676+
WORKDIR /opt/wave
677+
678+
RUN pixi init --import /opt/wave/conda.yml \\
679+
&& pixi add foo::one bar::two \\
680+
&& pixi shell-hook > /shell-hook.sh \\
681+
&& echo 'exec "$@"' >> /shell-hook.sh \\
682+
&& echo ">> CONDA_LOCK_START" \\
683+
&& cat /opt/wave/pixi.lock \\
684+
&& echo "<< CONDA_LOCK_END"
685+
686+
FROM base/image AS prod
687+
688+
# copy the pixi environment in the final container
689+
COPY --from=build /opt/wave/.pixi/envs/default /opt/wave/.pixi/envs/default
690+
COPY --from=build /shell-hook.sh /shell-hook.sh
691+
692+
# set the entrypoint to the shell-hook script (activate the environment and run the command)
693+
# no more pixi needed in the prod container
694+
ENTRYPOINT ["/bin/bash", "/shell-hook.sh"]
695+
696+
# Default command for "docker run"
697+
CMD ["/bin/bash"]
698+
'''.stripIndent()
699+
}
606700
}

0 commit comments

Comments
 (0)