diff options
| author | Bernát Gábor <bgabor8@bloomberg.net> | 2020-02-24 19:08:32 +0000 |
|---|---|---|
| committer | GitHub <noreply@github.com> | 2020-02-24 19:08:32 +0000 |
| commit | 9201422a7b2f61e1bcc836f80860d11daa84c507 (patch) | |
| tree | 121fd228dfc7dbf5655d15a83f3b51f6905591fe | |
| parent | ef711b75ed8947e63f0d1e21ef34928239b8e545 (diff) | |
| download | virtualenv-9201422a7b2f61e1bcc836f80860d11daa84c507.tar.gz | |
Ensure distutils configuration values do not escape virtual environment (#1657)
* Ensure distutils configuration values do not escape virtual environment
Distutils has some configuration files where the user may alter paths to
point outside of the virtual environment. Defend against this by
installing a pth file that resets this to their expected path.
Signed-off-by: Bernat Gabor <bgabor8@bloomberg.net>
* fix CI failure due to #pypa/pip/issues/7778
Signed-off-by: Bernat Gabor <bgabor8@bloomberg.net>
| -rw-r--r-- | setup.py | 5 | ||||
| -rw-r--r-- | src/virtualenv/create/via_global_ref/_distutils_patch_virtualenv.py | 49 | ||||
| -rw-r--r-- | src/virtualenv/create/via_global_ref/api.py | 22 | ||||
| -rw-r--r-- | src/virtualenv/create/via_global_ref/builtin/via_global_self_do.py | 1 | ||||
| -rw-r--r-- | src/virtualenv/create/via_global_ref/venv.py | 5 | ||||
| -rw-r--r-- | tests/unit/create/console_app/demo/__init__.py | 6 | ||||
| -rw-r--r-- | tests/unit/create/console_app/demo/__main__.py | 6 | ||||
| -rw-r--r-- | tests/unit/create/console_app/setup.cfg | 15 | ||||
| -rw-r--r-- | tests/unit/create/console_app/setup.py | 3 | ||||
| -rw-r--r-- | tests/unit/create/test_creator.py | 43 | ||||
| -rw-r--r-- | tests/unit/seed/test_boostrap_link_via_app_data.py | 5 |
11 files changed, 156 insertions, 4 deletions
@@ -5,5 +5,8 @@ if int(__version__.split(".")[0]) < 41: setup( use_scm_version={"write_to": "src/virtualenv/version.py", "write_to_template": '__version__ = "{version}"'}, - setup_requires=["setuptools_scm >= 2"], + setup_requires=[ + # this cannot be enabled until https://github.com/pypa/pip/issues/7778 is addressed + # "setuptools_scm >= 2" + ], ) diff --git a/src/virtualenv/create/via_global_ref/_distutils_patch_virtualenv.py b/src/virtualenv/create/via_global_ref/_distutils_patch_virtualenv.py new file mode 100644 index 0000000..d963c43 --- /dev/null +++ b/src/virtualenv/create/via_global_ref/_distutils_patch_virtualenv.py @@ -0,0 +1,49 @@ +# -*- coding: utf-8 -*- +""" +Distutils allows user to configure some arguments via a configuration file: +https://docs.python.org/3/install/index.html#distutils-configuration-files + +Some of this arguments though don't make sense in context of the virtual environment files, let's fix them up. +""" +import os +import sys + +VIRTUALENV_PATCH_FILE = os.path.join(__file__) + + +def patch(dist_of): + # we cannot allow the prefix override as that would get packages installed outside of the virtual environment + old_parse_config_files = dist_of.Distribution.parse_config_files + + def parse_config_files(self, *args, **kwargs): + result = old_parse_config_files(self, *args, **kwargs) + install_dict = self.get_option_dict("install") + + if "prefix" in install_dict: # the prefix governs where to install the libraries + install_dict["prefix"] = VIRTUALENV_PATCH_FILE, os.path.abspath(sys.prefix) + + if "install_scripts" in install_dict: # the install_scripts governs where to generate console scripts + script_path = os.path.abspath(os.path.join(os.path.dirname(__file__), "__SCRIPT_DIR__")) + install_dict["install_scripts"] = VIRTUALENV_PATCH_FILE, script_path + + return result + + dist_of.Distribution.parse_config_files = parse_config_files + + +def run(): + # patch distutils + from distutils import dist + + patch(dist) + + # patch setuptools (that has it's own copy of the dist package) + try: + from setuptools import dist + except ImportError: + pass # if setuptools is not around that's alright, just don't patch + else: + patch(dist) + + +run() diff --git a/src/virtualenv/create/via_global_ref/api.py b/src/virtualenv/create/via_global_ref/api.py index 1fc9999..33c606b 100644 --- a/src/virtualenv/create/via_global_ref/api.py +++ b/src/virtualenv/create/via_global_ref/api.py @@ -1,9 +1,14 @@ from __future__ import absolute_import, unicode_literals +import logging +import os from abc import ABCMeta from six import add_metaclass +from virtualenv.util.path import Path +from virtualenv.util.zipapp import ensure_file_on_disk + from ..creator import Creator @@ -43,6 +48,23 @@ class ViaGlobalRefApi(Creator): help="try to use copies rather than symlinks, even when symlinks are the default for the platform", ) + def create(self): + self.patch_distutils_via_pth() + + def patch_distutils_via_pth(self): + """Patch the distutils package to not be derailed by its configuration files""" + patch_file = Path(__file__).parent / "_distutils_patch_virtualenv.py" + with ensure_file_on_disk(patch_file) as resolved_path: + text = resolved_path.read_text() + text = text.replace('"__SCRIPT_DIR__"', repr(os.path.relpath(str(self.script_dir), str(self.purelib)))) + patch_path = self.purelib / "_distutils_patch_virtualenv.py" + logging.debug("add distutils patch file %s", patch_path) + patch_path.write_text(text) + + pth = self.purelib / "_distutils_patch_virtualenv.pth" + logging.debug("add distutils patch file %s", pth) + pth.write_text("import _distutils_patch_virtualenv") + def _args(self): return super(ViaGlobalRefApi, self)._args() + [("global", self.enable_system_site_package)] diff --git a/src/virtualenv/create/via_global_ref/builtin/via_global_self_do.py b/src/virtualenv/create/via_global_ref/builtin/via_global_self_do.py index bc1cc44..922a74d 100644 --- a/src/virtualenv/create/via_global_ref/builtin/via_global_self_do.py +++ b/src/virtualenv/create/via_global_ref/builtin/via_global_self_do.py @@ -79,6 +79,7 @@ class ViaGlobalRefVirtualenvBuiltin(ViaGlobalRefApi, VirtualenvBuiltin): finally: if true_system_site != self.enable_system_site_package: self.enable_system_site_package = true_system_site + super(ViaGlobalRefVirtualenvBuiltin, self).create() def ensure_directories(self): return {self.dest, self.bin_dir, self.script_dir, self.stdlib} | set(self.libs) diff --git a/src/virtualenv/create/via_global_ref/venv.py b/src/virtualenv/create/via_global_ref/venv.py index 56b4b91..0cbf1d1 100644 --- a/src/virtualenv/create/via_global_ref/venv.py +++ b/src/virtualenv/create/via_global_ref/venv.py @@ -38,9 +38,12 @@ class Venv(ViaGlobalRefApi): self.create_inline() else: self.create_via_sub_process() - # TODO: cleanup activation scripts + + # TODO: cleanup activation scripts + for lib in self.libs: ensure_dir(lib) + super(Venv, self).create() def create_inline(self): from venv import EnvBuilder diff --git a/tests/unit/create/console_app/demo/__init__.py b/tests/unit/create/console_app/demo/__init__.py new file mode 100644 index 0000000..a7e1f5a --- /dev/null +++ b/tests/unit/create/console_app/demo/__init__.py @@ -0,0 +1,6 @@ +def run(): + print("magic") + + +if __name__ == "__main__": + run() diff --git a/tests/unit/create/console_app/demo/__main__.py b/tests/unit/create/console_app/demo/__main__.py new file mode 100644 index 0000000..a7e1f5a --- /dev/null +++ b/tests/unit/create/console_app/demo/__main__.py @@ -0,0 +1,6 @@ +def run(): + print("magic") + + +if __name__ == "__main__": + run() diff --git a/tests/unit/create/console_app/setup.cfg b/tests/unit/create/console_app/setup.cfg new file mode 100644 index 0000000..abf82e0 --- /dev/null +++ b/tests/unit/create/console_app/setup.cfg @@ -0,0 +1,15 @@ +[metadata] +name = demo +version = 1.0.0 +description = magic package + +[options] +packages = find: +install_requires = + +[options.entry_points] +console_scripts = + magic=demo.__main__:run + +[bdist_wheel] +universal = true diff --git a/tests/unit/create/console_app/setup.py b/tests/unit/create/console_app/setup.py new file mode 100644 index 0000000..6068493 --- /dev/null +++ b/tests/unit/create/console_app/setup.py @@ -0,0 +1,3 @@ +from setuptools import setup + +setup() diff --git a/tests/unit/create/test_creator.py b/tests/unit/create/test_creator.py index f68d32d..e50ba7c 100644 --- a/tests/unit/create/test_creator.py +++ b/tests/unit/create/test_creator.py @@ -4,10 +4,12 @@ import difflib import gc import logging import os +import shutil import stat import subprocess import sys from itertools import product +from textwrap import dedent from threading import Thread import pytest @@ -131,7 +133,10 @@ def test_create_no_seed(python, creator, isolated, system, coverage_env, special # pypy cleans up file descriptors periodically so our (many) subprocess calls impact file descriptor limits # force a cleanup of these on system where the limit is low-ish (e.g. MacOS 256) gc.collect() - content = list(result.creator.purelib.iterdir()) + purelib = result.creator.purelib + patch_files = {purelib / "{}.{}".format("_distutils_patch_virtualenv", i) for i in ("py", "pyc", "pth")} + patch_files.add(purelib / "__pycache__") + content = set(result.creator.purelib.iterdir()) - patch_files assert not content, "\n".join(ensure_text(str(i)) for i in content) assert result.creator.env_name == ensure_text(dest.name) debug = result.creator.debug @@ -345,3 +350,39 @@ def test_create_long_path(current_fastest, tmp_path): cmd = [str(folder)] result = cli_run(cmd) subprocess.check_call([str(result.creator.script("pip")), "--version"]) + + +@pytest.mark.parametrize("creator", set(PythonInfo.current_system().creators().key_to_class) - {"builtin"}) +def test_create_distutils_cfg(creator, tmp_path, monkeypatch): + cmd = [ + ensure_text(str(tmp_path)), + "--activators", + "", + "--creator", + creator, + ] + result = cli_run(cmd) + + app = Path(__file__).parent / "console_app" + dest = tmp_path / "console_app" + shutil.copytree(str(app), str(dest)) + + setup_cfg = dest / "setup.cfg" + conf = dedent( + """ + [install] + prefix={}/a + install_scripts={}/b + """ + ).format(tmp_path, tmp_path) + setup_cfg.write_text(setup_cfg.read_text() + conf) + + monkeypatch.chdir(dest) # distutils will read the setup.cfg from the cwd, so change to that + install_demo_cmd = [str(result.creator.script("pip")), "install", str(dest), "--no-use-pep517"] + subprocess.check_call(install_demo_cmd) + + magic = result.creator.script("magic") # console scripts are created in the right location + assert magic.exists() + + package_folder = result.creator.platlib / "demo" # prefix is set to the virtualenv prefix for install + assert package_folder.exists() diff --git a/tests/unit/seed/test_boostrap_link_via_app_data.py b/tests/unit/seed/test_boostrap_link_via_app_data.py index 4019a46..436254b 100644 --- a/tests/unit/seed/test_boostrap_link_via_app_data.py +++ b/tests/unit/seed/test_boostrap_link_via_app_data.py @@ -93,7 +93,10 @@ def test_base_bootstrap_link_via_app_data(tmp_path, coverage_env, current_fastes assert not process.returncode # pip is greedy here, removing all packages removes the site-package too if site_package.exists(): - post_run = list(site_package.iterdir()) + purelib = result.creator.purelib + patch_files = {purelib / "{}.{}".format("_distutils_patch_virtualenv", i) for i in ("py", "pyc", "pth")} + patch_files.add(purelib / "__pycache__") + post_run = set(site_package.iterdir()) - patch_files assert not post_run, "\n".join(str(i) for i in post_run) if sys.version_info[0:2] == (3, 4) and os.environ.get(str("PIP_REQ_TRACKER")): |
