PipUtil now uses --user argument with pip install if detected as necessary
This commit is contained in:
parent
705f0706fd
commit
7d38d66489
2 changed files with 180 additions and 58 deletions
|
|
@ -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
|
||||
|
|
|
|||
30
src/octoprint/util/piptestballoon/setup.py
Normal file
30
src/octoprint/util/piptestballoon/setup.py
Normal 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())
|
||||
Loading…
Reference in a new issue