headroomlabs-ai/headroom · error · SystemExit

expected one sdist root directory, found {sorted(roots)}

Error message

expected one sdist root directory, found {sorted(roots)}

What it means

verify_sdist_license_files() opens the sdist tarball and requires every member to sit under exactly one top-level root directory (the standard `<name>-<version>/` layout sdists use). Multiple roots mean stray top-level files leaked into the archive; zero roots means an effectively empty archive.

Source

Thrown at scripts/build_python_release_smoke.py:125

            raise SystemExit(
                f"{wheel.name} should contain exactly one dist-info/METADATA, "
                f"found {len(metadata_members)}"
            )
        metadata = archive.read(metadata_members[0]).decode("utf-8")

    expected_line = f"Version: {expected_version}"
    if expected_line not in metadata.splitlines():
        raise SystemExit(f"{wheel.name} metadata missing {expected_line!r}")

    print(f"wheel metadata OK: {wheel.name} contains {native_members[0]}")


def verify_sdist_license_files(sdist: Path) -> None:
    with tarfile.open(sdist, "r:gz") as archive:
        names = set(archive.getnames())
        roots = {name.split("/", 1)[0] for name in names if "/" in name}
        if len(roots) != 1:
            raise SystemExit(f"expected one sdist root directory, found {sorted(roots)}")
        root = roots.pop()

        pkg_info_path = f"{root}/PKG-INFO"
        member = archive.getmember(pkg_info_path)
        fh = archive.extractfile(member)
        if fh is None:
            raise SystemExit(f"could not read {pkg_info_path} from {sdist.name}")
        pkg_info = fh.read().decode("utf-8")

    declared = []
    for line in pkg_info.splitlines():
        if not line.strip():
            break
        if line.startswith("License-File:"):
            declared.append(line.split(":", 1)[1].strip())

    if not declared:
        raise SystemExit(f"{sdist.name} declares no License-File entries")

View on GitHub (pinned to 322425c43b)

Solutions

  1. Inspect the archive roots: `tar -tzf file.tar.gz | cut -d/ -f1 | sort -u`.
  2. Move or exclude the stray top-level files (maturin sdist includes/excludes in pyproject.toml) so everything nests under the single `name-version/` root.
  3. Rebuild the sdist in a clean checkout to rule out untracked local files.
Defensive patterns

Strategy: validation

Validate before calling

import tarfile

def sdist_roots(sdist_path: str) -> set[str]:
    with tarfile.open(sdist_path, "r:gz") as t:
        return {n.split("/", 1)[0] for n in t.getnames() if "/" in n}
# assert len(sdist_roots(p)) == 1 before deeper verification

Prevention

When it happens

Trigger: maturin including extra top-level files in the sdist (stray files at repo root picked up by include patterns); an sdist built with a non-standard packer; an empty or corrupted tarball.

Common situations: Adding generated files at the repo root that maturin's sdist inclusion rules pick up; changing pyproject `[tool.maturin]` include/exclude globs; MANIFEST-style configuration drift.

Related errors


AI-assisted analysis of headroomlabs-ai/headroom@322425c43b (2026-08-15). Data as JSON: /api/errors/8f95b372a4ade7a8. Report an issue: GitHub.