summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorBernát Gábor <bgabor8@bloomberg.net>2020-02-24 19:08:32 +0000
committerGitHub <noreply@github.com>2020-02-24 19:08:32 +0000
commit9201422a7b2f61e1bcc836f80860d11daa84c507 (patch)
tree121fd228dfc7dbf5655d15a83f3b51f6905591fe
parentef711b75ed8947e63f0d1e21ef34928239b8e545 (diff)
downloadvirtualenv-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.py5
-rw-r--r--src/virtualenv/create/via_global_ref/_distutils_patch_virtualenv.py49
-rw-r--r--src/virtualenv/create/via_global_ref/api.py22
-rw-r--r--src/virtualenv/create/via_global_ref/builtin/via_global_self_do.py1
-rw-r--r--src/virtualenv/create/via_global_ref/venv.py5
-rw-r--r--tests/unit/create/console_app/demo/__init__.py6
-rw-r--r--tests/unit/create/console_app/demo/__main__.py6
-rw-r--r--tests/unit/create/console_app/setup.cfg15
-rw-r--r--tests/unit/create/console_app/setup.py3
-rw-r--r--tests/unit/create/test_creator.py43
-rw-r--r--tests/unit/seed/test_boostrap_link_via_app_data.py5
11 files changed, 156 insertions, 4 deletions
diff --git a/setup.py b/setup.py
index 0f0ab7a..18642d6 100644
--- a/setup.py
+++ b/setup.py
@@ -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")):