Build policy

Some Python packages publish dependency metadata that nab can read without running any code; others require invoking a PEP 517 build backend. [tool.nab].build-policy is the single knob that controls how far nab is willing to go.

Three levels, strictest first. Each level reads static metadata from every source it admits; the difference is what is permitted to fall through to a backend invocation when the static read returns nothing usable.

The default is build-local: local checkouts and workspace members may invoke a backend, but remote sources (PyPI sdists, VCS clones, archive sources) are read statically only. Lift a specific package to build-remote with a per-package override when you know it needs a real build ([tool.nab.packages.<name>]); keep the global default tight rather than enabling builds for the whole graph.

never

Static metadata only, from any source:

  • Wheels: always fine; their METADATA is static by definition.

  • PEP 643 (Metadata 2.2+) sdists: fine; the resolver reads the static Requires-Dist lines from PKG-INFO.

  • Sdists whose PKG-INFO declares Dynamic: Requires-Dist: nab falls back to reading [project].dependencies from the bundled pyproject.toml when the field is not listed under [project].dynamic. Anything beyond that is skipped at look-ahead and surfaces as a no-version diagnostic if no candidate ultimately works.

  • Local checkouts declared via [[tool.nab.local-sources]]: the directory’s pyproject.toml is read statically. A member that requires dynamic = ["dependencies"] is skipped.

  • VCS clones declared via [[tool.nab.vcs-sources]]: the clone is fetched and its pyproject.toml is read statically. Same skip behaviour for dynamic deps.

  • Archive sources declared via [[tool.nab.archive-sources]]: the .tar.gz is downloaded, hash-verified, and extracted, then its pyproject.toml is read statically. Same skip behaviour for dynamic deps.

Picks the most reproducible posture: every input to the SAT problem is a file read, not a sandboxed subprocess. Use never when you want a lockdown resolve with no backend invocations at all.

build-local (default)

Adds PEP 517 backend invocation on local checkouts. When a [[tool.nab.local-sources]] entry (or a workspace member) has dynamic = ["dependencies"], the project’s [build-system].build-backend runs inside an isolated venv via nab_python._build.runner and the resulting wheel METADATA is used. Remote PyPI sdists, VCS clones, and archive sources remain static-only.

build-remote

Builds extend to VCS clones, archive sources, and remote PyPI sdists. On top of build-local:

  • VCS-cloned trees with dynamic deps have the backend invoked on the clone.

  • Archive sources declared via [[tool.nab.archive-sources]] with dynamic deps have the backend invoked on the extracted tree; the bytes are network-fetched, so they count as remote.

  • PyPI sdists whose PKG-INFO is dynamic and which have no static pyproject.toml fallback are downloaded, extracted to a temp directory, and built.

A backend failure on any of these surfaces as UnsupportedSdistError; the resolver skips that version, then either picks the next candidate or, if no candidate works, reports the accumulated build failures as a no-version diagnostic. Honesty over silence: a version that needs a build which fails is treated as unbuildable, not as having zero dependencies.

Choosing a level

The default build-local handles the common case (a local checkout with dynamic = ["version"] from hatch-vcs or similar) without opening the door to remote-sdist builds. Lower to never when you want a fully hermetic resolve.

For transitive dependencies that only publish a dynamic sdist (native or CUDA-heavy wheels are the usual offenders), prefer a per-package override rather than raising the global to build-remote:

[tool.nab.packages.deepspeed]
build-policy = "build-remote"

That keeps the rest of the graph in the hermetic default while permitting the one package you actually need to build. When you know the package’s dependencies, a dependencies metadata override (see the configuration reference) resolves it under never without building at all.

Overrides

A per-package override replaces the global build policy for its selected packages, in either direction. Key it by name:

[tool.nab]
build-policy = "never"

[tool.nab.packages.deepspeed]
build-policy = "build-remote"

Or list several packages in one [[tool.nab.package-rules]] entry:

[[tool.nab.package-rules]]
match = ["deepspeed", "flash-attn"]
build-policy = "build-remote"

Set the build policy for every package served from a given index with a per-index override instead:

[tool.nab.index.internal]
build-policy = "build-remote"

A build-policy override for a local checkout, VCS clone, or archive source is matched by bare name only. A source build is decided before any version is resolved, so a version-scoped per-package override (a quoted "name <specifier>" key) does not govern a local, VCS, or archive source build, and per-index overrides do not apply to sources (a local source has no serving index). Use a bare-name key to govern a source build.

A declared platform forbids host builds

A PEP 517 backend always runs on the host nab runs on, so it reports the host’s dependencies. That is correct when you resolve for the host, but wrong when you resolve as if you were on another machine. Every target that moves the platform axis therefore forbids host builds: build-policy is forced to never, and an explicit non-never value (global or in any override) is a config error, checked before the resolve starts. That covers both surfaces that declare a machine:

  • [tool.nab.environment] with a platform or an implementation.

  • mode = "universal", where every matrix tuple declares one.

This matches pip, which requires --only-binary=:all: under --platform, --abi, or --implementation.

A retarget of the python axis alone ([tool.nab.environment].python, or --python X.Y) is different: the machine is still the host. nab warns that a build would report the host interpreter’s metadata, and permits it. This is a deliberate deviation from pip: the workspace build-local floor exists for members with dynamic metadata, and forbidding a build here would break every workspace that also pins a Python. Set build-policy = "never" to forbid it.