Merge branch 'devel' into dev/clientlib
This commit is contained in:
commit
b432496b7f
9 changed files with 450 additions and 91 deletions
|
|
@ -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
|
||||
|
|
|
|||
16
README.md
16
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
|
||||
|
|
|
|||
4
setup.py
4
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
|
||||
|
|
|
|||
|
|
@ -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:
|
||||
|
|
|
|||
|
|
@ -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
|
||||
)
|
||||
|
||||
|
|
|
|||
|
|
@ -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) {
|
||||
|
|
|
|||
|
|
@ -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"> </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">
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
30
src/octoprint/util/piptestballoon/setup.py
Normal file
30
src/octoprint/util/piptestballoon/setup.py
Normal file
|
|
@ -0,0 +1,30 @@
|
|||
from setuptools import setup
|
||||
|
||||
def run_checks():
|
||||
from distutils.command.install import install as cmd_install
|
||||
from distutils.dist import Distribution
|
||||
import sys
|
||||
import os
|
||||
|
||||
cmd = cmd_install(Distribution())
|
||||
cmd.finalize_options()
|
||||
|
||||
install_dir = cmd.install_lib
|
||||
virtual_env = hasattr(sys, "real_prefix")
|
||||
writable = os.access(install_dir, os.W_OK)
|
||||
|
||||
print("!!! PIP_INSTALL_DIR={}".format(install_dir))
|
||||
print("!!! PIP_VIRTUAL_ENV={}".format(virtual_env))
|
||||
print("!!! PIP_WRITABLE={}".format(writable))
|
||||
sys.stdout.flush()
|
||||
|
||||
def parameters():
|
||||
run_checks()
|
||||
|
||||
return dict(
|
||||
name="OctoPrint-PipTestBalloon",
|
||||
version="1.0",
|
||||
description="Just a test balloon to check a couple of pip related settings"
|
||||
)
|
||||
|
||||
setup(**parameters())
|
||||
Loading…
Reference in a new issue