diff --git a/bootstrap/__init__.py b/bootstrap/__init__.py new file mode 100644 index 0000000..a41ea90 --- /dev/null +++ b/bootstrap/__init__.py @@ -0,0 +1,137 @@ +# SPDX-License-Identifier: MIT + +import atexit +import logging +import os +import pathlib +import shutil +import subprocess +import sys +import tempfile + +from collections.abc import Collection, Mapping, Sequence +from typing import NamedTuple, Optional, Tuple + + +class Package(NamedTuple): + srcdir: pathlib.Path + module_path: pathlib.Path + module_sources: Collection[str] + + +ROOT = pathlib.Path(__file__).parent.parent +WORKING_DIR = ROOT / '.bootstrap' +MODULES = WORKING_DIR / 'modules' +TMPDIR = WORKING_DIR / 'tmp' +EXTERNAL = ROOT / 'external' + +PACKAGES = { + # what we need + 'build': Package( + EXTERNAL / 'build', + EXTERNAL / 'build' / 'src', + {'build'}, + ), + 'installer': Package( + EXTERNAL / 'installer', + EXTERNAL / 'installer' / 'src', + {'installer'}, + ), + # dependencies + 'setuptools': Package( + EXTERNAL / 'setuptools', + EXTERNAL / 'setuptools', + {'setuptools', 'pkg_resources', '_distutils_hack'}, + ), + 'flit_core': Package( + EXTERNAL / 'flit' / 'flit_core', + EXTERNAL / 'flit' / 'flit_core', + {'flit_core'}, + ), + 'wheel': Package( + EXTERNAL / 'wheel', + EXTERNAL / 'wheel' / 'src', + {'wheel'}, + ), + 'tomli': Package( + EXTERNAL / 'tomli', + EXTERNAL / 'tomli', + {'tomli'}, + ), + 'pep517': Package( + EXTERNAL / 'pep517', + EXTERNAL / 'pep517', + {'pep517'}, + ), +} + +EXTRA_PATH = [str(package.module_path) for package in PACKAGES.values()] +PACKAGE_PATH_ENV = { + 'PYTHONPATH': os.path.pathsep.join(EXTRA_PATH), +} + + +# copy sources to module dir and inject it into sys.path +MODULES.mkdir(parents=True) +for package in PACKAGES.values(): + for path in package.module_sources: + shutil.copytree( + package.module_path / path, + MODULES / path, + ) +atexit.register(shutil.rmtree, MODULES) +sys.path.insert(0, str(MODULES)) + + +# import what we need from the inject modules +import build # noqa: E402 +import build.env # noqa: E402 +import pep517 # noqa: E402 + + +_logger = logging.getLogger(__name__) + + +def log(msg): + _logger.info(msg) + + +# 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 install_package_egginfo(name: str) -> None: + package = PACKAGES[name] + subprocess.check_call( + [sys.executable, 'setup.py', 'egg_info'], + env=os.environ | PACKAGE_PATH_ENV, + cwd=package.srcdir, + ) + shutil.copytree( + package.module_path / f'{name}.egg-info', + MODULES / f'{name}.egg-info', + ) + + +def build_package(name: str, outdir: pathlib.Path) -> pathlib.Path: + log(f'Building {name}...') + srcdir = PACKAGES[name].srcdir + builder = build.ProjectBuilder(str(srcdir), runner=custom_runner) + 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..cadedf6 --- /dev/null +++ b/bootstrap/build.py @@ -0,0 +1,63 @@ +# 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) + + # first we install the setuptools egg-info, then wheel do that setuptools + # thinks they are installed + bootstrap.install_package_egginfo('setuptools') + bootstrap.install_package_egginfo('wheel') + # then we can build everything + artifacts = { + package: bootstrap.build_package(package, outdir) + for package in bootstrap.PACKAGES + } + # and finally, we generate the 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')