From e3a9500ebf3c5905511efee376008d009f80c147 Mon Sep 17 00:00:00 2001 From: James Hilliard Date: Sat, 15 Jan 2022 18:15:05 -0700 Subject: [PATCH 1/5] Add bootstrap script/wheel module for flit_core --- flit_core/bootstrap.py | 87 ++++++++++++++++++++++++++++++++++++ flit_core/flit_core/wheel.py | 39 ++++++++++++++++ flit_core/pyproject.toml | 3 ++ 3 files changed, 129 insertions(+) create mode 100644 flit_core/bootstrap.py diff --git a/flit_core/bootstrap.py b/flit_core/bootstrap.py new file mode 100644 index 00000000..830a127f --- /dev/null +++ b/flit_core/bootstrap.py @@ -0,0 +1,87 @@ +"""Install flit_core without using any other tools. + +Normally, you would install flit_core with pip like any other Python package. +This script is meant to help with 'bootstrapping' other packaging +systems, where you may need flit_core to build other packaging tools. + +Pass a path to the site-packages directory or equivalent where the package +should be placed. If omitted, this defaults to the site-packages directory +of the Python running the script. +""" +import argparse +import sys +import sysconfig +from pathlib import Path +from tempfile import TemporaryDirectory +from zipfile import ZipFile + +from flit_core.wheel import add_wheel_arguments, build_flit_wheel + +srcdir = Path(__file__).parent.resolve() + +def extract_wheel(whl_path, dest): + print("Installing to", dest.resolve()) + with ZipFile(whl_path) as zf: + zf.extractall(dest) + +def add_install_arguments(parser): + parser.add_argument( + '--wheeldir', + '-w', + type=str, + help=f'wheel dist directory (defaults to {srcdir.joinpath("dist").resolve()})', + ) + purelib = Path(sysconfig.get_path('purelib')) + parser.add_argument( + '--installdir', + '-i', + type=Path, + default=purelib, + help=f'installdir directory (defaults to {purelib.resolve()}', + ) + return parser + +def get_dist_wheel(wheeldir): + wheel_path = Path(wheeldir) if wheeldir is not None else srcdir.joinpath('dist') + wheel_glob = wheel_path.glob('flit_core-*.whl') + return next(wheel_glob, None) + +def build(args): + print("Building wheel") + outdir = srcdir.joinpath('dist') if args.outdir is None else Path(args.outdir) + whl_fname = build_flit_wheel(srcdir, outdir) + print("Wheel built", outdir.joinpath(whl_fname).resolve()) + +def install(args): + dist_wheel = get_dist_wheel(args.wheeldir) + + # User asked to install wheel but none was found + if dist_wheel is None and args.wheeldir is not None: + print(f"No wheel found in {Path(args.wheeldir).resolve()}") + sys.exit(1) + + if dist_wheel is not None: + print("Installing from wheel", dist_wheel.resolve()) + # Extract the prebuilt wheel + extract_wheel(dist_wheel, args.installdir) + else: + # No prebuilt wheel found, build in temp dir + with TemporaryDirectory(prefix='flit_core-bootstrap-') as td: + whl_fname = build_flit_wheel(srcdir, Path(td)) + whl_path = Path(td).joinpath(whl_fname) + extract_wheel(whl_path, args.installdir) + +if __name__ == "__main__": + parser = argparse.ArgumentParser() + subparsers = parser.add_subparsers() + + build_parser = subparsers.add_parser('build') + build_parser = add_wheel_arguments(build_parser, srcdir) + build_parser.set_defaults(func=build) + + install_parser = subparsers.add_parser('install') + install_parser = add_install_arguments(install_parser) + install_parser.set_defaults(func=install) + + args = parser.parse_args() + args.func(args) diff --git a/flit_core/flit_core/wheel.py b/flit_core/flit_core/wheel.py index 8c87451e..d9f83a83 100644 --- a/flit_core/flit_core/wheel.py +++ b/flit_core/flit_core/wheel.py @@ -1,3 +1,4 @@ +import argparse from base64 import urlsafe_b64encode import contextlib from datetime import datetime @@ -8,6 +9,7 @@ import os.path as osp import stat import tempfile +from pathlib import Path from types import SimpleNamespace from typing import Optional import zipfile @@ -215,3 +217,40 @@ def make_wheel_in(ini_path, wheel_directory, editable=False): log.info("Built wheel: %s", wheel_path) return SimpleNamespace(builder=wb, file=wheel_path) + +def add_wheel_arguments(parser, srcdir=None): + if srcdir is None: + srcdir = Path.cwd() + parser.add_argument( + '--srcdir', + '-s', + type=Path, + default=Path.cwd(), + help='source directory (defaults to current directory)', + ) + + parser.add_argument( + '--outdir', + '-o', + type=Path, + default=srcdir.joinpath('dist'), + help=f'output directory (defaults to {srcdir.joinpath("dist").resolve()})', + ) + return parser + +def build_flit_wheel(srcdir, outdir): + """Builds a wheel, places it in outdir""" + pyproj_toml = Path(srcdir).joinpath('pyproject.toml') + outdir.mkdir(parents=True, exist_ok=True) + info = make_wheel_in(pyproj_toml, Path(outdir)) + return info.file.name + +if __name__ == "__main__": + parser = argparse.ArgumentParser() + parser = add_wheel_arguments(parser) + args = parser.parse_args() + srcdir = Path.cwd() if args.srcdir is None else Path(args.srcdir) + outdir = args.outdir + print("Building wheel") + whl_fname = build_flit_wheel(srcdir, outdir) + print("Wheel built", outdir.joinpath(whl_fname).resolve()) diff --git a/flit_core/pyproject.toml b/flit_core/pyproject.toml index ca3f46b2..03771248 100644 --- a/flit_core/pyproject.toml +++ b/flit_core/pyproject.toml @@ -19,3 +19,6 @@ dynamic = ["version"] [project.urls] Source = "https://github.com/pypa/flit" + +[tool.flit.sdist] +include = ["bootstrap.py"] From cfac2b923b992d4c07b8e71cb029ed97126e2c31 Mon Sep 17 00:00:00 2001 From: Thomas Kluyver Date: Wed, 16 Feb 2022 11:53:50 +0000 Subject: [PATCH 2/5] Simplify bootstrap code --- flit_core/bootstrap.py | 74 +++++++----------------------------- flit_core/flit_core/wheel.py | 46 +++++++++------------- 2 files changed, 32 insertions(+), 88 deletions(-) diff --git a/flit_core/bootstrap.py b/flit_core/bootstrap.py index 830a127f..2c6c3d80 100644 --- a/flit_core/bootstrap.py +++ b/flit_core/bootstrap.py @@ -3,85 +3,39 @@ Normally, you would install flit_core with pip like any other Python package. This script is meant to help with 'bootstrapping' other packaging systems, where you may need flit_core to build other packaging tools. - -Pass a path to the site-packages directory or equivalent where the package -should be placed. If omitted, this defaults to the site-packages directory -of the Python running the script. """ import argparse import sys import sysconfig from pathlib import Path -from tempfile import TemporaryDirectory from zipfile import ZipFile -from flit_core.wheel import add_wheel_arguments, build_flit_wheel - -srcdir = Path(__file__).parent.resolve() - def extract_wheel(whl_path, dest): - print("Installing to", dest.resolve()) + print("Installing to", dest) with ZipFile(whl_path) as zf: zf.extractall(dest) -def add_install_arguments(parser): +if __name__ == "__main__": + parser = argparse.ArgumentParser() parser.add_argument( - '--wheeldir', - '-w', - type=str, - help=f'wheel dist directory (defaults to {srcdir.joinpath("dist").resolve()})', + 'wheel', + type=Path, + help=f'flit_core wheel to install (.whl file)', ) - purelib = Path(sysconfig.get_path('purelib')) + purelib = Path(sysconfig.get_path('purelib')).resolve() parser.add_argument( '--installdir', '-i', type=Path, default=purelib, - help=f'installdir directory (defaults to {purelib.resolve()}', + help=f'installdir directory (defaults to {purelib})', ) - return parser - -def get_dist_wheel(wheeldir): - wheel_path = Path(wheeldir) if wheeldir is not None else srcdir.joinpath('dist') - wheel_glob = wheel_path.glob('flit_core-*.whl') - return next(wheel_glob, None) - -def build(args): - print("Building wheel") - outdir = srcdir.joinpath('dist') if args.outdir is None else Path(args.outdir) - whl_fname = build_flit_wheel(srcdir, outdir) - print("Wheel built", outdir.joinpath(whl_fname).resolve()) -def install(args): - dist_wheel = get_dist_wheel(args.wheeldir) - - # User asked to install wheel but none was found - if dist_wheel is None and args.wheeldir is not None: - print(f"No wheel found in {Path(args.wheeldir).resolve()}") - sys.exit(1) - - if dist_wheel is not None: - print("Installing from wheel", dist_wheel.resolve()) - # Extract the prebuilt wheel - extract_wheel(dist_wheel, args.installdir) - else: - # No prebuilt wheel found, build in temp dir - with TemporaryDirectory(prefix='flit_core-bootstrap-') as td: - whl_fname = build_flit_wheel(srcdir, Path(td)) - whl_path = Path(td).joinpath(whl_fname) - extract_wheel(whl_path, args.installdir) - -if __name__ == "__main__": - parser = argparse.ArgumentParser() - subparsers = parser.add_subparsers() - - build_parser = subparsers.add_parser('build') - build_parser = add_wheel_arguments(build_parser, srcdir) - build_parser.set_defaults(func=build) + args = parser.parse_args() - install_parser = subparsers.add_parser('install') - install_parser = add_install_arguments(install_parser) - install_parser.set_defaults(func=install) + if not args.wheel.name.startswith('flit_core-'): + sys.exit("Use this script only for flit_core wheels") + if not args.installdir.is_dir(): + sys.exit(f"{args.installdir} is not a directory") - args = parser.parse_args() - args.func(args) + extract_wheel(args.wheel, args.installdir) diff --git a/flit_core/flit_core/wheel.py b/flit_core/flit_core/wheel.py index d9f83a83..df228ecd 100644 --- a/flit_core/flit_core/wheel.py +++ b/flit_core/flit_core/wheel.py @@ -218,39 +218,29 @@ def make_wheel_in(ini_path, wheel_directory, editable=False): log.info("Built wheel: %s", wheel_path) return SimpleNamespace(builder=wb, file=wheel_path) -def add_wheel_arguments(parser, srcdir=None): - if srcdir is None: - srcdir = Path.cwd() - parser.add_argument( - '--srcdir', - '-s', - type=Path, - default=Path.cwd(), - help='source directory (defaults to current directory)', - ) +def main(): + parser = argparse.ArgumentParser() parser.add_argument( - '--outdir', - '-o', + 'srcdir', type=Path, - default=srcdir.joinpath('dist'), - help=f'output directory (defaults to {srcdir.joinpath("dist").resolve()})', + nargs='?', + default=Path.cwd(), + help='source directory (defaults to current directory)', ) - return parser -def build_flit_wheel(srcdir, outdir): - """Builds a wheel, places it in outdir""" - pyproj_toml = Path(srcdir).joinpath('pyproject.toml') + parser.add_argument( + '--outdir', + '-o', + help='output directory (defaults to {srcdir}/dist)', + ) + args = parser.parse_args() + outdir = args.srcdir / 'dist' if args.outdir is None else Path(args.outdir) + print("Building wheel from", args.srcdir) + pyproj_toml = args.srcdir / 'pyproject.toml' outdir.mkdir(parents=True, exist_ok=True) - info = make_wheel_in(pyproj_toml, Path(outdir)) - return info.file.name + info = make_wheel_in(pyproj_toml, outdir) + print("Wheel built", outdir / info.file.name) if __name__ == "__main__": - parser = argparse.ArgumentParser() - parser = add_wheel_arguments(parser) - args = parser.parse_args() - srcdir = Path.cwd() if args.srcdir is None else Path(args.srcdir) - outdir = args.outdir - print("Building wheel") - whl_fname = build_flit_wheel(srcdir, outdir) - print("Wheel built", outdir.joinpath(whl_fname).resolve()) + main() From 9a403a8c891a8defa96af348a297debcd2966d30 Mon Sep 17 00:00:00 2001 From: Thomas Kluyver Date: Wed, 16 Feb 2022 11:58:04 +0000 Subject: [PATCH 3/5] Rename script to bootstrap_install.py --- flit_core/{bootstrap.py => bootstrap_install.py} | 7 +++++++ flit_core/pyproject.toml | 2 +- 2 files changed, 8 insertions(+), 1 deletion(-) rename flit_core/{bootstrap.py => bootstrap_install.py} (82%) diff --git a/flit_core/bootstrap.py b/flit_core/bootstrap_install.py similarity index 82% rename from flit_core/bootstrap.py rename to flit_core/bootstrap_install.py index 2c6c3d80..e04a007e 100644 --- a/flit_core/bootstrap.py +++ b/flit_core/bootstrap_install.py @@ -3,6 +3,13 @@ Normally, you would install flit_core with pip like any other Python package. This script is meant to help with 'bootstrapping' other packaging systems, where you may need flit_core to build other packaging tools. + +Use 'python -m flit_core.wheel' to make a wheel, then: + + python bootstrap_install.py flit_core-3.6.0-py3-none-any.whl + +To install for something other than the Python running the script, pass a +site-packages or equivalent directory with the --installdir option. """ import argparse import sys diff --git a/flit_core/pyproject.toml b/flit_core/pyproject.toml index 03771248..3652f9a3 100644 --- a/flit_core/pyproject.toml +++ b/flit_core/pyproject.toml @@ -21,4 +21,4 @@ dynamic = ["version"] Source = "https://github.com/pypa/flit" [tool.flit.sdist] -include = ["bootstrap.py"] +include = ["bootstrap_install.py", "build_dists.py"] From 17df326a26b2b6466e8b8aa4789e5d8e22579757 Mon Sep 17 00:00:00 2001 From: Thomas Kluyver Date: Wed, 16 Feb 2022 12:03:45 +0000 Subject: [PATCH 4/5] Add test for python -m flit_core.wheel --- flit_core/flit_core/tests/test_wheel.py | 11 ++++++++++- flit_core/flit_core/wheel.py | 4 ++-- 2 files changed, 12 insertions(+), 3 deletions(-) diff --git a/flit_core/flit_core/tests/test_wheel.py b/flit_core/flit_core/tests/test_wheel.py index 9bb38fb2..882ae66a 100644 --- a/flit_core/flit_core/tests/test_wheel.py +++ b/flit_core/flit_core/tests/test_wheel.py @@ -3,7 +3,7 @@ from testpath import assert_isfile -from flit_core.wheel import make_wheel_in +from flit_core.wheel import make_wheel_in, main samples_dir = Path(__file__).parent / 'samples' @@ -29,3 +29,12 @@ def test_zero_timestamp(tmp_path, monkeypatch): # Minimum value for zip timestamps is 1980-1-1 with ZipFile(info.file, 'r') as zf: assert zf.getinfo('module1a.py').date_time == (1980, 1, 1, 0, 0, 0) + + +def test_main(tmp_path): + main(['--outdir', str(tmp_path), str(samples_dir / 'pep621')]) + wheels = list(tmp_path.glob('*.whl')) + assert len(wheels) == 1 + # Minimum value for zip timestamps is 1980-1-1 + with ZipFile(wheels[0], 'r') as zf: + assert 'module1a.py' in zf.namelist() diff --git a/flit_core/flit_core/wheel.py b/flit_core/flit_core/wheel.py index df228ecd..8c3150ab 100644 --- a/flit_core/flit_core/wheel.py +++ b/flit_core/flit_core/wheel.py @@ -219,7 +219,7 @@ def make_wheel_in(ini_path, wheel_directory, editable=False): return SimpleNamespace(builder=wb, file=wheel_path) -def main(): +def main(argv=None): parser = argparse.ArgumentParser() parser.add_argument( 'srcdir', @@ -234,7 +234,7 @@ def main(): '-o', help='output directory (defaults to {srcdir}/dist)', ) - args = parser.parse_args() + args = parser.parse_args(argv) outdir = args.srcdir / 'dist' if args.outdir is None else Path(args.outdir) print("Building wheel from", args.srcdir) pyproj_toml = args.srcdir / 'pyproject.toml' From 6b5d64439bb299fc4875b7a7179c1fb613320800 Mon Sep 17 00:00:00 2001 From: Thomas Kluyver Date: Thu, 17 Feb 2022 15:35:03 +0000 Subject: [PATCH 5/5] Update bootstrapping doc --- doc/bootstrap.rst | 33 +++++++++++++++++++-------------- 1 file changed, 19 insertions(+), 14 deletions(-) diff --git a/doc/bootstrap.rst b/doc/bootstrap.rst index 02da8945..19d7d923 100644 --- a/doc/bootstrap.rst +++ b/doc/bootstrap.rst @@ -13,24 +13,29 @@ from source? The key piece is ``flit_core``. This is a package which can build itself using nothing except Python and the standard library. From an unpacked source archive, -you can run ``python build_dists.py``, of which the crucial part is:: +you can make a wheel by running:: - from flit_core import buildapi - whl_fname = buildapi.build_wheel('dist/') - print(os.path.join('dist', whl_fname)) + python -m flit_core.wheel -This produces a ``.whl`` wheel file, which you can unzip into your -``site-packages`` folder (or equivalent) to make ``flit_core`` available for -building other packages. (You could also just copy ``flit_core`` from the -source directory, but without the ``.dist-info`` folder, tools like pip won't -know that it's installed.) +And then you can install this wheel with the ``bootstrap_install.py`` script +included in the sdist (or by unzipping it to the correct directory):: + + # Install to site-packages for this Python: + python bootstrap_install.py dist/flit_core-*.whl + + # Install somewhere else: + python bootstrap_install.py --installdir /path/to/site-packages dist/flit_core-*.whl As of version 3.6, flit_core bundles the ``tomli`` TOML parser, to avoid a dependency cycle. If you need to unbundle it, you will need to special-case installing flit_core and/or tomli to get around that cycle. -I recommend that you get the `build `_ and -`installer `_ packages (and their -dependencies) installed as the goal of the bootstrapping phase. These tools -together can be used to install any other Python packages: ``build`` to create -wheels and ``installer`` to install them. +After ``flit_core``, I recommend that you get `installer +`_ set up. You can use +``python -m flit_core.wheel`` again to make a wheel, and then use installer +itself (from the source directory) to install it. + +After that, you probably want to get `build `_ +and its dependencies installed as the goal of the bootstrapping phase. You can +then use ``build`` to create wheels of any other Python packages, and +``installer`` to install them.