From 7d38d664896579893b6f1ea534d53d7a5642bf66 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Gina=20H=C3=A4u=C3=9Fge?= Date: Tue, 29 Sep 2015 13:39:08 +0200 Subject: [PATCH] PipUtil now uses --user argument with pip install if detected as necessary --- src/octoprint/util/pip.py | 208 +++++++++++++++------ src/octoprint/util/piptestballoon/setup.py | 30 +++ 2 files changed, 180 insertions(+), 58 deletions(-) create mode 100644 src/octoprint/util/piptestballoon/setup.py diff --git a/src/octoprint/util/pip.py b/src/octoprint/util/pip.py index 8d0a008f..ccf8796c 100644 --- a/src/octoprint/util/pip.py +++ b/src/octoprint/util/pip.py @@ -9,6 +9,7 @@ __copyright__ = "Copyright (C) 2015 The OctoPrint Project - Released under terms import sarge import sys import logging +import re import pkg_resources @@ -34,6 +35,8 @@ class PipCaller(CommandlineCaller): self._version = None self._version_string = None self._use_sudo = False + self._use_user = False + self._install_dir = None self.trigger_refresh() @@ -41,6 +44,14 @@ class PipCaller(CommandlineCaller): self.on_log_stdout = lambda *args, **kwargs: None self.on_log_stderr = lambda *args, **kwargs: None + def _reset(self): + self._command = None + self._version = None + self._version_string = None + self._use_sudo = False + self._use_user = False + self._install_dir = None + def __le__(self, other): return self.version is not None and self.version <= other @@ -65,17 +76,26 @@ class PipCaller(CommandlineCaller): def version_string(self): return self._version_string + @property + def install_dir(self): + return self._install_dir + @property def use_sudo(self): return self._use_sudo + @property + def use_user(self): + return self._use_user + @property def available(self): return self._command is not None def trigger_refresh(self): + self._reset() try: - self._command, self._version, self._version_string, self._use_sudo = self._find_pip() + self._setup_pip() except: self._logger.exception("Error while discovering pip command") self._command = None @@ -97,13 +117,15 @@ class PipCaller(CommandlineCaller): if self.version in self.__class__.no_use_wheel and not "--no-use-wheel" in arg_list: self._logger.debug("Version {} needs --no-use-wheel to properly work.".format(self.version)) arg_list.append("--no-use-wheel") + if self.use_user: + arg_list.append("--user") command = [self._command] + list(args) if self._use_sudo: command = ["sudo"] + command return self.call(command) - def _find_pip(self): + def _setup_pip(self): pip_command = self.configured if pip_command is not None and pip_command.startswith("sudo "): @@ -112,75 +134,145 @@ class PipCaller(CommandlineCaller): else: pip_sudo = False - pip_version = None - version_segment = None + if pip_command is None: + pip_command = self._autodetect_pip() if pip_command is None: - import os - python_command = sys.executable - binary_dir = os.path.dirname(python_command) + return - pip_command = os.path.join(binary_dir, "pip") - if sys.platform == "win32": - # Windows is a bit special... first of all the file will be called pip.exe, not just pip, and secondly - # for a non-virtualenv install (e.g. global install) the pip binary will not be located in the - # same folder as python.exe, but in a subfolder Scripts, e.g. - # - # C:\Python2.7\ - # |- python.exe - # `- Scripts - # `- pip.exe + # Determine the pip version - # virtual env? - pip_command = os.path.join(binary_dir, "pip.exe") + self._logger.debug("Found pip at {}, going to figure out its version".format(pip_command)) - if not os.path.isfile(pip_command): - # nope, let's try the Scripts folder then - scripts_dir = os.path.join(binary_dir, "Scripts") - if os.path.isdir(scripts_dir): - pip_command = os.path.join(scripts_dir, "pip.exe") + pip_version, version_segment = self._get_pip_version(pip_command, pip_sudo) + if pip_version is None: + return - if not os.path.isfile(pip_command) or not os.access(pip_command, os.X_OK): - pip_command = None + if pip_version in self.__class__.broken: + self._logger.error("This version of pip is known to have errors that make it incompatible with how it needs to be used by OctoPrint. Please upgrade your pip version.") + return - if pip_command is not None: - self._logger.debug("Found pip at {}, going to figure out its version".format(pip_command)) + self._logger.info("Version of pip at {} is {}".format(pip_command, version_segment)) - sarge_command = [pip_command, "--version"] - if pip_sudo: - sarge_command = ["sudo"] + sarge_command + # Now figure out if pip belongs to a virtual environment and if the + # default installation directory is writable. + # + # The idea is the following: If OctoPrint is installed globally, + # the site-packages folder is probably not writable by our user. + # However, the user site-packages folder as usable by providing the + # --user parameter during install is. This we may not use though if + # the provided pip belongs to a virtual env (since that hiccups hard). + # + # So we figure out the installation directory, check if it's writable + # and if not if pip belongs to a virtual environment. Only if the + # installation directory is NOT writable by us but we also don't run + # in a virtual environment may we proceed with the --user parameter. + + ok, pip_user, pip_install_dir = self._check_pip_setup(pip_command) + if not ok: + self._logger.error("Pip install directory {} is not writable and is part of a virtual environment, can't use this constellation".format(pip_install_dir)) + return + + self._logger.info("pip at {} installs to {}, --user flag needed => {}".format(pip_command, pip_install_dir, "yes" if pip_user else "no")) + + self._command = pip_command + self._version = pip_version + self._version_string = version_segment + self._use_sudo = pip_sudo + self._use_user = pip_user + self._install_dir = pip_install_dir + + def _autodetect_pip(self): + import os + python_command = sys.executable + binary_dir = os.path.dirname(python_command) + + pip_command = os.path.join(binary_dir, "pip") + if sys.platform == "win32": + # Windows is a bit special... first of all the file will be called pip.exe, not just pip, and secondly + # for a non-virtualenv install (e.g. global install) the pip binary will not be located in the + # same folder as python.exe, but in a subfolder Scripts, e.g. + # + # C:\Python2.7\ + # |- python.exe + # `- Scripts + # `- pip.exe + + # virtual env? + pip_command = os.path.join(binary_dir, "pip.exe") + + if not os.path.isfile(pip_command): + # nope, let's try the Scripts folder then + scripts_dir = os.path.join(binary_dir, "Scripts") + if os.path.isdir(scripts_dir): + pip_command = os.path.join(scripts_dir, "pip.exe") + + if not os.path.isfile(pip_command) or not os.access(pip_command, os.X_OK): + pip_command = None + + return pip_command + + def _get_pip_version(self, pip_command, pip_sudo): + sarge_command = [pip_command, "--version"] + if pip_sudo: + sarge_command = ["sudo"] + sarge_command + p = sarge.run(sarge_command, stdout=sarge.Capture(), stderr=sarge.Capture()) + + if p.returncode != 0: + self._logger.warn("Error while trying to run pip --version: {}".format(p.stderr.text)) + return None, None + + output = p.stdout.text + # output should look something like this: + # + # pip from () + # + # we'll just split on whitespace and then try to use the second entry + + if not output.startswith("pip"): + self._logger.warn("pip command returned unparseable output, can't determine version: {}".format(output)) + + split_output = map(lambda x: x.strip(), output.split()) + if len(split_output) < 2: + self._logger.warn("pip command returned unparseable output, can't determine version: {}".format(output)) + + version_segment = split_output[1] + + try: + pip_version = pkg_resources.parse_version(version_segment) + except: + self._logger.exception("Error while trying to parse version string from pip command") + return None, None + + return pip_version, version_segment + + pip_install_dir_regex = re.compile("^\s*!!! PIP_INSTALL_DIR=(.*)\s*$", re.MULTILINE) + pip_virtual_env_regex = re.compile("^\s*!!! PIP_VIRTUAL_ENV=(True|False)\s*$", re.MULTILINE) + pip_writable_regex = re.compile("^\s*!!! PIP_WRITABLE=(True|False)\s*$", re.MULTILINE) + + def _check_pip_setup(self, pip_command): + import os + testballoon = os.path.join(os.path.realpath(os.path.dirname(__file__)), "piptestballoon") + + sarge_command = [pip_command, "install", testballoon, "--verbose"] + try: p = sarge.run(sarge_command, stdout=sarge.Capture(), stderr=sarge.Capture()) - if p.returncode != 0: - self._logger.warn("Error while trying to run pip --version: {}".format(p.stderr.text)) - pip_command = None - output = p.stdout.text - # output should look something like this: - # - # pip from () - # - # we'll just split on whitespace and then try to use the second entry + install_dir_match = self.__class__.pip_install_dir_regex.search(output) + virtual_env_match = self.__class__.pip_virtual_env_regex.search(output) + writable_match = self.__class__.pip_writable_regex.search(output) - if not output.startswith("pip"): - self._logger.warn("pip command returned unparseable output, can't determine version: {}".format(output)) + if install_dir_match and virtual_env_match and writable_match: + install_dir = install_dir_match.group(1) + virtual_env = virtual_env_match.group(1) == "True" + writable = writable_match.group(1) == "True" - split_output = map(lambda x: x.strip(), output.split()) - if len(split_output) < 2: - self._logger.warn("pip command returned unparseable output, can't determine version: {}".format(output)) - - version_segment = split_output[1] - - try: - pip_version = pkg_resources.parse_version(version_segment) - except: - self._logger.exception("Error while trying to parse version string from pip command") - return None, None, None + return writable or not virtual_env, not writable and not virtual_env, install_dir else: - self._logger.info("Found pip at {}, version is {}".format(pip_command, version_segment)) + return False, False, None - if pip_version in self.__class__.broken: - self._logger.error("This version of pip is known to have errors that make it incompatible with how it needs to be used by OctoPrint. Please upgrade your pip version.") - return None, None, None, False + finally: + sarge_command = [pip_command, "uninstall", "-y", "OctoPrint-PipTestBalloon"] + sarge.run(sarge_command, stdout=sarge.Capture(), stderr=sarge.Capture()) - return pip_command, pip_version, version_segment, pip_sudo diff --git a/src/octoprint/util/piptestballoon/setup.py b/src/octoprint/util/piptestballoon/setup.py new file mode 100644 index 00000000..7e0036b7 --- /dev/null +++ b/src/octoprint/util/piptestballoon/setup.py @@ -0,0 +1,30 @@ +from setuptools import setup + +def run_checks(): + from distutils.command.install import install as cmd_install + from distutils.dist import Distribution + import sys + import os + + cmd = cmd_install(Distribution()) + cmd.finalize_options() + + install_dir = cmd.install_lib + virtual_env = hasattr(sys, "real_prefix") + writable = os.access(install_dir, os.W_OK) + + print("!!! PIP_INSTALL_DIR={}".format(install_dir)) + print("!!! PIP_VIRTUAL_ENV={}".format(virtual_env)) + print("!!! PIP_WRITABLE={}".format(writable)) + sys.stdout.flush() + +def parameters(): + run_checks() + + return dict( + name="OctoPrint-PipTestBalloon", + version="1.0", + description="Just a test balloon to check a couple of pip related settings" + ) + +setup(**parameters())