-
-
Notifications
You must be signed in to change notification settings - Fork 1.2k
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Add tool to help with "bootstraping" build #4389
base: main
Are you sure you want to change the base?
Changes from 9 commits
18bb4b4
2653064
74439ae
937b4ce
0d1f5c6
5a82eb9
59c6682
a4c7370
e8c733e
e8740f7
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,57 @@ | ||
---------------------------- | ||
Bootstrapping ``setuptools`` | ||
---------------------------- | ||
|
||
If you need to *build* ``setuptools`` without the help of any third party tool | ||
(like :pypi:`build` or :pypi:`pip`), you can use the following procedure: | ||
|
||
1. Obtain ``setuptools``'s source code and change to the project root directory. | ||
For example:: | ||
|
||
$ git clone https://github.com/pypa/setuptools | ||
$ cd setuptools | ||
|
||
2. Run the bootstrap utility with the version of Python you intend to use | ||
``setuptools`` with:: | ||
|
||
$ python3 -m setuptools._bootstrap | ||
|
||
This will create a :term:`setuptools-*.whl <PyPUG:Wheel>` file in the ``./dist`` directory. | ||
|
||
Furthermore, if you also need to bootstrap the *installation* of ``setuptools``, | ||
you can follow the additional steps: | ||
|
||
3. Find out the directory where Python expects packages to be installed. | ||
The following command can help with that:: | ||
|
||
$ python3 -m sysconfig | ||
|
||
Since ``setuptools`` is a pure-Python distribution, | ||
usually you will only need the path referring to ``purelib``. | ||
|
||
4. Extract the created ``.whl`` file into the relevant directory. | ||
For example:: | ||
|
||
$ python3 -m zipfile -e ./dist/setuptools-*.whl $TARGET_DIR | ||
|
||
|
||
Notes | ||
~~~~~ | ||
|
||
This procedure assumes that you have access to a fully functional Python | ||
installation, including the standard library. | ||
|
||
The ``setuptools._bootstrap`` tool is a modest bare-bones implementation | ||
that follows the :pep:`PyPA's build-system spec <517>`, | ||
simplified and stripped down to only support the ``setuptools`` package. | ||
|
||
This procedure is not intended for other packages, it will not | ||
provide the same guarantees as a proper Python package installer | ||
or build-frontend tool, and it is still experimental. | ||
|
||
The naming intentionally uses a ``_`` character to discourage | ||
regular users, as the tool is only provided for developers (or downstream packaging | ||
consumers) that need to deploy ``setuptools`` from scratch. | ||
|
||
This is a CLI-only implementation, with no API provided. | ||
Users interested in API usage are invited to follow :pep:`PyPA's build-system spec <517>`. |
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -32,3 +32,4 @@ setuptools changes. You have been warned. | |
|
||
developer-guide | ||
releases | ||
bootstrap |
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -167,6 +167,7 @@ exclude = [ | |
"newsfragments*", | ||
"docs", | ||
"docs.*", | ||
"build*", | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. When testing localy, I found pesky This is also the reason why I changed There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Ugh. That doesn't seem right... adding a concern that seemingly affects all builds for something that's only affected when a bootstrap was run. Perhaps instead the bootstrap routine could remove the build directory after running? There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I think it would make more sense if the routine could remove the build directory before running... I noted that if There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Would it make more sense if instead we do: [tool.setuptools.packages.find]
include = [
"setuptools*",
"pkg_resources*",
"_distutils_hack*",
]
exclude = [
"*.tests",
"*.tests.*",
] I think this simplifies things a bit and it is optimised for the case when the probability of adding If the probabilities are reversed, then it makes less sense. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
Probably the reason why There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I created a reproducer for the observed problem in #4394. (other than |
||
] | ||
namespaces = true | ||
|
||
|
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,65 @@ | ||
from __future__ import annotations | ||
|
||
import argparse | ||
import subprocess | ||
import sys | ||
import tempfile | ||
from pathlib import Path | ||
|
||
__all__: list[str] = [] # No public function, only CLI is provided. | ||
|
||
|
||
def _build(output_dir: Path) -> None: | ||
"""Emulate as close as possible the way a build frontend would work.""" | ||
# Call build_wheel hook from CWD | ||
_hook("build_sdist", Path.cwd(), output_dir) | ||
sdist = _find_or_halt(output_dir, "setuptools*.tar.gz", "Error building sdist") | ||
print(f"**** sdist created in `{sdist}` ****") | ||
|
||
# Call build_wheel hook from the sdist | ||
with tempfile.TemporaryDirectory() as tmp: | ||
subprocess.run([sys.executable, "-m", "tarfile", "-e", str(sdist), tmp]) | ||
|
||
root = _find_or_halt(Path(tmp), "setuptools-*", "Error finding sdist root") | ||
_hook("build_wheel", root, output_dir) | ||
|
||
wheel = _find_or_halt(output_dir, "setuptools*.whl", "Error building wheel") | ||
print(f"**** wheel created in `{wheel}` ****") | ||
|
||
|
||
def _find_or_halt(parent: Path, pattern: str, error: str) -> Path: | ||
if file := next(parent.glob(pattern), None): | ||
return file | ||
raise SystemExit(f"{error}. Cannot find `{parent / pattern}`") | ||
|
||
|
||
def _hook(name: str, source_dir: Path, output_dir: Path) -> None: | ||
# Call each hook in a fresh subprocess as required by PEP 517 | ||
out = str(output_dir.absolute()) | ||
script = f"from setuptools.build_meta import {name}; {name}({out!r})" | ||
subprocess.run([sys.executable, "-c", script], cwd=source_dir) | ||
|
||
|
||
def _cli() -> None: | ||
parser = argparse.ArgumentParser( | ||
description="**EXPERIMENTAL** bootstrapping script for setuptools. " | ||
"Note that this script will perform a **simplified** procedure and may not " | ||
"provide all the guarantees of full-blown Python build-frontend.\n" | ||
"To install the created wheel, please extract it into the relevant directory.", | ||
formatter_class=argparse.ArgumentDefaultsHelpFormatter, | ||
) | ||
parser.add_argument( | ||
"--output-dir", | ||
type=Path, | ||
default="./dist", | ||
help="Where to store the build artifacts", | ||
) | ||
params = parser.parse_args() | ||
if params.output_dir.exists() and len(list(params.output_dir.iterdir())) > 0: | ||
# Let's avoid accidents by preventing multiple wheels in the directory | ||
raise SystemExit(f'--output-dir="{params.output_dir}" must be empty.') | ||
_build(params.output_dir) | ||
|
||
|
||
if __name__ == "__main__": | ||
_cli() |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,51 @@ | ||
import os | ||
import shutil | ||
|
||
import pytest | ||
from setuptools.archive_util import unpack_archive | ||
|
||
|
||
CMD = ["python", "-m", "setuptools._bootstrap"] | ||
|
||
|
||
@pytest.fixture | ||
def setuptools_sourcetree(tmp_path, setuptools_sdist, request): | ||
""" | ||
Recreate the setuptools source tree. | ||
We use sdist in a temporary directory to avoid race conditions with build/dist dirs. | ||
""" | ||
unpack_archive(setuptools_sdist, tmp_path) | ||
root = next(tmp_path.glob("setuptools-*")) | ||
# Remove sdist's metadata/cache/artifacts to simulate fresh repo | ||
shutil.rmtree(root / "setuptools.egg-info", ignore_errors=True) | ||
(root / "PKG-INFO").unlink() | ||
# We need the bootstrap folder (not included in the sdist) | ||
shutil.copytree( | ||
os.path.join(request.config.rootdir, "bootstrap.egg-info"), | ||
os.path.join(root, "bootstrap.egg-info"), | ||
) | ||
return root | ||
|
||
|
||
def test_bootstrap_sourcetree(tmp_path, bare_venv, setuptools_sourcetree): | ||
bare_venv.run(CMD, cwd=str(setuptools_sourcetree)) | ||
wheel = next((setuptools_sourcetree / "dist").glob("*.whl")) | ||
assert wheel.name.startswith("setuptools") | ||
|
||
target = tmp_path / "target" | ||
target.mkdir() | ||
bare_venv.run(["python", "-m", "zipfile", "-e", str(wheel), str(target)]) | ||
|
||
# Included in wheel: | ||
assert (target / "distutils-precedence.pth").is_file() | ||
assert (target / "setuptools/__init__.py").is_file() | ||
assert (target / "pkg_resources/__init__.py").is_file() | ||
# Excluded from wheel: | ||
assert not (target / "setuptools/tests").is_dir() | ||
assert not (target / "pkg_resources/tests").is_dir() | ||
|
||
# Avoid errors on Windows by copying env before modifying | ||
# https://stackoverflow.com/questions/58997105 | ||
env = {**os.environ, "PYTHONPATH": str(target)} | ||
test = ["python", "-c", "print(__import__('setuptools').__version__)"] | ||
bare_venv.run(test, env=env) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I'm surprised this behavior isn't the default. Is it the case that if a dist directory existed prior to this change, it would end up in the sdist? That seems like bad default behavior and would have come up in other projects. I'd like to avoid this concern if possible. It's another case where adopting setuptools_scm will also bypass the concern.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
We probably don't need this one. The problem is likely to be with
tool.setuptools.packages.find
: #4394.