Merge branch 'devel' into dev/clientlib

This commit is contained in:
Gina Häußge 2015-09-30 14:40:46 +02:00
commit b432496b7f
9 changed files with 450 additions and 91 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

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

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:

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
@ -83,6 +84,8 @@ class PluginManagerPlugin(octoprint.plugin.SimpleApiPlugin,
repository="http://plugins.octoprint.org/plugins.json",
repository_ttl=24*60,
pip=None,
pip_args=None,
pip_force_user=False,
dependency_links=False,
hidden=[]
)
@ -93,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:
@ -187,15 +191,20 @@ 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(),
pip=dict(
available=self._pip_caller.available,
command=self._pip_caller.command,
version=str(self._pip_caller.version)
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"])
))
def on_api_command(self, command, data):
@ -328,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))
@ -342,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()))
@ -389,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:
@ -453,6 +471,10 @@ class PluginManagerPlugin(octoprint.plugin.SimpleApiPlugin,
if "--process-dependency-links" in args:
self._log_message(u"Installation needs to process external dependencies, that might make it take a bit longer than usual depending on the pip version")
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):
@ -657,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

@ -76,6 +76,8 @@ $(function() {
self.config_repositoryUrl = ko.observable();
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");
@ -148,6 +150,21 @@ $(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();
@ -165,6 +182,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;
@ -275,9 +293,19 @@ $(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);
}
};
@ -463,12 +491,19 @@ $(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,
pip_force_user: self.config_pipForceUser()
}
}
};
@ -483,6 +518,8 @@ $(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.config_pipForceUser(self.settingsViewModel.settings.plugins.pluginmanager.pip_force_user());
};
self.installed = function(data) {

View file

@ -6,14 +6,25 @@
{% macro pluginmanager_nopip() %}
<div class="alert" data-bind="visible: !pipAvailable()">{% trans %}
The <code>pip</code> command could not be detected automatically,
please configure it manually. No installation and uninstallation of plugin
The <code>pip</code> command could not be found.
Please configure it manually. No installation and uninstallation of plugin
packages is possible while <code>pip</code> is unavailable.
{% endtrans %}</div>
{% endmacro %}
{% macro pluginmanager_sudopip() %}
<div class="alert alert-error" data-bind="visible: pipUseSudo()">{% trans %}
The <code>pip</code> command is configured to use <code>sudo</code>. This
is <strong>not</strong> recommended due to security reasons. It is <strong>strongly</strong>
suggested you install OctoPrint under a
<a href="https://github.com/foosel/OctoPrint/#installation">user-owned virtual environment</a>
so that the use of <code>sudo</code> is not needed for plugin management.
{% endtrans %}</div>
{% endmacro %}
{{ pluginmanager_printing() }}
{{ pluginmanager_nopip() }}
{{ pluginmanager_sudopip() }}
<div class="pull-right">
<button class="btn btn-small" data-bind="click: function() { $root.showPluginSettings(); }" title="{{ _('Plugin Configuration') }}"><i class="icon-wrench"></i></button>
@ -31,7 +42,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>
@ -60,9 +71,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>)</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">
@ -85,6 +109,7 @@
<div class="modal-body">
{{ pluginmanager_printing() }}
{{ pluginmanager_nopip() }}
{{ pluginmanager_sudopip() }}
<h4 style="position: relative">
{{ _('... from the <a href="%(url)s" target="_blank">Plugin Repository</a>', url='http://plugins.octoprint.org') }}
<a class="dropdown-toggle pull-right" data-toggle="dropdown" href="#">
@ -193,13 +218,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 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,15 +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()
@ -39,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
@ -59,13 +80,34 @@ class PipCaller(CommandlineCaller):
def version(self):
return self._version
@property
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._find_pip()
self._setup_pip()
except:
self._logger.exception("Error while discovering pip command")
self._command = None
@ -80,83 +122,203 @@ 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")
command = [self._command] + arg_list
# 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)
# 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
pip_version = None
if pip_command is not None and pip_command.startswith("sudo "):
pip_command = pip_command[len("sudo "):]
pip_sudo = True
else:
pip_sudo = False
if pip_command is None:
import os
python_command = sys.executable
binary_dir = os.path.dirname(python_command)
pip_command = self._autodetect_pip()
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
if pip_command is None:
return
# virtual env?
pip_command = os.path.join(binary_dir, "pip.exe")
# Determine the pip version
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")
self._logger.debug("Found pip at {}, going to figure out its version".format(pip_command))
if not os.path.isfile(pip_command) or not os.access(pip_command, os.X_OK):
pip_command = None
pip_version, version_segment = self._get_pip_version(pip_command)
if pip_version is None:
return
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())
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 p.returncode != 0:
self._logger.warn("Error while trying to run pip --version: {}".format(p.stderr.text))
pip_command = None
self._logger.info("Version of pip at {} is {}".format(pip_command, version_segment))
# 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_virtual_env, 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 => {}, 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
self._logger.debug("Got output from {}: {}".format(" ".join(sarge_command), output))
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]
# 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
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
else:
self._logger.info("Found pip at {}, version is {}".format(pip_command, version_segment))
finally:
sarge_command = [pip_command, "uninstall", "-y", "OctoPrint-PipTestBalloon"]
sarge.run(sarge_command, stdout=sarge.Capture(), stderr=sarge.Capture())
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
return pip_command, pip_version

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