Skip to content

Package directive#7317

Draft
evanroyrees wants to merge 6 commits into
nextflow-io:package-directivefrom
evanroyrees:package-directive
Draft

Package directive#7317
evanroyrees wants to merge 6 commits into
nextflow-io:package-directivefrom
evanroyrees:package-directive

Conversation

@evanroyrees

Copy link
Copy Markdown

Generic package directive with pluggable package providers

Summary

This PR continues the package-directive proof-of-concept from #6342, building it out into a
complete pluggable package-provider system. It adds a generic package process directive backed
by per-manager nf-<provider> plugins, so each package manager is an independent plugin rather
than a dedicated process directive:

process hello {
    package 'numpy pandas', provider: 'uv'

    script:
    """
    python -c 'import numpy, pandas'
    """
}

Lineage: picks up the conda/pixi PoC from #6342 (thanks @edmundmiller) and follows the
maintainer guidance there: a single generic directive with pluggable providers, aligned with
Wave, instead of per-manager directives (as discussed in #7023). New providers address
long-standing requests: nix and guix (#1526) and the R ecosystem via pak and install2.r
(requested in the #6342 review).

What's included

  • Core enablement: the package directive under the v2 (strict) syntax parser, the
    nextflow.preview.package feature flag (script header or config), a packages config scope
    (packages.provider, packages.autoDetect), and manifest auto-detection. When a process has
    no package directive, the process module directory is scanned for a provider manifest
    (e.g. environment.yml, requirements.txt, pixi.toml), analogous to Wave's Dockerfile
    auto-detection.
  • Five new provider plugins: nf-uv (Python venvs), nf-nix, nf-guix, nf-pak and
    nf-install2r (R), each implementing the PackageProvider SPI with its own environment
    cache, config scope, and unit tests. All create local-filesystem environments.
  • conda/pixi providers extended and fixed: packaged extensions.idx so the extensions are
    actually discovered by pf4j, per-process options: override (folded into the environment
    cache hash), pixi fixes ([workspace] table, valid TOML dependency entries, host platform,
    .pixi marker handling), and removal of the PoC's duplicated CondaCache/CondaConfig
    copies (see design notes).
  • Wave hand-off: explicit provider-to-Wave mapping.
  • nf-test CI matrix: one job per provider, each provisioning its tool and running tagged
    nf-tests against a dev build.
  • Docs: docs/package.md (in the toctree) plus entries in the config, feature-flags, and
    process references.
  • ADR: adr/20260707-package-provider-plugins.md records the design decision (generic
    directive + provider SPI + plugins), the alternatives considered, and the conda-in-core
    rationale, following the ADR convention on master. It is marked proposed since you are
    the deciders. Happy to remove or rework it if you prefer the design discussion to live in
    this PR instead.

Design notes (deviations called out per the review guidelines)

  • Parser change is minimal and v2-only. package is a Groovy/Java keyword, so it cannot be
    declared in the DirectiveDsl interface. The v2 AST builder stops rejecting package as an
    identifier (the PACKAGE lexer token was already disabled in ScriptLexer.g4), and the
    directive resolver allows it only in the process-directive section: it is still rejected
    as an input/output qualifier or in output {} blocks, with tests locking this in. The v1
    parser is untouched; the feature requires NXF_SYNTAX_PARSER=v2 and is gated behind
    nextflow.preview.package.
  • Wave routing contract. Wave can only build conda-based environments, so conda, mamba,
    micromamba, and pixi map to Wave's CONDA build type, while local-only providers (uv,
    nix, guix, pak, install2r) skip Wave and are built locally by their plugin. Mixed
    pipelines work (conda processes built by Wave alongside uv processes created locally). No
    Wave functionality is added.
  • conda stays in core (for now). The PoC copied CondaCache/CondaConfig into
    plugins/nf-conda, leaving two classes with the same fully-qualified name on different
    classloaders. This PR removes the copies: the plugin reuses the core classes that the legacy
    conda directive already depends on, and the per-process options override lives in the core
    CondaCache. Fully extracting conda into nf-conda is a natural follow-up once the legacy
    directive is deprecated.
  • Config validation is wired end-to-end. Every provider scope (uv, nix, guix, pak,
    install2r, pixi) and the new packages scope implement ConfigScope and are registered
    in extensions.idx, so the strict config validator recognizes them instead of warning.

Why five providers in one PR

The SPI is only proven generic by implementations that differ structurally: uv (venv +
lockfile), nix/guix (store/profile model), pak/install2.r (language-level installers), alongside
conda/pixi (env-file model). Each plugin is small (~6 classes), isolated, and independently
testable; dropping any provider is a directory deletion. Happy to split if preferred.

Testing

  • Unit tests for every new class (Spock), including per-provider cache/config/provider suites,
    parser scoping, feature-flag handling, Wave mapping, manifest auto-detection, per-process
    options overrides, and expected-failure cases. Full suite green locally (make test for the
    touched modules: nextflow, nf-lang, and all provider plugins).
  • .github/workflows/package-providers.yml runs an nf-test job per provider (conda, uv, pixi,
    pak, install2r), provisioning each tool on the runner. nix and guix are excluded from CI
    because their store/daemon model does not provision cleanly on ephemeral runners; they are
    exercised via the demo workflows under tests/nf-test/examples/.

Limitations / follow-ups

  • The feature is preview-gated and v2-parser-only by design.
  • Spack provider (existing community interest), conda-lock support, and full extraction of
    conda from core are natural follow-ups.

All commits are DCO signed-off.

Free `package` as an identifier in the v2 lexer/AST builder and allow it as a process directive, scoped to the process-directive section only (it cannot be declared in the Java DirectiveDsl because it is a keyword). Add the `nextflow.preview.package` feature flag (NextflowMeta preview property, honored from the pipeline script or the config file) and the `packages` config scope (default provider, manifest auto-detection). Parse the directive's runtime argument shape (named-args options map, CharSequence/file values) and auto-detect provider manifest files in the process module directory via PackageProvider.getManifestFileNames() and PackageManager.detectSpec(). The directive requires NXF_SYNTAX_PARSER=v2.

Signed-off-by: Evan Rees <evanroyrees@gmail.com>
Each package manager is its own nf-<provider> plugin implementing PackageProvider (environment creation, activation, per-process options), with unit tests, registered in settings.gradle: uv (Python venvs), nix, guix, and R via pak and install2.r. All create local-filesystem environments. Each plugin registers its config scope (ConfigScope) so provider settings are recognized by config validation.

Signed-off-by: Evan Rees <evanroyrees@gmail.com>
Move extensions.idx to src/resources so the provider extensions are actually packaged and discovered by pf4j (they sat in a non-packaged directory). Remove the plugin's duplicated copies of CondaCache/CondaConfig and reuse the core classes instead -- the duplicate fully-qualified class names were a classloader hazard; the per-process create-options override moves into the core CondaCache (folded into the environment cache hash). Rework PixiConfig as a ConfigScope so the pixi scope is recognized by config validation. Fix pixi: use the [workspace] table instead of the deprecated [project], generate valid TOML dependency entries, declare the host platform, and treat only a regular .pixi file (not the env directory) as the project marker. Add conda/pixi unit tests.

Signed-off-by: Evan Rees <evanroyrees@gmail.com>
Map the conda family and pixi to Wave's CONDA build type and skip Wave for local-only providers (uv, nix, guix, pak, install2r), so their environments are built locally instead of being silently mis-built as conda. No Wave functionality is added.

Signed-off-by: Evan Rees <evanroyrees@gmail.com>
tests/nf-test provides a tagged nf-test per provider plus fixtures and a shared config; .github/workflows/package-providers.yml runs one CI job per provider (conda, uv, pixi, nix, pak, install2r), provisioning each tool -- nix via the Determinate installer, which sets up the store and daemon on the runner with flakes enabled. guix remains excluded from CI because it has no comparable turnkey provisioning for ephemeral runners; it is exercised via the demo workflows. Multi-provider demo workflows live under tests/nf-test/examples, kept out of the top-level tests glob so the v1 integration suite does not try to parse the package directive.

Signed-off-by: Evan Rees <evanroyrees@gmail.com>
docs/package.md (registered in the docs toctree): the provider/plugin/Wave-compatibility matrix, manifest auto-detection and custom-named manifest rules, per-process options, and the v2 parser requirement. Add the `packages` scope to the config reference, the `nextflow.preview.package` entry to the feature-flags reference, and the `package` directive to the process reference. Record the design decision as an ADR (adr/20260707-package-provider-plugins.md) following the convention on master.

Signed-off-by: Evan Rees <evanroyrees@gmail.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant