From 1178fe9e95a77efafcbfbcefa94836cb1d0199f2 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Gina=20H=C3=A4u=C3=9Fge?= Date: Mon, 28 Sep 2015 19:53:30 +0200 Subject: [PATCH 01/19] Support sudo for installing plugins, but warn about it --- README.md | 16 ++++++++++++- .../plugins/pluginmanager/__init__.py | 9 ++++++- .../pluginmanager/static/js/pluginmanager.js | 16 ++++++++++++- .../templates/pluginmanager_settings.jinja2 | 24 ++++++++++++++++--- src/octoprint/util/pip.py | 22 ++++++++++++++--- 5 files changed, 78 insertions(+), 9 deletions(-) diff --git a/README.md b/README.md index 06d5b6b0..4d5a9ba0 100644 --- a/README.md +++ b/README.md @@ -38,11 +38,25 @@ For information about how to go about contributions of any kind, please see the Installation ------------ -Installation instructions for installing from source for different operating systems can be found [on the wiki](https://github.com/foosel/OctoPrint/wiki#assorted-guides). +Installation instructions for installing from source for different operating +systems can be found [on the wiki](https://github.com/foosel/OctoPrint/wiki#assorted-guides). If you want to run OctoPrint on a Raspberry Pi you might want to take a look at [OctoPi](https://github.com/guysoft/OctoPi) which is a custom SD card image that includes OctoPrint plus dependencies. +The generic steps that should basically be done regardless of operating system +and runtime environment are the following (as *regular +user*, please keep your hands *off* of the `sudo` command here!) - this assumes +you already have Python 2.7, pip and virtualenv set up: + +1. Checkout OctoPrint: `git clone https://github.com/foosel/OctoPrint.git` +2. Change into the OctoPrint folder: `cd OctoPrint` +3. Create a user-owned virtual environment therein: `virtualenv --system-site-packages venv` +4. Install OctoPrint *into that virtual environment*: `./venv/bin/python setup.py install` + +You may then start the OctoPrint server via `/path/to/OctoPrint/venv/bin/octoprint`, see [Usage](#usage) +for details. + After installation, please make sure you follow the first-run wizard and set up access control as necessary. If you want to not only be notified about new releases but also be able to automatically upgrade to them from within diff --git a/src/octoprint/plugins/pluginmanager/__init__.py b/src/octoprint/plugins/pluginmanager/__init__.py index 8390cd8e..a92fa91c 100644 --- a/src/octoprint/plugins/pluginmanager/__init__.py +++ b/src/octoprint/plugins/pluginmanager/__init__.py @@ -82,6 +82,7 @@ class PluginManagerPlugin(octoprint.plugin.SimpleApiPlugin, repository="http://plugins.octoprint.org/plugins.json", repository_ttl=24*60, pip=None, + pip_args=None, dependency_links=False, hidden=[] ) @@ -187,7 +188,9 @@ class PluginManagerPlugin(octoprint.plugin.SimpleApiPlugin, pip=dict( available=self._pip_caller.available, command=self._pip_caller.command, - version=str(self._pip_caller.version) + version=str(self._pip_caller.version), + use_sudo=self._pip_caller.use_sudo, + additional_args=self._settings.get(["pip_args"]) )) def on_api_command(self, command, data): @@ -447,6 +450,10 @@ class PluginManagerPlugin(octoprint.plugin.SimpleApiPlugin, if self._pip_caller < self._pip_version_dependency_links: args.remove("--process-dependency-links") + additional_args = self._settings.get(["pip_args"]) + if additional_args: + args.append(additional_args) + return self._pip_caller.execute(*args) def _log_message(self, *lines): diff --git a/src/octoprint/plugins/pluginmanager/static/js/pluginmanager.js b/src/octoprint/plugins/pluginmanager/static/js/pluginmanager.js index 35a634d8..75c8e2f3 100644 --- a/src/octoprint/plugins/pluginmanager/static/js/pluginmanager.js +++ b/src/octoprint/plugins/pluginmanager/static/js/pluginmanager.js @@ -9,6 +9,7 @@ $(function() { self.config_repositoryUrl = ko.observable(); self.config_repositoryTtl = ko.observable(); self.config_pipCommand = ko.observable(); + self.config_pipAdditionalArgs = ko.observable(); self.configurationDialog = $("#settings_plugin_pluginmanager_configurationdialog"); @@ -81,6 +82,8 @@ $(function() { self.pipAvailable = ko.observable(false); self.pipCommand = ko.observable(); self.pipVersion = ko.observable(); + self.pipUseSudo = ko.observable(); + self.pipAdditionalArgs = ko.observable(); self.working = ko.observable(false); self.workingTitle = ko.observable(); @@ -208,9 +211,13 @@ $(function() { if (data.available) { self.pipCommand(data.command); self.pipVersion(data.version); + self.pipUseSudo(data.use_sudo); + self.pipAdditionalArgs(data.additional_args); } else { self.pipCommand(undefined); self.pipVersion(undefined); + self.pipUseSudo(undefined); + self.pipAdditionalArgs(undefined); } }; @@ -392,12 +399,18 @@ $(function() { repositoryTtl = null; } + var pipArgs = self.config_pipAdditionalArgs(); + if (pipArgs != undefined && pipArgs.trim() == "") { + pipArgs = null; + } + var data = { plugins: { pluginmanager: { repository: repository, repository_ttl: repositoryTtl, - pip: pipCommand + pip: pipCommand, + pip_args: pipArgs } } }; @@ -412,6 +425,7 @@ $(function() { self.config_repositoryUrl(self.settingsViewModel.settings.plugins.pluginmanager.repository()); self.config_repositoryTtl(self.settingsViewModel.settings.plugins.pluginmanager.repository_ttl()); self.config_pipCommand(self.settingsViewModel.settings.plugins.pluginmanager.pip()); + self.config_pipAdditionalArgs(self.settingsViewModel.settings.plugins.pluginmanager.pip_args()); }; self.installed = function(data) { diff --git a/src/octoprint/plugins/pluginmanager/templates/pluginmanager_settings.jinja2 b/src/octoprint/plugins/pluginmanager/templates/pluginmanager_settings.jinja2 index 7c00534a..d399b593 100644 --- a/src/octoprint/plugins/pluginmanager/templates/pluginmanager_settings.jinja2 +++ b/src/octoprint/plugins/pluginmanager/templates/pluginmanager_settings.jinja2 @@ -6,14 +6,25 @@ {% macro pluginmanager_nopip() %}
{% trans %} - The pip command could not be detected automatically, - please configure it manually. No installation and uninstallation of plugin + The pip command could not be found. + Please configure it manually. No installation and uninstallation of plugin packages is possible while pip is unavailable. {% endtrans %}
{% endmacro %} +{% macro pluginmanager_sudopip() %} +
{% trans %} + The pip command is configured to use sudo. This + is not recommended due to security reasons. It is strongly + suggested you install OctoPrint under a + user-owned virtual environment + so that the use of sudo is not needed for plugin management. +{% endtrans %}
+{% endmacro %} + {{ pluginmanager_printing() }} {{ pluginmanager_nopip() }} +{{ pluginmanager_sudopip() }}
@@ -61,7 +72,7 @@

- Using pip at "" (Version ) + Using pip at "" (Version , additional arguments: )

+
+ +
+ +
+
diff --git a/src/octoprint/util/pip.py b/src/octoprint/util/pip.py index 7fa3dbc3..11c6f118 100644 --- a/src/octoprint/util/pip.py +++ b/src/octoprint/util/pip.py @@ -26,6 +26,7 @@ class PipCaller(object): self._command = None self._version = None + self._use_sudo = False self.trigger_refresh() @@ -53,13 +54,17 @@ class PipCaller(object): def version(self): return self._version + @property + def use_sudo(self): + return self._use_sudo + @property def available(self): return self._command is not None def trigger_refresh(self): try: - self._command, self._version = self._find_pip() + self._command, self._version, self._use_sudo = self._find_pip() except: self._logger.exception("Error while discovering pip command") self._command = None @@ -74,6 +79,8 @@ class PipCaller(object): raise UnknownPip() command = [self._command] + list(args) + if self._use_sudo: + command = ["sudo"] + command joined_command = " ".join(command) self._logger.debug(u"Calling: {}".format(joined_command)) @@ -120,6 +127,11 @@ class PipCaller(object): def _find_pip(self): pip_command = self.configured + if pip_command is not None and pip_command.startswith("sudo "): + pip_command = pip_command[len("sudo "):] + pip_sudo = True + else: + pip_sudo = False pip_version = None if pip_command is None: @@ -152,7 +164,11 @@ class PipCaller(object): if pip_command is not None: self._logger.debug("Found pip at {}, going to figure out its version".format(pip_command)) - p = sarge.run([pip_command, "--version"], stdout=sarge.Capture(), stderr=sarge.Capture()) + + 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)) @@ -182,7 +198,7 @@ class PipCaller(object): else: self._logger.info("Found pip at {}, version is {}".format(pip_command, version_segment)) - return pip_command, pip_version + return pip_command, pip_version, pip_sudo def _log_stdout(self, *lines): self.on_log_stdout(*lines) From fce7b40b513fdfed82f62f1d364899546ff4b6c5 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Gina=20H=C3=A4u=C3=9Fge?= Date: Mon, 28 Sep 2015 20:20:56 +0200 Subject: [PATCH 02/19] pip: Use string representation of version for display in UI --- src/octoprint/plugins/pluginmanager/__init__.py | 2 +- src/octoprint/util/pip.py | 12 ++++++++++-- 2 files changed, 11 insertions(+), 3 deletions(-) diff --git a/src/octoprint/plugins/pluginmanager/__init__.py b/src/octoprint/plugins/pluginmanager/__init__.py index a92fa91c..a93b6cc2 100644 --- a/src/octoprint/plugins/pluginmanager/__init__.py +++ b/src/octoprint/plugins/pluginmanager/__init__.py @@ -188,7 +188,7 @@ class PluginManagerPlugin(octoprint.plugin.SimpleApiPlugin, pip=dict( available=self._pip_caller.available, command=self._pip_caller.command, - version=str(self._pip_caller.version), + version=self._pip_caller.version_string, use_sudo=self._pip_caller.use_sudo, additional_args=self._settings.get(["pip_args"]) )) diff --git a/src/octoprint/util/pip.py b/src/octoprint/util/pip.py index 11c6f118..60ceb1b6 100644 --- a/src/octoprint/util/pip.py +++ b/src/octoprint/util/pip.py @@ -26,6 +26,7 @@ class PipCaller(object): self._command = None self._version = None + self._version_string = None self._use_sudo = False self.trigger_refresh() @@ -54,6 +55,10 @@ class PipCaller(object): def version(self): return self._version + @property + def version_string(self): + return self._version_string + @property def use_sudo(self): return self._use_sudo @@ -64,7 +69,7 @@ class PipCaller(object): def trigger_refresh(self): try: - self._command, self._version, self._use_sudo = self._find_pip() + self._command, self._version, self._version_string, self._use_sudo = self._find_pip() except: self._logger.exception("Error while discovering pip command") self._command = None @@ -127,12 +132,15 @@ class PipCaller(object): def _find_pip(self): pip_command = self.configured + if pip_command is not None and pip_command.startswith("sudo "): pip_command = pip_command[len("sudo "):] pip_sudo = True else: pip_sudo = False + pip_version = None + version_segment = None if pip_command is None: import os @@ -198,7 +206,7 @@ class PipCaller(object): else: self._logger.info("Found pip at {}, version is {}".format(pip_command, version_segment)) - return pip_command, pip_version, pip_sudo + return pip_command, pip_version, version_segment, pip_sudo def _log_stdout(self, *lines): self.on_log_stdout(*lines) 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 03/19] 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()) From 000f8e9310f04ead9d79136ea48e491cdc2aa276 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Gina=20H=C3=A4u=C3=9Fge?= Date: Tue, 29 Sep 2015 13:47:35 +0200 Subject: [PATCH 04/19] Debug logging for testballoon install --- src/octoprint/util/pip.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/src/octoprint/util/pip.py b/src/octoprint/util/pip.py index ccf8796c..9a860421 100644 --- a/src/octoprint/util/pip.py +++ b/src/octoprint/util/pip.py @@ -259,6 +259,8 @@ class PipCaller(CommandlineCaller): p = sarge.run(sarge_command, stdout=sarge.Capture(), stderr=sarge.Capture()) output = p.stdout.text + self._logger.debug("Got output from {}: {}".format(" ".join(sarge_command), output)) + 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) From 5df576b73e320b41423e845e7ba8a63c1fc7b2c0 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Gina=20H=C3=A4u=C3=9Fge?= Date: Tue, 29 Sep 2015 13:53:48 +0200 Subject: [PATCH 05/19] Install testballoon using pip install . in testballoon folder --- src/octoprint/util/pip.py | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/src/octoprint/util/pip.py b/src/octoprint/util/pip.py index 9a860421..aad082b2 100644 --- a/src/octoprint/util/pip.py +++ b/src/octoprint/util/pip.py @@ -254,9 +254,12 @@ class PipCaller(CommandlineCaller): import os testballoon = os.path.join(os.path.realpath(os.path.dirname(__file__)), "piptestballoon") - sarge_command = [pip_command, "install", testballoon, "--verbose"] + sarge_command = [pip_command, "install", ".", "--verbose"] try: - p = sarge.run(sarge_command, stdout=sarge.Capture(), stderr=sarge.Capture()) + p = sarge.run(sarge_command, + stdout=sarge.Capture(), + stderr=sarge.Capture(), + cwd=testballoon) output = p.stdout.text self._logger.debug("Got output from {}: {}".format(" ".join(sarge_command), output)) From cae73c1ee5ad323bcb8330280fd3314105d17082 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Gina=20H=C3=A4u=C3=9Fge?= Date: Tue, 29 Sep 2015 14:22:29 +0200 Subject: [PATCH 06/19] Cache for pip setup and version information Can take a bit of time to collect that data since it needs some invocations of pip, so we cache that data unless told otherwise. --- src/octoprint/util/pip.py | 28 +++++++++++++++++++--------- 1 file changed, 19 insertions(+), 9 deletions(-) diff --git a/src/octoprint/util/pip.py b/src/octoprint/util/pip.py index aad082b2..1303caf9 100644 --- a/src/octoprint/util/pip.py +++ b/src/octoprint/util/pip.py @@ -15,6 +15,7 @@ import pkg_resources from .commandline import CommandlineCaller +_cache = dict(version=dict(), setup=dict()) class UnknownPip(Exception): pass @@ -24,12 +25,13 @@ class PipCaller(CommandlineCaller): no_use_wheel = pkg_resources.parse_requirements("pip==1.5.0") broken = pkg_resources.parse_requirements("pip>=6.0.1,<=6.0.3") - def __init__(self, configured=None): + def __init__(self, configured=None, ignore_cache=False): CommandlineCaller.__init__(self) self._logger = logging.getLogger(__name__) self.configured = configured self.refresh = False + self.ignore_cache = ignore_cache self._command = None self._version = None @@ -144,7 +146,7 @@ class PipCaller(CommandlineCaller): self._logger.debug("Found pip at {}, going to figure out its version".format(pip_command)) - pip_version, version_segment = self._get_pip_version(pip_command, pip_sudo) + pip_version, version_segment = self._get_pip_version(pip_command) if pip_version is None: return @@ -212,10 +214,12 @@ class PipCaller(CommandlineCaller): return pip_command - def _get_pip_version(self, pip_command, pip_sudo): + def _get_pip_version(self, pip_command): + if not self.ignore_cache and pip_command in _cache["version"]: + self._logger.debug("Using cached pip version information for {}".format(pip_command)) + return _cache["version"][pip_command] + 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: @@ -244,13 +248,19 @@ class PipCaller(CommandlineCaller): self._logger.exception("Error while trying to parse version string from pip command") return None, None - return pip_version, version_segment + result = pip_version, version_segment + _cache["version"][pip_command] = result + return result 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): + if not self.ignore_cache and pip_command in _cache["setup"]: + self._logger.debug("Using cached pip setup information for {}".format(pip_command)) + return _cache["setup"][pip_command] + import os testballoon = os.path.join(os.path.realpath(os.path.dirname(__file__)), "piptestballoon") @@ -273,9 +283,9 @@ class PipCaller(CommandlineCaller): virtual_env = virtual_env_match.group(1) == "True" writable = writable_match.group(1) == "True" - return writable or not virtual_env, not writable and not virtual_env, install_dir - else: - return False, False, None + result = writable or not virtual_env, not writable and not virtual_env, install_dir + _cache["setup"][pip_command] = result + return result finally: sarge_command = [pip_command, "uninstall", "-y", "OctoPrint-PipTestBalloon"] From 22509f02b5c0f6a3f4752637af68e862eb238686 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Gina=20H=C3=A4u=C3=9Fge?= Date: Tue, 29 Sep 2015 14:23:10 +0200 Subject: [PATCH 07/19] Make sure to include testballoon package in install --- MANIFEST.in | 1 + setup.py | 4 +++- 2 files changed, 4 insertions(+), 1 deletion(-) diff --git a/MANIFEST.in b/MANIFEST.in index abffeac2..15833310 100644 --- a/MANIFEST.in +++ b/MANIFEST.in @@ -2,6 +2,7 @@ recursive-include src/octoprint/static * recursive-include src/octoprint/templates * recursive-include src/octoprint/plugins * recursive-include src/octoprint/translations * +include src/octoprint/util/piptestballoon/setup.py include versioneer.py include src/octoprint/_version.py include AUTHORS.md diff --git a/setup.py b/setup.py index 3633a628..ec24b83f 100644 --- a/setup.py +++ b/setup.py @@ -149,7 +149,9 @@ def params(): "": "src", } package_data = { - "octoprint": octoprint_setuptools.package_data_dirs('src/octoprint', ['static', 'templates', 'plugins', 'translations']) + "octoprint": octoprint_setuptools.package_data_dirs('src/octoprint', + ['static', 'templates', 'plugins', 'translations']) + + ['util/piptestballoon/setup.py'] } include_package_data = True From 62478fd98f964a9b0af967d80406cb0a654e0110 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Gina=20H=C3=A4u=C3=9Fge?= Date: Tue, 29 Sep 2015 14:51:58 +0200 Subject: [PATCH 08/19] More pip data in plugin manager front end --- .../plugins/pluginmanager/__init__.py | 6 ++++-- .../pluginmanager/static/js/pluginmanager.js | 13 +++++++++++++ .../templates/pluginmanager_settings.jinja2 | 18 +++++++++++++++--- 3 files changed, 32 insertions(+), 5 deletions(-) diff --git a/src/octoprint/plugins/pluginmanager/__init__.py b/src/octoprint/plugins/pluginmanager/__init__.py index 7b5679b7..d724f8e5 100644 --- a/src/octoprint/plugins/pluginmanager/__init__.py +++ b/src/octoprint/plugins/pluginmanager/__init__.py @@ -188,8 +188,8 @@ class PluginManagerPlugin(octoprint.plugin.SimpleApiPlugin, return jsonify(plugins=self._get_plugins(), repository=dict( - available=self._repository_available, - plugins=self._repository_plugins + available=self._repository_available, + plugins=self._repository_plugins ), os=self._get_os(), octoprint=self._get_octoprint_version_string(), @@ -197,7 +197,9 @@ class PluginManagerPlugin(octoprint.plugin.SimpleApiPlugin, available=self._pip_caller.available, command=self._pip_caller.command, version=self._pip_caller.version_string, + install_dir=self._pip_caller.install_dir, use_sudo=self._pip_caller.use_sudo, + use_user=self._pip_caller.use_user, additional_args=self._settings.get(["pip_args"]) )) diff --git a/src/octoprint/plugins/pluginmanager/static/js/pluginmanager.js b/src/octoprint/plugins/pluginmanager/static/js/pluginmanager.js index 75c8e2f3..0f61a927 100644 --- a/src/octoprint/plugins/pluginmanager/static/js/pluginmanager.js +++ b/src/octoprint/plugins/pluginmanager/static/js/pluginmanager.js @@ -82,9 +82,18 @@ $(function() { self.pipAvailable = ko.observable(false); self.pipCommand = ko.observable(); self.pipVersion = ko.observable(); + self.pipInstallDir = ko.observable(); + self.pipUseUser = ko.observable(); self.pipUseSudo = ko.observable(); self.pipAdditionalArgs = ko.observable(); + self.pipUseSudoString = ko.computed(function() { + return self.pipUseSudo() ? "yes" : "no"; + }); + self.pipUseUserString = ko.computed(function() { + return self.pipUseUser() ? "yes" : "no"; + }); + self.working = ko.observable(false); self.workingTitle = ko.observable(); self.workingDialog = undefined; @@ -211,11 +220,15 @@ $(function() { if (data.available) { self.pipCommand(data.command); self.pipVersion(data.version); + self.pipInstallDir(data.install_dir); + self.pipUseUser(data.use_user); self.pipUseSudo(data.use_sudo); self.pipAdditionalArgs(data.additional_args); } else { self.pipCommand(undefined); self.pipVersion(undefined); + self.pipInstallDir(undefined); + self.pipUseUser(data.use_user); self.pipUseSudo(undefined); self.pipAdditionalArgs(undefined); } diff --git a/src/octoprint/plugins/pluginmanager/templates/pluginmanager_settings.jinja2 b/src/octoprint/plugins/pluginmanager/templates/pluginmanager_settings.jinja2 index d399b593..9c55fac0 100644 --- a/src/octoprint/plugins/pluginmanager/templates/pluginmanager_settings.jinja2 +++ b/src/octoprint/plugins/pluginmanager/templates/pluginmanager_settings.jinja2 @@ -71,9 +71,21 @@ -

- Using pip at "" (Version , additional arguments: ) -

+
+ +
+ + Installation directory: ("--user" flag: , sudo: )
+ Additional Arguments: +
+
+