Skip to content
Draft
Show file tree
Hide file tree
Changes from 29 commits
Commits
Show all changes
49 commits
Select commit Hold shift + click to select a range
cb07e9d
WIP: Start working on wave commands: Add command dummies.
JulianFlesch Dec 2, 2025
aa40d8b
[automated] Update CHANGELOG.md
nf-core-bot Dec 2, 2025
6b73c3c
added architecture
ningyuxin1999 Dec 2, 2025
5819387
Merge branch 'dev' into feature/3952-create-waveyml
ningyuxin1999 Dec 2, 2025
e689754
wire commands with functions
ningyuxin1999 Dec 2, 2025
c8873db
Merge branch 'feature/3952-create-waveyml' of https://github.com/Juli…
ningyuxin1999 Dec 2, 2025
8328f6f
containers obj
ningyuxin1999 Dec 2, 2025
6fba8ae
Merge branch 'dev' into feature/3952-create-waveyml
ningyuxin1999 Dec 3, 2025
78358ae
Update resolve module_dir. Change to using ValueError
JulianFlesch Dec 3, 2025
4493cad
Add TODOs
JulianFlesch Dec 3, 2025
1915c15
WIP: add container listing
JulianFlesch Dec 3, 2025
109d0b8
Move container constants to global utils.
JulianFlesch Dec 3, 2025
e723d5b
Use constants from global utils. Change list_containers to return lis…
JulianFlesch Dec 3, 2025
f44757b
add dry-run to the containers create, fixed the uitls import and modu…
ningyuxin1999 Dec 3, 2025
7c369a4
Add contextmanager that extends set_wd but changes into a tempdir
JulianFlesch Dec 4, 2025
81ddaf8
Add extracting container string from a module main.nf
JulianFlesch Dec 4, 2025
882ed67
Rename method to match field name
JulianFlesch Dec 4, 2025
aa9fa01
Print container info as richt table
JulianFlesch Dec 4, 2025
3e119a4
Merge branch 'feature/3952-create-waveyml' of github.com:JulianFlesch…
JulianFlesch Dec 4, 2025
11228d4
Move calling wave to ModuleContainers class. Finish create method and…
JulianFlesch Dec 4, 2025
c08a6dd
Fix output encoding
JulianFlesch Dec 4, 2025
c6e8360
Remove comment
JulianFlesch Dec 4, 2025
4317977
Add TODOs
JulianFlesch Dec 4, 2025
e1d7f7d
buildid and scanid saved to the dict
ningyuxin1999 Dec 5, 2025
6cd3380
Merge branch 'feature/3952-create-waveyml' of github.com:JulianFlesch…
JulianFlesch Dec 5, 2025
180287e
meta yaml
ningyuxin1999 Dec 5, 2025
d6e744f
Update calling wave command to get buildId/scanId. Only add to outpu …
JulianFlesch Dec 5, 2025
ad5463c
Simplify create method
JulianFlesch Dec 5, 2025
e0edf97
Refactor to reuse key constants.
JulianFlesch Dec 5, 2025
141c2b8
Add conda-lock information to containers dict
JulianFlesch Dec 5, 2025
972cd5e
Fix space in url
JulianFlesch Dec 5, 2025
78bd273
Implement conda lock functions. Do not fail in containers_from_meta i…
JulianFlesch Dec 5, 2025
ad640fa
Simplify class
JulianFlesch Dec 5, 2025
9b821e7
Implement updating meta - WIP: sorting
JulianFlesch Dec 5, 2025
f117fca
Fix ModuleContainers initialisation
JulianFlesch Dec 5, 2025
d0f42cc
Fix expected meta.yml path
JulianFlesch Dec 5, 2025
3ccd190
Update containers in meta.yml - WIP: sorting
JulianFlesch Dec 5, 2025
cf1dcfa
update container from meta method
JulianFlesch Dec 5, 2025
76c64f2
Review: Rename conda lock methods
JulianFlesch Dec 8, 2025
6a69fb2
Fix typo
JulianFlesch Dec 8, 2025
ff23c7d
Update nf_core/modules/containers.py
JulianFlesch Dec 8, 2025
b7e5102
Review command: Remove dry_run flag for module container creation#
JulianFlesch Dec 8, 2025
2fa3538
remove unnecessary string casting
JulianFlesch Dec 8, 2025
af97d90
Review comment: Remove extra loop for conda-lock files
JulianFlesch Dec 8, 2025
77e263e
Merge
JulianFlesch Dec 8, 2025
8f3bfb1
Make wave container requests run concurrently
JulianFlesch Dec 8, 2025
3221030
fix gathering concurrent results
JulianFlesch Dec 8, 2025
6aca629
fix building conda section
JulianFlesch Dec 8, 2025
69b6424
Remove threads option.
JulianFlesch Dec 8, 2025
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
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@
- Pin j178/prek-action action to 91fd7d7 ([#3931](https://github.com/nf-core/tools/pull/3931))
- add pre-commit hook to keep uv.lock in sync ([#3933](https://github.com/nf-core/tools/pull/3933))
- Update mcr.microsoft.com/devcontainers/miniconda Docker digest to 2be0f5a ([#3946](https://github.com/nf-core/tools/pull/3946))
- Implement wave container commands ([#3954](https://github.com/nf-core/tools/pull/3954))
- Fix docker errors in test ([#3924](https://github.com/nf-core/tools/pull/3924))
- Update actions/checkout digest to 8e8c483 ([#3956](https://github.com/nf-core/tools/pull/3956))

Expand Down
96 changes: 95 additions & 1 deletion nf_core/__main__.py
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,10 @@
modules_remove,
modules_test,
modules_update,
modules_containers_create,
modules_containers_conda_lock,
modules_containers_list,
modules_containers_lint,
)
from nf_core.commands_pipelines import (
pipelines_bump_version,
Expand Down Expand Up @@ -862,7 +866,7 @@ def command_pipelines_schema_docs(directory, schema_file, output, format, force,
help="Do not pull in latest changes to local clone of modules repository.",
)
@click.command_panel("For pipeline development", commands=["list", "info", "install", "update", "remove", "patch"])
@click.command_panel("For module development", commands=["create", "lint", "test", "bump-versions"])
@click.command_panel("For module development", commands=["create", "lint", "test", "bump-versions", "containers"])
@click.pass_context
def modules(ctx, git_remote, branch, no_pull):
"""
Expand Down Expand Up @@ -1377,6 +1381,96 @@ def command_modules_bump_versions(ctx, tool, directory, all, show_all, dry_run):
modules_bump_versions(ctx, tool, directory, all, show_all, dry_run)


@modules.group("containers")
@click.pass_context
def modules_containers(ctx):
"""Manage module container builds and metadata."""
pass


@modules_containers.command("create")
@click.pass_context
@click.option(
"-await",
"--await",
"await_",
is_flag=True,
default=False,
help="Wait for the container build to finish.",
)
@click.argument(
"module",
type=str,
required=False,
callback=normalize_case,
metavar="<module> or <module/submodule>",
shell_complete=autocomplete_modules,
)
@click.option(
"--dry-run/--run",
"dry_run",
is_flag=True,
default=False,
help="Print the wave commands instead of executing them.",
)
def command_modules_containers_create(ctx, await_, dry_run, module):
"""
Build docker and singularity container files for linux/arm64 and linux/amd64 with wave from environment.yml and create container config file.
"""
modules_containers_create(ctx, module, await_, dry_run)


@modules_containers.command("conda-lock")
@click.pass_context
@click.argument(
"module",
type=str,
required=False,
callback=normalize_case,
metavar="<module> or <module/submodule>",
shell_complete=autocomplete_modules,
)
def command_modules_containers_conda_lock(ctx, module):
"""
Build a Docker linux/arm64 container and fetch the conda lock file for a module.
"""
modules_containers_conda_lock(ctx, module)


@modules_containers.command("lint")
@click.pass_context
@click.argument(
"module",
type=str,
required=False,
callback=normalize_case,
metavar="<module> or <module/submodule>",
shell_complete=autocomplete_modules,
)
def command_modules_containers_lint(ctx, module):
"""
Confirm that container images for a module exist.
"""
modules_containers_lint(ctx, module)


@modules_containers.command("list")
@click.pass_context
@click.argument(
"module",
type=str,
required=False,
callback=normalize_case,
metavar="<module> or <module/submodule>",
shell_complete=autocomplete_modules,
)
def command_modules_containers_list(ctx, module):
"""
Print containers defined in a module meta.yml.
"""
modules_containers_list(ctx, module)


# nf-core subworkflows click command
@nf_core_cli.group(aliases=["s", "swf", "subworkflow"])
@click.option(
Expand Down
88 changes: 88 additions & 0 deletions nf_core/commands_modules.py
Original file line number Diff line number Diff line change
Expand Up @@ -356,3 +356,91 @@ def modules_bump_versions(ctx, tool, directory, all, show_all, dry_run):
except (UserWarning, LookupError) as e:
log.critical(e)
sys.exit(1)


def modules_containers_create(ctx, module, await_: bool, dry_run: bool = False):
"""
Build docker and singularity containers for linux/arm64 and linux/amd64 using wave.
"""
from nf_core.modules.containers import ModuleContainers

try:
manager = ModuleContainers(
".",
ctx.obj.get("modules_repo_url"),
ctx.obj.get("modules_repo_branch"),
ctx.obj.get("modules_repo_no_pull"),
ctx.obj.get("hide_progress"),
)
containers = manager.create(module, await_, dry_run)
# make ruff happy
print(containers)
except (UserWarning, LookupError, FileNotFoundError, ValueError) as e:
log.error(e)
sys.exit(1)


def modules_containers_conda_lock(ctx, module):
"""
Build a Docker linux/arm64 container and fetch the conda lock file using wave.
"""
from nf_core.modules.containers import ModuleContainers

try:
manager = ModuleContainers(
".",
ctx.obj.get("modules_repo_url"),
ctx.obj.get("modules_repo_branch"),
ctx.obj.get("modules_repo_no_pull"),
ctx.obj.get("hide_progress"),
)
cmd = manager.conda_lock(module)
stdout.print(" ".join(cmd))
except (UserWarning, LookupError, FileNotFoundError, ValueError) as e:
log.error(e)
sys.exit(1)


def modules_containers_list(ctx, module):
"""
Print containers defined in a module meta.yml.
"""
from nf_core.modules.containers import ModuleContainers

try:
manager = ModuleContainers(
".",
ctx.obj.get("modules_repo_url"),
ctx.obj.get("modules_repo_branch"),
ctx.obj.get("modules_repo_no_pull"),
ctx.obj.get("hide_progress"),
)
containers = manager.list_containers(module)
t = rich.table.Table("Container System", "Platform", "Image")
for cs, p, img in containers:
t.add_row(cs, p, img)
stdout.print(t)
except (UserWarning, LookupError, FileNotFoundError, ValueError) as e:
log.error(e)
sys.exit(1)


def modules_containers_lint(ctx, module):
"""
Confirm containers are defined for the module.
"""
from nf_core.modules.containers import ModuleContainers

try:
manager = ModuleContainers(
".",
ctx.obj.get("modules_repo_url"),
ctx.obj.get("modules_repo_branch"),
ctx.obj.get("modules_repo_no_pull"),
ctx.obj.get("hide_progress"),
)
containers = manager.lint(module)
stdout.print(f"Found {len(containers)} container(s) for {module}.")
except (UserWarning, LookupError, FileNotFoundError, ValueError) as e:
log.error(e)
sys.exit(1)
64 changes: 64 additions & 0 deletions nf_core/components/nfcore_component.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,11 +2,14 @@
The NFCoreComponent class holds information and utility functions for a single module or subworkflow
"""

import json
import logging
import re
from pathlib import Path
from typing import Any

from nf_core.utils import NF_INSPECT_MIN_NF_VERSION, check_nextflow_version, run_cmd, set_wd_tempdir

log = logging.getLogger(__name__)


Expand Down Expand Up @@ -58,6 +61,7 @@ def __init__(
self.is_patched: bool = False
self.branch: str | None = None
self.workflow_name: str | None = None
self.container: str

if remote_component:
# Initialize the important files
Expand Down Expand Up @@ -341,3 +345,63 @@ def get_topics_from_main_nf(self) -> None:
log.debug(f"Found {len(list(topics.keys()))} topics in {self.main_nf}")
log.debug(f"Topics: {topics}")
self.topics = topics

def get_container_from_main_nf(self) -> None:
if self.component_type == "module":
if check_nextflow_version(NF_INSPECT_MIN_NF_VERSION):
self.container = self._get_container_with_inspect()
else:
self.container = self._get_container_with_regex()

if not self.container:
log.warning(f"No container was extracted for {self.component_name} from {self.main_nf}")

def _get_container_with_inspect(self):
with set_wd_tempdir():
self.component_dir.absolute()

executable = "nextflow"
cmd_params = f"inspect -format json {self.main_nf}"
cmd_out = run_cmd(executable, cmd_params)
if cmd_out is None:
log.debug("Failed to run `nextflow inspect`")
log.debug("Falling back to regex method")
return self._get_container_with_regex()

out, _ = cmd_out
out_json = json.loads(out)
container = out_json.get("processes", [{}])[0].get("container", None)
if container is None:
log.debug(
f"Container for {self.component_name} could not be extracted from the output of nextflow inspect"
)
log.debug(f"Output of nextflow inspect: {out}")
log.debug("Falling back to regex method.")
return self._get_container_with_regex()

return container

def _get_container_with_regex(self):
with open(self.main_nf) as f:
data = f.read()

if "container:" not in data:
log.debug(f"Could not find a container directive for {self.component_name} in {self.main_nf}")
return ""

# Regex explained:
# 1. negative lookahead for "container" and arbitrary white spaces.
# 2. Capturing group 1: Match a quote char " or '
# 3. Match any characters
# 4. Match whatever was most recently captured in capturing group 1
regex_container = r"(?<=container\s+)([\"']).+?(\1)"
match = re.search(regex_container, data)
if not match:
log.warning(
f"Container for {self.component_name} could not be extracted from {self.main_nf} with regex"
)
return ""

# quotes " or ' were matched as well and are clipped
container = data[match.start()[0] + 1 : match.end()[0] - 1]
return container
2 changes: 2 additions & 0 deletions nf_core/module-template/main.nf
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,8 @@ process {{ component_name_underscore|upper }} {
// TODO nf-core: See section in main README for further information regarding finding and adding container addresses to the section below.
{% endif -%}
conda "${moduleDir}/environment.yml"

// TODO container-conversion: Update to only one line. Move the platform logic to meta.yml
container "${ workflow.containerEngine == 'singularity' && !task.ext.singularity_pull_docker_container ?
'{{ singularity_container if singularity_container else 'https://depot.galaxyproject.org/singularity/YOUR-TOOL-HERE' }}':
'{{ docker_container if docker_container else 'biocontainers/YOUR-TOOL-HERE' }}' }"
Expand Down
2 changes: 2 additions & 0 deletions nf_core/module-template/meta.yml
Original file line number Diff line number Diff line change
Expand Up @@ -73,3 +73,5 @@ authors:
- "{{ author }}"
maintainers:
- "{{ author }}"

# TODO container-conversion: Add "containers" section
Loading