Merge remote-tracking branch 'upstream/devel' into dev/folderSupport

This commit is contained in:
Salandora 2015-10-01 01:56:37 +02:00
commit 579c8f6748
8 changed files with 388 additions and 104 deletions

View file

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

View file

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

View file

@ -133,6 +133,7 @@ class PluginInfo(object):
self.enabled = True
self.bundled = False
self.loaded = False
self.managable = True
self._name = name
self._version = version
@ -475,8 +476,25 @@ class PluginManager(object):
self.marked_plugins = defaultdict(list)
self._python_install_dir = None
self._python_virtual_env = False
self._detect_python_environment()
self.reload_plugins(startup=True, initialize_implementations=False)
def _detect_python_environment(self):
from distutils.command.install import install as cmd_install
from distutils.dist import Distribution
import sys
cmd = cmd_install(Distribution())
cmd.finalize_options()
self._python_install_dir = cmd.install_lib
self._python_prefix = sys.prefix
self._python_virtual_env = hasattr(sys, "real_prefix") \
or (hasattr(sys, "base_prefix") and sys.prefix != sys.base_prefix)
@property
def plugins(self):
plugins = dict(self.enabled_plugins)
@ -503,12 +521,13 @@ class PluginManager(object):
result = dict()
for folder in folders:
readonly = False
flagged_readonly = False
if isinstance(folder, (list, tuple)):
if len(folder) == 2:
folder, readonly = folder
folder, flagged_readonly = folder
else:
continue
actual_readonly = not os.access(folder, os.W_OK)
if not os.path.exists(folder):
self.logger.warn("Plugin folder {folder} could not be found, skipping it".format(folder=folder))
@ -531,8 +550,8 @@ class PluginManager(object):
plugin = self._import_plugin_from_module(key, folder=folder)
if plugin:
plugin.origin = FolderOrigin("folder", folder)
if readonly:
plugin.bundled = True
plugin.managable = not flagged_readonly and not actual_readonly
plugin.bundled = flagged_readonly
plugin.enabled = False
@ -543,9 +562,18 @@ class PluginManager(object):
def _find_plugins_from_entry_points(self, groups, existing, ignore_uninstalled=True):
result = dict()
# let's make sure we have a current working set
# let's make sure we have a current working set ...
working_set = pkg_resources.WorkingSet()
# ... including the user's site packages
import site
import sys
if site.ENABLE_USER_SITE:
if not site.USER_SITE in working_set.entries:
working_set.add_entry(site.USER_SITE)
if not site.USER_SITE in sys.path:
site.addsitedir(site.USER_SITE)
if not isinstance(groups, (list, tuple)):
groups = [groups]
@ -578,6 +606,16 @@ class PluginManager(object):
plugin = self._import_plugin_from_module(key, **kwargs)
if plugin:
plugin.origin = EntryPointOrigin("entry_point", group, module_name, package_name, version)
# plugin is manageable if its location is writable and OctoPrint
# is either not running from a virtual env or the plugin is
# installed in that virtual env - the virtual env's pip will not
# allow us to uninstall stuff that is installed outside
# of the virtual env, so this check is necessary
plugin.managable = os.access(plugin.location, os.W_OK) \
and (not self._python_virtual_env
or plugin.location.startswith(self._python_prefix))
plugin.enabled = False
result[key] = plugin
@ -648,15 +686,24 @@ class PluginManager(object):
hooks=sum(map(lambda x: len(x), self.plugin_hooks.values()))
))
def mark_plugin(self, name, uninstalled=None):
def mark_plugin(self, name, **kwargs):
if not name in self.plugins:
self.logger.warn("Trying to mark an unknown plugin {name}".format(**locals()))
self.logger.debug("Trying to mark an unknown plugin {name}".format(**locals()))
if uninstalled is not None:
if uninstalled and not name in self.marked_plugins["uninstalled"]:
self.marked_plugins["uninstalled"].append(name)
elif not uninstalled and name in self.marked_plugins["uninstalled"]:
self.marked_plugins["uninstalled"].remove(name)
for key, value in kwargs.items():
if value is None:
continue
if value and not name in self.marked_plugins[key]:
self.marked_plugins[key].append(name)
elif not value and name in self.marked_plugins[key]:
self.marked_plugins[key].remove(name)
def is_plugin_marked(self, name, key):
if not name in self.plugins:
return False
return name in self.marked_plugins[key]
def load_plugin(self, name, plugin=None, startup=False, initialize_implementation=True):
if not name in self.plugins:
@ -886,7 +933,7 @@ class PluginManager(object):
additional_pre_inits=additional_pre_inits,
additional_post_inits=additional_post_inits)
self.logger.info("Initialized {count} plugin(s)".format(count=len(self.plugin_implementations)))
self.logger.info("Initialized {count} plugin implementation(s)".format(count=len(self.plugin_implementations)))
def initialize_implementation_of_plugin(self, name, plugin, additional_injects=None, additional_inject_factories=None, additional_pre_inits=None, additional_post_inits=None):
if plugin.implementation is None:
@ -980,15 +1027,13 @@ class PluginManager(object):
self.logger.info("No plugins available")
else:
self.logger.info("{count} plugin(s) registered with the system:\n{plugins}".format(count=len(all_plugins), plugins="\n".join(
sorted(
map(lambda x: "| " + x.long_str(show_bundled=show_bundled,
bundled_strs=bundled_str,
show_location=show_location,
location_str=location_str,
show_enabled=show_enabled,
enabled_strs=enabled_str),
self.enabled_plugins.values())
)
map(lambda x: "| " + x.long_str(show_bundled=show_bundled,
bundled_strs=bundled_str,
show_location=show_location,
location_str=location_str,
show_enabled=show_enabled,
enabled_strs=enabled_str),
sorted(self.plugins.values(), key=lambda x: str(x).lower()))
)))
def get_plugin(self, identifier, require_enabled=True):

View file

@ -52,7 +52,8 @@ class PluginManagerPlugin(octoprint.plugin.SimpleApiPlugin,
self._repository_cache_path = os.path.join(self.get_plugin_data_folder(), "plugins.json")
self._repository_cache_ttl = self._settings.get_int(["repository_ttl"]) * 60
self._pip_caller = PipCaller(configured=self._settings.get(["pip"]))
self._pip_caller = PipCaller(configured=self._settings.get(["pip"]),
force_user=self._settings.get_boolean(["pip_force_user"]))
self._pip_caller.on_log_call = self._log_call
self._pip_caller.on_log_stdout = self._log_stdout
self._pip_caller.on_log_stderr = self._log_stderr
@ -84,6 +85,7 @@ class PluginManagerPlugin(octoprint.plugin.SimpleApiPlugin,
repository_ttl=24*60,
pip=None,
pip_args=None,
pip_force_user=False,
dependency_links=False,
hidden=[]
)
@ -94,6 +96,7 @@ class PluginManagerPlugin(octoprint.plugin.SimpleApiPlugin,
new_pip = self._settings.get(["pip"])
self._repository_cache_ttl = self._settings.get_int(["repository_ttl"]) * 60
self._pip_caller.force_user = self._settings.get_boolean(["pip_force_user"])
if old_pip != new_pip:
self._pip_caller.configured = new_pip
try:
@ -188,8 +191,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 +200,10 @@ 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,
virtual_env=self._pip_caller.virtual_env,
additional_args=self._settings.get(["pip_args"])
))
@ -331,12 +337,15 @@ class PluginManagerPlugin(octoprint.plugin.SimpleApiPlugin,
self._send_result_notification("install", result)
return jsonify(result)
self._plugin_manager.mark_plugin(new_plugin_key, uninstalled=False)
self._plugin_manager.reload_plugins()
needs_restart = self._plugin_manager.is_restart_needing_plugin(new_plugin) or new_plugin_key in all_plugins_before or reinstall is not None
needs_refresh = new_plugin.implementation and isinstance(new_plugin.implementation, octoprint.plugin.ReloadNeedingPlugin)
is_reinstall = self._plugin_manager.is_plugin_marked(new_plugin_key, "uninstalled")
self._plugin_manager.mark_plugin(new_plugin_key,
uninstalled=False,
installed=not is_reinstall and needs_restart)
self._plugin_manager.log_all_plugins()
result = dict(result=True, url=url, needs_restart=needs_restart, needs_refresh=needs_refresh, was_reinstalled=new_plugin_key in all_plugins_before or reinstall is not None, plugin=self._to_external_representation(new_plugin))
@ -345,10 +354,13 @@ class PluginManagerPlugin(octoprint.plugin.SimpleApiPlugin,
def command_uninstall(self, plugin):
if plugin.key == "pluginmanager":
return make_response("Can't uninstall Plugin Manager", 400)
return make_response("Can't uninstall Plugin Manager", 403)
if not plugin.managable:
return make_response("Plugin is not managable and hence cannot be uninstalled", 403)
if plugin.bundled:
return make_response("Bundled plugins cannot be uninstalled", 400)
return make_response("Bundled plugins cannot be uninstalled", 403)
if plugin.origin is None:
self._logger.warn(u"Trying to uninstall plugin {plugin} but origin is unknown".format(**locals()))
@ -392,7 +404,10 @@ class PluginManagerPlugin(octoprint.plugin.SimpleApiPlugin,
needs_restart = self._plugin_manager.is_restart_needing_plugin(plugin)
needs_refresh = plugin.implementation and isinstance(plugin.implementation, octoprint.plugin.ReloadNeedingPlugin)
self._plugin_manager.mark_plugin(plugin.key, uninstalled=True)
was_pending_install = self._plugin_manager.is_plugin_marked(plugin.key, "installed")
self._plugin_manager.mark_plugin(plugin.key,
uninstalled=not was_pending_install and needs_restart,
installed=False)
if not needs_restart:
try:
@ -664,11 +679,12 @@ class PluginManagerPlugin(octoprint.plugin.SimpleApiPlugin,
url=plugin.url,
license=plugin.license,
bundled=plugin.bundled,
managable=plugin.managable,
enabled=plugin.enabled,
pending_enable=(not plugin.enabled and plugin.key in self._pending_enable),
pending_disable=(plugin.enabled and plugin.key in self._pending_disable),
pending_install=(plugin.key in self._pending_install),
pending_uninstall=(plugin.key in self._pending_uninstall),
pending_install=(self._plugin_manager.is_plugin_marked(plugin.key, "installed")),
pending_uninstall=(self._plugin_manager.is_plugin_marked(plugin.key, "uninstalled")),
origin=plugin.origin.type
)

View file

@ -10,6 +10,7 @@ $(function() {
self.config_repositoryTtl = ko.observable();
self.config_pipCommand = ko.observable();
self.config_pipAdditionalArgs = ko.observable();
self.config_pipForceUser = ko.observable();
self.configurationDialog = $("#settings_plugin_pluginmanager_configurationdialog");
@ -82,9 +83,22 @@ $(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.pipVirtualEnv = 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.pipVirtualEnvString = ko.computed(function() {
return self.pipVirtualEnv() ? "yes" : "no";
});
self.working = ko.observable(false);
self.workingTitle = ko.observable();
self.workingDialog = undefined;
@ -101,6 +115,7 @@ $(function() {
self.enableUninstall = function(data) {
return self.enableManagement()
&& (data.origin != "entry_point" || self.pipAvailable())
&& data.managable
&& !data.bundled
&& data.key != 'pluginmanager'
&& !data.pending_uninstall;
@ -211,12 +226,18 @@ $(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.pipVirtualEnv(data.virtual_env);
self.pipAdditionalArgs(data.additional_args);
} else {
self.pipCommand(undefined);
self.pipVersion(undefined);
self.pipInstallDir(undefined);
self.pipUseUser(undefined);
self.pipUseSudo(undefined);
self.pipVirtualEnv(undefined);
self.pipAdditionalArgs(undefined);
}
};
@ -410,7 +431,8 @@ $(function() {
repository: repository,
repository_ttl: repositoryTtl,
pip: pipCommand,
pip_args: pipArgs
pip_args: pipArgs,
pip_force_user: self.config_pipForceUser()
}
}
};
@ -426,6 +448,7 @@ $(function() {
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.config_pipForceUser(self.settingsViewModel.settings.plugins.pluginmanager.pip_force_user());
};
self.installed = function(data) {

View file

@ -6,8 +6,9 @@
{% macro pluginmanager_nopip() %}
<div class="alert" data-bind="visible: !pipAvailable()">{% trans %}
The <code>pip</code> command could not be found.
Please configure it manually. No installation and uninstallation of plugin
The <code>pip</code> command could not be found or does not work correctly
for this installation of OctoPrint - please consult the log file for details
and if necessary configure it manually. No installation and uninstallation of plugin
packages is possible while <code>pip</code> is unavailable.
{% endtrans %}</div>
{% endmacro %}
@ -42,7 +43,7 @@
<tbody data-bind="foreach: plugins.paginatedItems">
<tr>
<td class="settings_plugin_plugin_manager_plugins_name">
<div data-bind="css: {muted: !enabled}"><span data-bind="text: name"></span> <span data-bind="visible: version">(<span data-bind="text: version"></span>)</span> <i title="{{ _('Bundled') }}" class="icon-th-large" data-bind="visible: bundled"></i> <i title="Restart needed" class="icon-refresh" data-bind="visible: pending_enable || pending_disable || pending_uninstall"></i> <i title="Uninstalled" class="icon-remove" data-bind="visible: pending_uninstall"></i></div>
<div data-bind="css: {muted: !enabled}"><span data-bind="text: name"></span> <span data-bind="visible: version">(<span data-bind="text: version"></span>)</span> <i title="{{ _('Bundled with OctoPrint') }}" class="icon-th-large" data-bind="visible: bundled"></i> <i class="icon-lock" title="{{ _('Cannot be uninstalled through OctoPrint') }}" data-bind="visible: !managable"></i> <i title="{{ _('Restart of OctoPrint needed for changes to take effect') }}" class="icon-refresh" data-bind="visible: pending_enable || pending_disable || pending_install || pending_uninstall"></i> <i title="{{ _('Pending install') }}" class="icon-plus" data-bind="visible: pending_install"></i> <i title="{{ _('Pending uninstall') }}" class="icon-minus" data-bind="visible: pending_uninstall"></i></div>
<div><small class="muted" data-bind="text: description">&nbsp;</small></div>
<div data-bind="css: {muted: !enabled}">
<small data-bind="visible: url"><i class="icon-home"></i> <a data-bind="attr: {href: url}">{{ _('Homepage') }}</a></small>
@ -71,9 +72,22 @@
<button class="btn btn-block" data-bind="click: $root.showRepository">{{ _('Get More...') }}</button>
<p class="muted" data-bind="visible: pipAvailable()">
<small>Using pip at "<span data-bind="text: pipCommand"></span>" (Version <span data-bind="text: pipVersion"></span><span data-bind="visible: pipAdditionalArgs">, additional arguments: <span data-bind="text: pipAdditionalArgs"></span></span>)</small>
</p>
<div class="muted" data-bind="visible: pipAvailable()">
<div>
<small>
<a href="#" class="muted" onclick="$(this).children('i.toggle-arrow').toggleClass('icon-caret-right icon-caret-down').parent().parent().parent().next().slideToggle('fast')">
<i class="toggle-arrow icon-caret-right"></i> Using pip at "<span data-bind="text: pipCommand"></span>", Version <span data-bind="text: pipVersion"></span>
</a>
</small>
</div>
<div class="hide">
<small>
Installation directory: <span data-bind="text: pipInstallDir"></span> ("--user" flag: <span data-bind="text: pipUseUserString"></span>, sudo: <span data-bind="text: pipUseSudoString"></span>)<br />
Virtual environment: <span data-bind="text: pipVirtualEnvString"></span><br />
<span data-bind="visible: pipAdditionalArgs">Additional Arguments: <span data-bind="text: pipAdditionalArgs"></span></span>
</small>
</div>
</div>
<div id="settings_plugin_pluginmanager_workingdialog" class="modal hide fade">
<div class="modal-header">
@ -205,19 +219,31 @@
</div>
<div class="modal-body">
<form class="form-horizontal">
<legend>{{ _('pip configuration') }}</legend>
<div class="control-group" title="{{ _('pip command to use for managing plugins. You might have to configure this if auto detection fails.') }}">
<label class="control-label">{{ _('pip command') }}</label>
<label class="control-label">{{ _('Command') }}</label>
<div class="controls">
<input type="text" class="input-block-level" data-bind="value: config_pipCommand" placeholder="{{ _('Autodetect') }}">
<span class="help-inline">{{ _('<strong>Only</strong> set this if OctoPrint cannot autodetect the path to <code>pip</code> to use for managing plugins.') }}</span>
</div>
</div>
<div class="control-group" title="{{ _('Additional arguments for pip command. You should normally not have to change this.') }}">
<label class="control-label">{{ _('Additional pip arguments') }}</label>
<label class="control-label">{{ _('Additional arguments') }}</label>
<div class="controls">
<input type="text" class="input-block-level" data-bind="value: config_pipAdditionalArgs">
</div>
</div>
<div class="control-group">
<div class="controls">
<label class="checkbox">
<input type="checkbox" data-bind="checked: config_pipForceUser"> {{ _('Force the user of the <code>--user</code> flag with <code>pip install</code>') }}
</label>
</div>
</div>
<legend>{{ _('Plugin repository configuration') }}</legend>
<div class="control-group" title="{{ _('URL of the Plugin Repository to use. You should normally not have to change this.') }}">
<label class="control-label">{{ _('Repository URL') }}</label>
<div class="controls">

View file

@ -9,11 +9,14 @@ __copyright__ = "Copyright (C) 2015 The OctoPrint Project - Released under terms
import sarge
import sys
import logging
import re
import site
import pkg_resources
from .commandline import CommandlineCaller
_cache = dict(version=dict(), setup=dict())
class UnknownPip(Exception):
pass
@ -23,17 +26,25 @@ 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, force_sudo=False,
force_user=False):
CommandlineCaller.__init__(self)
self._logger = logging.getLogger(__name__)
self.configured = configured
self.refresh = False
self.ignore_cache = ignore_cache
self.force_sudo = force_sudo
self.force_user = force_user
self._command = None
self._version = None
self._version_string = None
self._use_sudo = False
self._use_user = False
self._virtual_env = False
self._install_dir = None
self.trigger_refresh()
@ -41,6 +52,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 +84,30 @@ 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 virtual_env(self):
return self._virtual_env
@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
@ -90,20 +122,38 @@ class PipCaller(CommandlineCaller):
raise UnknownPip()
arg_list = list(args)
if "install" in arg_list:
# strip --process-dependency-links for versions that don't support it
if not self.version in self.__class__.process_dependency_links and "--process-dependency-links" in arg_list:
self._logger.debug("Found --process-dependency-links flag, version {} doesn't need that yet though, removing.".format(self.version))
arg_list.remove("--process-dependency-links")
# add --no-use-wheel for versions that otherwise break
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")
# remove --user if it's present and a virtual env is detected
if "--user" in arg_list:
if self._virtual_env or not site.ENABLE_USER_SITE:
self._logger.debug("Virtual environment detected, removing --user flag.")
arg_list.remove("--user")
# otherwise add it if necessary
elif not self._virtual_env and site.ENABLE_USER_SITE and (self.use_user or self.force_user):
self._logger.debug("pip needs --user flag for installations.")
arg_list.append("--user")
# add args to command
command = [self._command] + list(arg_list)
if self._use_sudo:
# add sudo if necessary
if self._use_sudo or self.force_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 +162,166 @@ 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)
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
p = sarge.run(sarge_command, stdout=sarge.Capture(), stderr=sarge.Capture())
# 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.
if p.returncode != 0:
self._logger.warn("Error while trying to run pip --version: {}".format(p.stderr.text))
pip_command = None
ok, pip_user, pip_virtual_env, pip_install_dir = self._check_pip_setup(pip_command)
if not ok:
self._logger.error("Cannot use pip at {}".format(pip_command))
return
self._logger.info("pip at {} installs to {}, --user flag needed => {}, virtual env => {}".format(pip_command, pip_install_dir, "yes" if pip_user else "no", "yes" if pip_virtual_env 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._virtual_env = pip_virtual_env
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):
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"]
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
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")
sarge_command = [pip_command, "install", ".", "--verbose"]
try:
p = sarge.run(sarge_command,
stdout=sarge.Capture(),
stderr=sarge.Capture(),
cwd=testballoon)
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
except:
self._logger.exception("Error while trying to install testballoon to figure out pip setup")
return False, False, False, None
finally:
sarge_command = [pip_command, "uninstall", "-y", "OctoPrint-PipTestBalloon"]
sarge.run(sarge_command, stdout=sarge.Capture(), stderr=sarge.Capture())
if not output.startswith("pip"):
self._logger.warn("pip command returned unparseable output, can't determine version: {}".format(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)
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))
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"
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
else:
self._logger.info("Found pip at {}, version is {}".format(pip_command, version_segment))
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
return pip_command, pip_version, version_segment, pip_sudo
# ok, enable user flag, virtual env yes/no, installation dir
result = writable or not virtual_env, \
not writable and not virtual_env and site.ENABLE_USER_SITE, \
virtual_env, \
install_dir
_cache["setup"][pip_command] = result
return result
else:
self._logger.debug("Could not detect desired output from testballoon install, got this instead: {}".format(" ".join(sarge_command), output))
return False, False, False, None

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