PipUtil now uses --user argument with pip install if detected as necessary

This commit is contained in:
Gina Häußge 2015-09-29 13:39:08 +02:00
parent 705f0706fd
commit 7d38d66489
2 changed files with 180 additions and 58 deletions

View file

@ -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 <version> from <path> (<python version>)
#
# 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 <version> from <path> (<python version>)
#
# 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

View file

@ -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())