Skip to content

Commit b06f16c

Browse files
pditommasoclaude
andcommitted
Map conda channels to prefix.dev mirrors for pixi builds
Enable sharded repodata for pixi-based container builds by automatically mapping well-known conda channels to their prefix.dev mirrors: - conda-forge -> https://prefix.dev/conda-forge - bioconda -> https://prefix.dev/bioconda This provides 50-100x faster package resolution according to: https://prefix.dev/blog/sharded_repodata The mapping is only applied when using the CONDA_PIXI_V1 build template, leaving other build templates (micromamba v1/v2) unchanged. Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
1 parent ed82ba5 commit b06f16c

File tree

4 files changed

+154
-2
lines changed

4 files changed

+154
-2
lines changed

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

Lines changed: 7 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -84,17 +84,22 @@ class ContainerHelper {
8484
if( req.packages.type != PackagesSpec.Type.CONDA )
8585
return null
8686

87+
// Map channels to prefix.dev mirrors for pixi builds (enables sharded repodata for faster resolution)
88+
final channels = req.buildTemplate == CONDA_PIXI_V1
89+
? PixiHelper.mapChannelsToPixiServers(req.packages.channels)
90+
: req.packages.channels
91+
8792
if( req.packages.environment ) {
8893
// parse the attribute as a conda file path *and* append the base packages if any
8994
// note 'channel' is null, because they are expected to be provided in the conda file
9095
final decoded = decodeBase64OrFail(req.packages.environment, 'packages.envFile')
91-
return condaEnvironmentToCondaYaml(decoded, req.packages.channels)
96+
return condaEnvironmentToCondaYaml(decoded, channels)
9297
}
9398

9499
if ( req.packages.entries && !CondaHelper.tryGetLockFile(req.packages.entries)) {
95100
// create a minimal conda file with package spec from user input
96101
final String packages = req.packages.entries.join(' ')
97-
return condaPackagesToCondaYaml(packages, req.packages.channels)
102+
return condaPackagesToCondaYaml(packages, channels)
98103
}
99104

100105
return null;

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

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -34,6 +34,30 @@ import static TemplateUtils.condaFileToSingularityFileUsingPixi
3434
@CompileStatic
3535
class PixiHelper {
3636

37+
/**
38+
* Map of well-known conda channels to their prefix.dev mirrors.
39+
* Using prefix.dev mirrors enables sharded repodata which provides
40+
* 50-100x faster package resolution.
41+
* See: https://prefix.dev/blog/sharded_repodata
42+
*/
43+
static final Map<String, String> CHANNEL_TO_PREFIX_DEV = Map.of(
44+
'conda-forge', 'https://prefix.dev/conda-forge',
45+
'bioconda', 'https://prefix.dev/bioconda'
46+
)
47+
48+
/**
49+
* Maps conda channels to their prefix.dev mirrors for faster package resolution.
50+
* Channels that don't have a known prefix.dev mirror are left unchanged.
51+
*
52+
* @param channels List of conda channel names
53+
* @return List of channels with known channels mapped to prefix.dev mirrors
54+
*/
55+
static List<String> mapChannelsToPixiServers(List<String> channels) {
56+
if( !channels )
57+
return channels
58+
return channels.collect { ch -> CHANNEL_TO_PREFIX_DEV.getOrDefault(ch, ch) }
59+
}
60+
3761
/**
3862
* Generate a container file (Dockerfile or Singularity) using the Pixi template.
3963
* Only supports CONDA package type. Lock files are not supported.

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

Lines changed: 65 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -896,4 +896,69 @@ class ContainerHelperTest extends Specification {
896896
e.message == "Unexpected or missing package type 'CONDA' or build template 'invalid-template'"
897897
}
898898
899+
// === Channel mapping tests for pixi builds ===
900+
901+
def 'should map channels to prefix.dev mirrors for pixi builds' () {
902+
given:
903+
def spec = new PackagesSpec(type: PackagesSpec.Type.CONDA, entries: ['bwa', 'samtools'], channels: ['conda-forge', 'bioconda'])
904+
def req = new SubmitContainerTokenRequest(packages: spec, buildTemplate: BuildTemplate.CONDA_PIXI_V1)
905+
906+
when:
907+
def result = ContainerHelper.condaFileFromRequest(req)
908+
909+
then:
910+
result == '''\
911+
channels:
912+
- https://prefix.dev/conda-forge
913+
- https://prefix.dev/bioconda
914+
dependencies:
915+
- bwa
916+
- samtools
917+
'''.stripIndent()
918+
}
919+
920+
def 'should not map channels for non-pixi builds' () {
921+
given:
922+
def spec = new PackagesSpec(type: PackagesSpec.Type.CONDA, entries: ['bwa', 'samtools'], channels: ['conda-forge', 'bioconda'])
923+
def req = new SubmitContainerTokenRequest(packages: spec, buildTemplate: BuildTemplate.CONDA_MICROMAMBA_V2)
924+
925+
when:
926+
def result = ContainerHelper.condaFileFromRequest(req)
927+
928+
then:
929+
result == '''\
930+
channels:
931+
- conda-forge
932+
- bioconda
933+
dependencies:
934+
- bwa
935+
- samtools
936+
'''.stripIndent()
937+
}
938+
939+
def 'should map channels in environment file for pixi builds' () {
940+
given:
941+
def CONDA = '''\
942+
channels:
943+
- conda-forge
944+
dependencies:
945+
- bwa
946+
'''.stripIndent()
947+
and:
948+
def spec = new PackagesSpec(type: PackagesSpec.Type.CONDA, environment: CONDA.bytes.encodeBase64().toString(), channels: ['bioconda'])
949+
def req = new SubmitContainerTokenRequest(packages: spec, buildTemplate: BuildTemplate.CONDA_PIXI_V1)
950+
951+
when:
952+
def result = ContainerHelper.condaFileFromRequest(req)
953+
954+
then:
955+
result == '''\
956+
channels:
957+
- conda-forge
958+
- https://prefix.dev/bioconda
959+
dependencies:
960+
- bwa
961+
'''.stripIndent()
962+
}
963+
899964
}

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

Lines changed: 58 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -133,4 +133,62 @@ class PixiHelperTest extends Specification {
133133
def ex = thrown(BadRequestException)
134134
ex.message.contains("Conda lock file is not supported by 'conda/pixi:v1' template")
135135
}
136+
137+
// ---- Tests for channel mapping to prefix.dev mirrors ----
138+
139+
def 'should map conda-forge channel to prefix.dev mirror'() {
140+
when:
141+
def result = PixiHelper.mapChannelsToPixiServers(['conda-forge'])
142+
143+
then:
144+
result == ['https://prefix.dev/conda-forge']
145+
}
146+
147+
def 'should map bioconda channel to prefix.dev mirror'() {
148+
when:
149+
def result = PixiHelper.mapChannelsToPixiServers(['bioconda'])
150+
151+
then:
152+
result == ['https://prefix.dev/bioconda']
153+
}
154+
155+
def 'should map multiple known channels to prefix.dev mirrors'() {
156+
when:
157+
def result = PixiHelper.mapChannelsToPixiServers(['conda-forge', 'bioconda'])
158+
159+
then:
160+
result == ['https://prefix.dev/conda-forge', 'https://prefix.dev/bioconda']
161+
}
162+
163+
def 'should leave unknown channels unchanged'() {
164+
when:
165+
def result = PixiHelper.mapChannelsToPixiServers(['defaults', 'my-custom-channel'])
166+
167+
then:
168+
result == ['defaults', 'my-custom-channel']
169+
}
170+
171+
def 'should handle mixed known and unknown channels'() {
172+
when:
173+
def result = PixiHelper.mapChannelsToPixiServers(['conda-forge', 'defaults', 'bioconda', 'my-channel'])
174+
175+
then:
176+
result == ['https://prefix.dev/conda-forge', 'defaults', 'https://prefix.dev/bioconda', 'my-channel']
177+
}
178+
179+
def 'should handle null channel list'() {
180+
when:
181+
def result = PixiHelper.mapChannelsToPixiServers(null)
182+
183+
then:
184+
result == null
185+
}
186+
187+
def 'should handle empty channel list'() {
188+
when:
189+
def result = PixiHelper.mapChannelsToPixiServers([])
190+
191+
then:
192+
result == []
193+
}
136194
}

0 commit comments

Comments
 (0)