diff --git a/CHANGES.md b/CHANGES.md index d96eaead..52055703 100644 --- a/CHANGES.md +++ b/CHANGES.md @@ -24,6 +24,11 @@ development source code and as such may not be routinely kept up to date. ([#501](https://github.com/nextstrain/cli/pull/501)) +* `nextstrain build` and `nextstrain run` now run Snakemake with the + `--benchmark-extended` option for more detailed benchmark files when used with + recent versions of managed runtimes. + ([#467](https://github.com/nextstrain/cli/issues/467)) + # 10.4.2 (7 January 2026) ## Improvements diff --git a/doc/changes.md b/doc/changes.md index 0480819d..6772a9b4 100644 --- a/doc/changes.md +++ b/doc/changes.md @@ -28,6 +28,11 @@ development source code and as such may not be routinely kept up to date. ([#501](https://github.com/nextstrain/cli/pull/501)) +* `nextstrain build` and `nextstrain run` now run Snakemake with the + `--benchmark-extended` option for more detailed benchmark files when used with + recent versions of managed runtimes. + ([#467](https://github.com/nextstrain/cli/issues/467)) + (v10-4-2)= ## 10.4.2 (7 January 2026) diff --git a/nextstrain/cli/command/build.py b/nextstrain/cli/command/build.py index 0aa35d7a..c395e112 100644 --- a/nextstrain/cli/command/build.py +++ b/nextstrain/cli/command/build.py @@ -24,7 +24,7 @@ from ..argparse import add_extended_help_flags, AppendOverwriteDefault, SKIP_AUTO_DEFAULT_IN_HELP from ..debug import debug from ..errors import UsageError, UserError -from ..runner import docker, singularity, aws_batch +from ..runner import docker, conda, singularity, aws_batch from ..util import byte_quantity, runner_name, split_image_name, warn from ..volume import NamedVolume @@ -206,7 +206,7 @@ def register_parser(subparser): nargs = "?") # Register runner flags and arguments - runner.register_runners(parser, exec = ["snakemake", "--printshellcmds", ...]) + runner.register_runners(parser, exec = ["snakemake", ...]) return parser @@ -243,12 +243,21 @@ def run(opts): opts.volumes.append(build_volume) # for Docker, Singularity, and AWS Batch - # Automatically pass thru appropriate resource options to Snakemake to - # avoid the user having to repeat themselves (once for us, once for - # snakemake). if opts.exec == "snakemake": + opts.default_exec_args += [ + # Useful to see what's going on; see also 08ffc925. + "--printshellcmds", + + # Useful to have additional information in benchmark files. + *(["--benchmark-extended"] + if supports_benchmark_extended(opts) else []), + ] + snakemake_opts = parse_snakemake_args(opts.extra_exec_args) + # Automatically pass thru appropriate resource options to Snakemake to + # avoid the user having to repeat themselves (once for us, once for + # snakemake). if not snakemake_opts["--cores"]: if opts.cpus: opts.extra_exec_args += ["--cores=%d" % opts.cpus] @@ -443,6 +452,23 @@ def pathogen_volumes(directory: Path, *, name = "build") -> Tuple[NamedVolume, N return build_volume, working_volume +def supports_benchmark_extended(opts) -> bool: + """ + Check if the runner's image or environment supports Snakemake's + ``--benchmark-extended`` option (requires Snakemake ≥8.11.0). + """ + if opts.__runner__ in (docker, aws_batch, singularity): + image = runner.configured_image(opts) + if opts.__runner__ is singularity: + image = singularity.docker_image_name(image) + return docker.image_supports(docker.IMAGE_FEATURE.benchmark_extended, image) + + if opts.__runner__ is conda: + return conda.env_supports(conda.ENV_FEATURE.benchmark_extended) + + return False + + def parse_snakemake_args(args): """ Inspects a tiny subset of Snakemake's CLI arguments in order to determine diff --git a/nextstrain/cli/command/run.py b/nextstrain/cli/command/run.py index 1966a85b..73d2de45 100644 --- a/nextstrain/cli/command/run.py +++ b/nextstrain/cli/command/run.py @@ -292,6 +292,10 @@ def run(opts): # Useful to see what's going on; see also 08ffc925. "--printshellcmds", + # Useful to have additional information in benchmark files. + *(["--benchmark-extended"] + if build.supports_benchmark_extended(opts) else []), + # In our experience,¹ it's rarely useful to fail on incomplete outputs # (Snakemake's default behaviour) instead of automatically regenerating # them. diff --git a/nextstrain/cli/runner/__init__.py b/nextstrain/cli/runner/__init__.py index 779d7cca..402bdad6 100644 --- a/nextstrain/cli/runner/__init__.py +++ b/nextstrain/cli/runner/__init__.py @@ -239,10 +239,7 @@ def run(opts: Options, working_volume: NamedVolume = None, extra_env: Env = {}, supports these runtimes with `nextstrain check-setup`. """) - # Account for potentially different defaults for --image depending on the - # selected runner. - if opts.__runner__ is singularity and opts.image is docker.DEFAULT_IMAGE: - opts.image = singularity.DEFAULT_IMAGE + opts.image = configured_image(opts) if envdirs := os.environ.get("NEXTSTRAIN_RUNTIME_ENVDIRS"): try: @@ -277,3 +274,15 @@ def replace_ellipsis(items, elided_items): y for x in items for y in (elided_items if x is ... else [x]) ] + + +def configured_image(opts: Options) -> str: + """ + Return the effective image for the selected runner. + """ + # Account for potentially different defaults for --image depending on the + # selected runner. + if opts.__runner__ is singularity and opts.image is docker.DEFAULT_IMAGE: + return singularity.DEFAULT_IMAGE + + return opts.image diff --git a/nextstrain/cli/runner/conda.py b/nextstrain/cli/runner/conda.py index 8f41583d..29859606 100644 --- a/nextstrain/cli/runner/conda.py +++ b/nextstrain/cli/runner/conda.py @@ -106,6 +106,7 @@ import sys import tarfile import traceback +from enum import Enum from pathlib import Path, PurePosixPath from tempfile import TemporaryFile from typing import IO, Iterable, List, NamedTuple, Optional, cast @@ -173,6 +174,27 @@ } +class ENV_FEATURE(Enum): + # --benchmark-extended was introduced in Snakemake 8.11.0. + benchmark_extended = "20250717T164942Z" + + +def env_supports(feature: ENV_FEATURE) -> bool: + """ + Test if the conda environment supports a *feature*, i.e. by version + comparison of the nextstrain-base package against the feature's first + release. + + If the nextstrain-base package is not found, it is assumed to not have + support for the feature. + """ + meta = package_meta(NEXTSTRAIN_BASE) + if not meta: + return False + version = meta.get("version", "0") + return parse_version_lax(version) >= parse_version_lax(feature.value) + + def register_arguments(parser) -> None: """ No-op. No arguments necessary. diff --git a/nextstrain/cli/runner/docker.py b/nextstrain/cli/runner/docker.py index ea5182b3..c46eefa1 100644 --- a/nextstrain/cli/runner/docker.py +++ b/nextstrain/cli/runner/docker.py @@ -116,6 +116,9 @@ class IMAGE_FEATURE(Enum): # file overwriting) in ZIP extraction. aws_batch_overlays = "build-20250321T184358Z" + # --benchmark-extended was introduced in Snakemake 8.11.0. + benchmark_extended = "build-20250717T164950Z" + def register_arguments(parser) -> None: # Docker development options diff --git a/nextstrain/cli/util.py b/nextstrain/cli/util.py index 91ec1537..331bb1bf 100644 --- a/nextstrain/cli/util.py +++ b/nextstrain/cli/util.py @@ -64,6 +64,7 @@ def colored(color, text): ) +# TODO: Use str.removeprefix/removesuffix once Python 3.9 is the minimum supported version. def remove_prefix(prefix, string): return re.sub('^' + re.escape(prefix), '', string)