From 6f1205a4b4f066d7315cff01d46d2b831f46d7fa Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Filipe=20La=C3=ADns?= Date: Sun, 24 Oct 2021 02:35:10 +0100 Subject: [PATCH] bootstrap: add initial implementation MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Has some rough edges, but seems to be working. Signed-off-by: Filipe LaĆ­ns --- bootstrap/__init__.py | 126 ++++++++++++++++++++++++++++++++++++++++++ bootstrap/build.py | 64 +++++++++++++++++++++ bootstrap/install.py | 12 ++++ 3 files changed, 202 insertions(+) create mode 100644 bootstrap/__init__.py create mode 100644 bootstrap/build.py create mode 100644 bootstrap/install.py diff --git a/bootstrap/__init__.py b/bootstrap/__init__.py new file mode 100644 index 0000000..9344b37 --- /dev/null +++ b/bootstrap/__init__.py @@ -0,0 +1,126 @@ +# SPDX-License-Identifier: MIT + +import logging +import os +import pathlib +import subprocess +import sys +import tempfile + +from collections.abc import Iterable, Mapping, Sequence +from typing import NamedTuple, Optional, Tuple, Union + + +class Package(NamedTuple): + srcdir: pathlib.Path + module_path: pathlib.Path + + +ROOT = pathlib.Path(__file__).parent.parent +TMPDIR = ROOT / '.bootstrap' +EXTERNAL = ROOT / 'external' + +PACKAGES = { + # what we need + 'build': Package( + EXTERNAL / 'build', + EXTERNAL / 'build' / 'src', + ), + 'installer': Package( + EXTERNAL / 'installer', + EXTERNAL / 'installer' / 'src', + ), + # dependencies + 'setuptools': Package( + EXTERNAL / 'setuptools', + EXTERNAL / 'setuptools', + ), + 'flit': Package( + EXTERNAL / 'flit', + EXTERNAL / 'flit', + ), + 'flit_core': Package( + EXTERNAL / 'flit' / 'flit_core', + EXTERNAL / 'flit' / 'flit_core', + ), + 'wheel': Package( + EXTERNAL / 'wheel', + EXTERNAL / 'wheel', + ), + 'tomli': Package( + EXTERNAL / 'tomli', + EXTERNAL / 'tomli', + ), + 'pep517': Package( + EXTERNAL / 'pep517', + EXTERNAL / 'pep517', + ), +} + +EXTRA_PATH = [str(package.module_path) for package in PACKAGES.values()] +PACKAGE_PATH_ENV = { + 'PYTHONPATH': os.path.pathsep.join(EXTRA_PATH), +} + + +_logger = logging.getLogger(__name__) + + +# inject extra sources into sys.path and import what we need +sys.path = EXTRA_PATH + sys.path +import build # noqa: E402 +import build.env # noqa: E402 +import pep517 # noqa: E402 + + +# not needed after https://github.com/pypa/build/pull/361 +def create_isolated_env_venv_no_pip(path: str) -> Tuple[str, str]: + import venv + + venv.EnvBuilder(symlinks=build.env._fs_supports_symlink()).create(path) + executable, script_dir, purelib = build.env._find_executable_and_scripts(path) + + return executable, script_dir + + +def custom_runner( + cmd: Sequence[str], + cwd: Optional[str] = None, + extra_environ: Optional[Mapping[str, str]] = None, +) -> None: + extra_environ = dict(extra_environ) if extra_environ else {} + extra_environ.update(PACKAGE_PATH_ENV) + pep517.default_subprocess_runner(cmd, cwd, extra_environ) + + +def build_package(name: str, outdir: pathlib.Path) -> pathlib.Path: + _logger.info(f'Building {name}...') + srcdir = PACKAGES[name].srcdir + builder = build.ProjectBuilder(srcdir, runner=custom_runner) + wheel = builder.build('wheel', str(outdir)) + return pathlib.Path(wheel) + + +def build_package_setuptools( + outdir: pathlib.Path, + wheel_whl: pathlib.Path, +) -> pathlib.Path: + # setuptools needs wheel installed + _logger.info('Building setuptools...') + srcdir = PACKAGES['setuptools'].srcdir + TMPDIR.mkdir(exist_ok=True) + with tempfile.TemporaryDirectory(prefix='setuptools-venv-', dir=TMPDIR) as venv_path: + executable, scripts_dir = create_isolated_env_venv_no_pip(venv_path) + + # install wheel + subprocess.check_call( + [executable, '-m', 'installer', str(wheel_whl)], + env=os.environ | PACKAGE_PATH_ENV, + ) + + # build setuptools + builder = build.ProjectBuilder(srcdir, runner=custom_runner) + builder.python_executable = executable + builder.scripts_dir = scripts_dir + wheel = builder.build('wheel', str(outdir)) + return pathlib.Path(wheel) diff --git a/bootstrap/build.py b/bootstrap/build.py new file mode 100644 index 0000000..fb5d0d3 --- /dev/null +++ b/bootstrap/build.py @@ -0,0 +1,64 @@ +# SPDX-License-Identifier: MIT + +import argparse +import json +import logging +import pathlib +import shutil +import sys + +from typing import Dict, Optional +from collections.abc import Sequence + +import bootstrap + + +def main_parser() -> argparse.ArgumentParser: + parser = argparse.ArgumentParser() + parser.add_argument( + '--outdir', + '-o', + type=str, + default='dist', + help='output directory (defaults to `dist`)', + ) + return parser + + +def main(cli_args: Sequence[str], prog: Optional[str] = None): + parser = main_parser() + if prog: + parser.prog = prog + args, unknown = parser.parse_known_args(cli_args) + + logging.basicConfig(level=logging.INFO) + + outdir = pathlib.Path(args.outdir).absolute() + if outdir.exists(): + if not outdir.is_dir(): + raise NotADirectoryError(f"{str(outdir)} exists and it's not a directory") + shutil.rmtree(outdir) + outdir.mkdir(parents=True) + + artifacts: Dict[str, pathlib.Path] = {} + # build wheel first because setuptools needs it installed + artifacts['wheel'] = bootstrap.build_package('wheel', outdir) + # build setuptools via the custom builder + bootstrap.build_package_setuptools(outdir, artifacts['wheel']) + # build the rest + for package in bootstrap.PACKAGES: + if package in ('wheel', 'setuptools'): + continue + artifacts[package] = bootstrap.build_package(package, outdir) + # write artifact metadata + outdir.joinpath('artifacts.json').write_text(json.dumps({ + package: path.name for package, path in artifacts.items() + })) + + print(f'Written wheels to `{str(args.outdir)}`:' + ''.join(sorted( + f'\n\t{artifact.name}' for artifact in artifacts.values() + ))) + + +if __name__ == '__main__': + main(sys.argv[1:], 'python -m bootstra.build') diff --git a/bootstrap/install.py b/bootstrap/install.py new file mode 100644 index 0000000..63753d6 --- /dev/null +++ b/bootstrap/install.py @@ -0,0 +1,12 @@ +# SPDX-License-Identifier: MIT + +import sys + +import bootstrap # noqa: F401 + +# bootstrap injects the extra sources +import installer.__main__ # noqa: E402 + + +if __name__ == '__main__': + installer.__main__.main(sys.argv[1:], 'python -m bootstrap.install')