diff --git a/src/octoprint/plugins/softwareupdate/__init__.py b/src/octoprint/plugins/softwareupdate/__init__.py index 779c6af0..1efdb064 100644 --- a/src/octoprint/plugins/softwareupdate/__init__.py +++ b/src/octoprint/plugins/softwareupdate/__init__.py @@ -122,6 +122,7 @@ class SoftwareUpdatePlugin(octoprint.plugin.BlueprintPlugin, "octoprint_restart_command": None, "environment_restart_command": None, + "pip_command": None, "cache_ttl": 24 * 60, } @@ -571,6 +572,8 @@ class SoftwareUpdatePlugin(octoprint.plugin.BlueprintPlugin, if "update_script" in check: return updaters.update_script elif "pip" in check: + if not "pip_command" in check and self._settings.get(["pip_command"]) is not None: + check["pip_command"] = self._settings.get(["pip_command"]) return updaters.pip elif "python_updater" in check: return updaters.python_updater diff --git a/src/octoprint/plugins/softwareupdate/updaters/pip.py b/src/octoprint/plugins/softwareupdate/updaters/pip.py index 3b1eeac4..c6f8e590 100644 --- a/src/octoprint/plugins/softwareupdate/updaters/pip.py +++ b/src/octoprint/plugins/softwareupdate/updaters/pip.py @@ -7,19 +7,19 @@ __copyright__ = "Copyright (C) 2014 The OctoPrint Project - Released under terms import logging +import sarge +import sys -try: - import pip as _pip -except: - _pip = None - +logger = logging.getLogger("octoprint.plugins.softwareupdate.updaters.pip") +console_logger = logging.getLogger("octoprint.plugins.softwareupdate.updaters.pip.console") def can_perform_update(target, check): - return "pip" in check and _pip is not None - + return "pip" in check def perform_update(target, check, target_version): - logger = logging.getLogger("octoprint.plugins.softwareupdate.updaters.pip") + pip_command = None + if "pip_command" in check: + pip_command = check["pip_command"] install_arg = check["pip"].format(target_version=target_version) @@ -27,15 +27,94 @@ def perform_update(target, check, target_version): pip_args = ["install", check["pip"].format(target_version=target_version, target=target_version)] if "dependency_links" in check and check["dependency_links"]: - pip_args += "--process-dependency-links" + pip_args += ["--process-dependency-links"] - _pip.main(pip_args) + _call_pip(pip_args, pip_command=pip_command) - if "force_reinstall" in check and check["force_reinstall"]: - # if force_reinstall is true, we need to install the package a second time, this time forcing its reinstall - # without forcing its dependencies too - logger.debug("Target. %s, executing pip install %s --ignore-reinstalled --force-reinstall --no-deps" % (target, install_arg)) - pip_args += ["--ignore-installed", "--force-reinstall", "--no-deps"] - _pip.main(pip_args) + logger.debug("Target. %s, executing pip install %s --ignore-reinstalled --force-reinstall --no-deps" % (target, install_arg)) + pip_args += ["--ignore-installed", "--force-reinstall", "--no-deps"] + _call_pip(pip_args, pip_command=pip_command) return "ok" + +def _call_pip(args, pip_command=None): + if pip_command is None: + 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): + raise RuntimeError(u"No pip path configured and {pip_command} does not exist or is not executable, can't install".format(**locals())) + + command = [pip_command] + args + + logger.debug(u"Calling: {}".format(" ".join(command))) + + p = sarge.run(" ".join(command), shell=True, async=True, stdout=sarge.Capture(), stderr=sarge.Capture()) + p.wait_events() + + all_stdout = [] + all_stderr = [] + try: + while p.returncode is None: + line = p.stderr.readline(timeout=0.5) + if line: + _log_stderr(line) + all_stderr.append(line) + + line = p.stdout.readline(timeout=0.5) + if line: + _log_stdout(line) + all_stdout.append(line) + + p.commands[0].poll() + + finally: + p.close() + + stderr = p.stderr.text + if stderr: + split_lines = stderr.split("\n") + _log_stderr(*split_lines) + all_stderr += split_lines + + stdout = p.stdout.text + if stdout: + split_lines = stdout.split("\n") + _log_stdout(*split_lines) + all_stdout += split_lines + + return p.returncode, all_stdout, all_stderr + +def _log_stdout(*lines): + _log(lines, prefix=">", stream="stdout") + +def _log_stderr(*lines): + _log(lines, prefix="!", stream="stderr") + +def _log(lines, prefix=None, stream=None, strip=True): + if strip: + lines = map(lambda x: x.strip(), lines) + for line in lines: + console_logger.debug(u"{prefix} {line}".format(**locals())) +