From 1a3a7d208fd48446de3d35e5c640378dc1dfd437 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Micha=C5=82=20G=C3=B3rny?= Date: Sun, 5 Jun 2022 12:08:44 +0200 Subject: [PATCH] main: Implement --prefix option to override install path Implement a --prefix option that can be used to override platbase and base when expanding the paths from install scheme. This can be used to install paths to a layout using another prefix than the one used to run installer. Gentoo use case for this is running installer inside a venv to prepare installation images for the real system. The nixOS maintainers have also indicated interest in this feature. --- src/installer/__main__.py | 23 ++++++++++++++++++----- tests/test_main.py | 30 ++++++++++++++++++++++++++++++ 2 files changed, 48 insertions(+), 5 deletions(-) diff --git a/src/installer/__main__.py b/src/installer/__main__.py index 3357ec5..51014b9 100644 --- a/src/installer/__main__.py +++ b/src/installer/__main__.py @@ -23,6 +23,13 @@ def _get_main_parser() -> argparse.ArgumentParser: type=str, help="destination directory (prefix to prepend to each file)", ) + parser.add_argument( + "--prefix", + "-p", + metavar="path", + type=str, + help="override prefix to install packages to", + ) parser.add_argument( "--compile-bytecode", action="append", @@ -39,12 +46,18 @@ def _get_main_parser() -> argparse.ArgumentParser: return parser -def _get_scheme_dict(distribution_name: str) -> Dict[str, str]: +def _get_scheme_dict( + distribution_name: str, prefix: Optional[str] = None +) -> Dict[str, str]: """Calculate the scheme dictionary for the current Python environment.""" - scheme_dict = sysconfig.get_paths() + vars = {} + if prefix is None: + installed_base = sysconfig.get_config_var("base") + assert installed_base + else: + vars["base"] = vars["platbase"] = installed_base = prefix - installed_base = sysconfig.get_config_var("base") - assert installed_base + scheme_dict = sysconfig.get_paths(vars=vars) # calculate 'headers' path, not currently in sysconfig - see # https://bugs.python.org/issue44445. This is based on what distutils does. @@ -72,7 +85,7 @@ def _main(cli_args: Sequence[str], program: Optional[str] = None) -> None: with WheelFile.open(args.wheel) as source: destination = SchemeDictionaryDestination( - scheme_dict=_get_scheme_dict(source.distribution), + scheme_dict=_get_scheme_dict(source.distribution, prefix=args.prefix), interpreter=sys.executable, script_kind=get_launcher_kind(), bytecode_optimization_levels=bytecode_levels, diff --git a/tests/test_main.py b/tests/test_main.py index c6f7c40..391a13d 100644 --- a/tests/test_main.py +++ b/tests/test_main.py @@ -1,3 +1,5 @@ +import os + from installer.__main__ import _get_scheme_dict as get_scheme_dict from installer.__main__ import _main as main @@ -7,6 +9,14 @@ def test_get_scheme_dict(): assert set(d.keys()) >= {"purelib", "platlib", "headers", "scripts", "data"} +def test_get_scheme_dict_prefix(): + d = get_scheme_dict(distribution_name="foo", prefix="/foo") + for key in ("purelib", "platlib", "headers", "scripts", "data"): + assert d[key].startswith( + f"{os.sep}foo" + ), f"{key} does not start with /foo: {d[key]}" + + def test_main(fancy_wheel, tmp_path): destdir = tmp_path / "dest" @@ -23,6 +33,26 @@ def test_main(fancy_wheel, tmp_path): } +def test_main_prefix(fancy_wheel, tmp_path): + destdir = tmp_path / "dest" + + main([str(fancy_wheel), "-d", str(destdir), "-p", "/foo"], "python -m installer") + + installed_py_files = list(destdir.rglob("*.py")) + + for f in installed_py_files: + assert str(f.parent).startswith( + str(destdir / "foo") + ), f"path does not respect destdir+prefix: {f}" + assert {f.stem for f in installed_py_files} == {"__init__", "__main__", "data"} + + installed_pyc_files = destdir.rglob("*.pyc") + assert {f.name.split(".")[0] for f in installed_pyc_files} == { + "__init__", + "__main__", + } + + def test_main_no_pyc(fancy_wheel, tmp_path): destdir = tmp_path / "dest"