Skip to content
Merged
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
5 changes: 5 additions & 0 deletions CHANGES.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
5 changes: 5 additions & 0 deletions doc/changes.md
Original file line number Diff line number Diff line change
Expand Up @@ -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)

Expand Down
36 changes: 31 additions & 5 deletions nextstrain/cli/command/build.py
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down Expand Up @@ -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

Expand Down Expand Up @@ -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]
Expand Down Expand Up @@ -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
Comment on lines +455 to +469
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

What happens on ambient? Is directly interrogating the snakemake version acceptable?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It returns False and doesn't automatically add --benchmark-extended.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We could run snakemake --version and parse that, but I don't think it's worth the added complexity because this is currently all happening before actual invocation of Snakemake.



def parse_snakemake_args(args):
"""
Inspects a tiny subset of Snakemake's CLI arguments in order to determine
Expand Down
4 changes: 4 additions & 0 deletions nextstrain/cli/command/run.py
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down
17 changes: 13 additions & 4 deletions nextstrain/cli/runner/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -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:
Expand Down Expand Up @@ -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
22 changes: 22 additions & 0 deletions nextstrain/cli/runner/conda.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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.
Expand Down
3 changes: 3 additions & 0 deletions nextstrain/cli/runner/docker.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
1 change: 1 addition & 0 deletions nextstrain/cli/util.py
Original file line number Diff line number Diff line change
Expand Up @@ -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)

Expand Down
Loading