Skip to content

✨ feat(cli): accept sdist tarball as srcdir argument - #1057

Merged
gaborbernat merged 3 commits into
pypa:mainfrom
gaborbernat:311
May 12, 2026
Merged

gaborbernat merged 3 commits into
pypa:mainfrom
gaborbernat:311

Conversation

@gaborbernat

@gaborbernat gaborbernat commented May 5, 2026 •

Copy link
Copy Markdown
Contributor

Closes #311.

Anyone testing a release artifact has had to extract the sdist by hand or run a separate pip wheel against the tarball before they could exercise the wheel build path. The positional srcdir argument now accepts a .tar.gz source distribution directly, matching the "build a wheel from an sdist" recipe that came out of the discussion on #311. 📦

When srcdir points at a .tar.gz, build first validates it: the filename must parse per PEP 625, the archive must open as tar, contain a single top-level directory, and include a PKG-INFO. The validated archive is extracted into a temporary directory and a wheel is built from the extracted source. The default --outdir for archive input is the directory containing the archive, matching the convention pip wheel already uses, so the wheel lands next to the sdist that produced it. ✨

The default for archive input is wheel-only rather than sdist+wheel: regenerating an sdist from another sdist is not a defined operation across backends, and the issue thread converged on rejecting it for now. Passing --sdist against an sdist input is therefore an error with a message pointing back at #311. --metadata and --wheel work as expected; --config-setting, --no-isolation, --installer, and friends are passed through unchanged. The public Python API (build_package, build_package_via_sdist, ProjectBuilder) keeps its directory-only contract — this is a CLI-only affordance, since broadening the API surface was outside the scope FFY00 had approved on the issue.

Input default --wheel --sdist --metadata
directory sdist + wheel via sdist wheel sdist metadata
.tar.gz wheel wheel error metadata

Validation against real PyPI sdists

Built five sdists pulled from PyPI:

Package Backend Result
attrs-26.1.0 hatch-vcs wheel built
click-8.3.3 flit-core wheel built
idna-3.13 flit-core wheel built
wheel-0.47.0 flit-core wheel built
markupsafe-3.0.3 setuptools + C ext platform wheel built (native ext compiled)

Error paths exercised manually:

  • --sdist <archive>: parser rejects with the [FR] Implement building wheels from sdist/tarball artifacts #311-pointed error
  • corrupt .tar.gz: BuildException: "failed to read source distribution" with tarfile diagnostics
  • filename without a version (notavalid.tar.gz): "does not look like a source distribution"
  • --metadata <archive>: PKG-INFO surfaced as JSON
  • default no-flag: wheel lands next to the input archive

Source-built vs sdist-built wheel equivalence

Built the same package both ways and compared:

Package Backend sha256 (source) == sha256 (sdist)
build (this repo) flit-core 87361bc1… (byte-identical)
attrs-26.1.0 hatch-vcs 94d456bc… (byte-identical)

For pure-Python wheels with deterministic backends, the sdist path produces a bit-for-bit identical wheel.

@gaborbernat gaborbernat added the enhancement New feature or request label May 5, 2026
@gaborbernat
gaborbernat force-pushed the 311 branch 2 times, most recently from 700e8c1 to 904db90 Compare May 5, 2026 23:46
Users testing release artifacts had to either extract the sdist by hand
or run a separate `pip wheel`/`pyproject-build` against the tarball. The
positional `srcdir` now accepts a `.tar.gz` directly, matching the
"build a wheel from an sdist" recipe discussed in the issue.

Validation rejects archives whose filename does not parse per PEP 625,
that fail to open as tar, that contain multiple top-level directories,
or that lack a `PKG-INFO`. Validated archives are extracted into a
temporary directory and a wheel is built from the extracted source; the
wheel lands next to the input archive by default, matching `pip wheel`.

The default for archive input is wheel-only rather than sdist+wheel:
regenerating an sdist from another sdist is not a defined operation
across backends, so `--sdist` against an sdist input is an error. The
public Python API stays directory-only; this is a CLI-only affordance.
Comment thread src/build/__main__.py Outdated

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Pull request overview

Adds a CLI-only enhancement so python -m build can take a .tar.gz sdist directly as the positional srcdir, enabling “build a wheel from an sdist artifact” workflows without manual extraction or an intermediate pip wheel.

Changes:

  • Extend CLI srcdir parsing to detect .tar.gz inputs, validate the archive as an sdist, extract to a temp dir, and build from the extracted source.
  • Adjust default behavior for archive inputs (wheel-only; default --outdir becomes the archive’s parent directory; reject --sdist with an actionable error).
  • Add unit/integration tests and update documentation + changelog to describe the new CLI behavior.

Reviewed changes

Copilot reviewed 7 out of 7 changed files in this pull request and generated no comments.

Show a summary per file
File Description
tests/test_main.py Adds fixtures and coverage for sdist-archive validation/extraction, CLI dispatch behavior, error paths, and an end-to-end wheel build from a generated sdist.
src/build/__main__.py Implements .tar.gz detection, archive validation + extraction helpers, updates CLI defaults/erroring, and reuses extraction logic for build_package_via_sdist.
docs/tutorial/getting-started.rst Documents building a wheel directly from a previously-built sdist.
docs/reference/cli.rst Updates CLI reference prose to note .tar.gz archive input behavior and --outdir/--sdist semantics.
docs/how-to/basic-usage.rst Adds a “Building from a source distribution” section and updates examples to use the new default invocation.
docs/explanation/how-it-works.rst Explains the wheel-only default behavior when the positional argument is an sdist archive.
docs/changelog/311.feature.rst Adds a changelog fragment announcing the new CLI capability.

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment thread docs/how-to/basic-usage.rst
Co-authored-by: Henry Schreiner <HenrySchreinerIII@gmail.com>

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Pull request overview

Copilot reviewed 7 out of 7 changed files in this pull request and generated 1 comment.

Comment thread src/build/__main__.py Outdated
Use the PEP 706 stdlib data filter when present and validate members
manually on 3.10.0-3.10.12 / 3.11.0-3.11.4, where the filter was not
backported. Rejects device files, paths that escape the destination,
and symlinks/hardlinks whose targets resolve outside it.
@gaborbernat
gaborbernat merged commit f4ebd49 into pypa:main May 12, 2026
59 checks passed
@gaborbernat
gaborbernat deleted the 311 branch June 29, 2026 22:29
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

enhancement New feature or request

Projects

None yet

Development

Successfully merging this pull request may close these issues.

[FR] Implement building wheels from sdist/tarball artifacts

3 participants