From 214b46213cd2978e1aede42699920550178a2ead Mon Sep 17 00:00:00 2001 From: Thomas Leonard Date: Sat, 8 Jan 2011 17:18:48 +0000 Subject: [PATCH] Added "0install select" sub-command Also, added --help and --version. --- 0install | 14 +++ 0install.1 | 6 +- setup.py | 2 +- zeroinstall/cmd/__init__.py | 118 ++++++++++++++++++++++++ zeroinstall/cmd/select.py | 212 ++++++++++++++++++++++++++++++++++++++++++++ 5 files changed, 347 insertions(+), 5 deletions(-) create mode 100755 0install create mode 100644 zeroinstall/cmd/__init__.py create mode 100644 zeroinstall/cmd/select.py diff --git a/0install b/0install new file mode 100755 index 0000000..fc267ee --- /dev/null +++ b/0install @@ -0,0 +1,14 @@ +#!/usr/bin/env python + +import locale +from logging import warn +try: + locale.setlocale(locale.LC_ALL, '') +except locale.Error: + warn('Error setting locale (eg. Invalid locale)') + +## PATH ## + +from zeroinstall.cmd import main +import sys +main(sys.argv[1:]) diff --git a/0install.1 b/0install.1 index e94e02a..6166069 100644 --- a/0install.1 +++ b/0install.1 @@ -86,10 +86,6 @@ them. More verbose output. Use twice for even more verbose output. .TP -\fB\-V\fP, \fB\-\-version\fP -Display version information. - -.TP \fB\-\-with\-store=DIR\fP Append a directory to the list of implementation caches. Each sub-directory of DIR contains the contents of one version of a program or library. @@ -298,6 +294,8 @@ With no arguments, `0install config' displays all configuration settings. With one argument, it displays the current value of the named setting. With two arguments, it sets the setting to the given value. +.SS 0install --version +This can be used (without any command) the get version of 0install itself: .SH DEBUGGING TIPS diff --git a/setup.py b/setup.py index 9b82754..0d0c84b 100644 --- a/setup.py +++ b/setup.py @@ -141,4 +141,4 @@ use. The injector solves this problem by selecting components to meet a program's requirements, according to a policy you give it. The injector finds out which versions are available, and downloads and runs the ones you choose.""", - packages=["zeroinstall", "zeroinstall.support", "zeroinstall.zerostore", "zeroinstall.injector", "zeroinstall.0launch-gui", "zeroinstall.gtkui"]) + packages=["zeroinstall", "zeroinstall.support", "zeroinstall.zerostore", "zeroinstall.injector", "zeroinstall.0launch-gui", "zeroinstall.gtkui", "zeroinstall.cmd"]) diff --git a/zeroinstall/cmd/__init__.py b/zeroinstall/cmd/__init__.py new file mode 100644 index 0000000..7094d2a --- /dev/null +++ b/zeroinstall/cmd/__init__.py @@ -0,0 +1,118 @@ +""" +The B{0install} command-line interface. +""" + +# Copyright (C) 2011, Thomas Leonard +# See the README file for details, or visit http://0install.net. + +from zeroinstall import _ +import os, sys +from optparse import OptionParser +import logging + +from zeroinstall import SafeException, NeedDownload +from zeroinstall.injector import model, autopolicy, selections +from zeroinstall.injector.iface_cache import iface_cache + +valid_commands = ['select', 'download', 'run', 'update', + 'config', 'import', 'list', 'add-feed', 'remove-feed'] + +class UsageError(Exception): pass + +def _ensure_standard_fds(): + "Ensure stdin, stdout and stderr FDs exist, to avoid confusion." + for std in (0, 1, 2): + try: + os.fstat(std) + except OSError: + fd = os.open('/dev/null', os.O_RDONLY) + if fd != std: + os.dup2(fd, std) + os.close(fd) + +def _no_command(command_args): + """Handle --help and --version""" + parser = OptionParser(usage=_("usage: %prog COMMAND\n\nTry --help with one of these:") + + "\n\n0install " + '\n0install '.join(valid_commands)) + parser.add_option("-V", "--version", help=_("display version information"), action='store_true') + + (options, args) = parser.parse_args(command_args) + if options.version: + import zeroinstall + print "0install (zero-install) " + zeroinstall.version + print "Copyright (C) 2011 Thomas Leonard" + print _("This program comes with ABSOLUTELY NO WARRANTY," + "\nto the extent permitted by law." + "\nYou may redistribute copies of this program" + "\nunder the terms of the GNU Lesser General Public License." + "\nFor more information about these matters, see the file named COPYING.") + sys.exit(0) + parser.print_help() + sys.exit(2) + +def main(command_args): + """Act as if 0install was run with the given arguments. + @arg command_args: array of arguments (e.g. C{sys.argv[1:]}) + @type command_args: [str] + """ + _ensure_standard_fds() + + # The first non-option argument is the command name (or "help" if none is found). + command = None + for arg in command_args: + if not arg.startswith('-'): + command = arg + break + elif arg == '--': + break + + verbose = False + try: + if command is None: + return _no_command(command_args) + + if command not in valid_commands: + raise SafeException(_("Unknown sub-command '%s': try --help") % command) + + # Configure a parser for the given command + cmd = __import__('zeroinstall.cmd.' + command, globals(), locals(), [command], 0) + parser = OptionParser(usage=_("usage: %%prog %s [OPTIONS] %s") % (command, cmd.syntax)) + + parser.add_option("-c", "--console", help=_("never use GUI"), action='store_false', dest='gui') + parser.add_option("-g", "--gui", help=_("show graphical policy editor"), action='store_true') + parser.add_option("-v", "--verbose", help=_("more verbose output"), action='count') + parser.add_option("", "--with-store", help=_("add an implementation cache"), action='append', metavar='DIR') + + cmd.add_options(parser) + (options, args) = parser.parse_args(command_args) + assert args[0] == command + + if options.verbose: + logger = logging.getLogger() + if options.verbose == 1: + logger.setLevel(logging.INFO) + else: + logger.setLevel(logging.DEBUG) + import zeroinstall + logging.info(_("Running 0install %(version)s %(args)s; Python %(python_version)s"), {'version': zeroinstall.version, 'args': repr(command_args), 'python_version': sys.version}) + + args = args[1:] + + if options.with_store: + from zeroinstall import zerostore + for x in options.with_store: + iface_cache.stores.stores.append(zerostore.Store(os.path.abspath(x))) + logging.info(_("Stores search path is now %s"), iface_cache.stores.stores) + + cmd.handle(options, args) + except UsageError: + parser.print_help() + sys.exit(1) + except SafeException, ex: + if verbose: raise + try: + print >>sys.stderr, unicode(ex) + except: + print >>sys.stderr, repr(ex) + sys.exit(1) + return diff --git a/zeroinstall/cmd/select.py b/zeroinstall/cmd/select.py new file mode 100644 index 0000000..c071b1e --- /dev/null +++ b/zeroinstall/cmd/select.py @@ -0,0 +1,212 @@ +""" +The B{0install select} command-line interface. +""" + +# Copyright (C) 2011, Thomas Leonard +# See the README file for details, or visit http://0install.net. + +from optparse import OptionParser +import os, sys +import logging + +from zeroinstall import cmd, SafeException, _ +from zeroinstall.cmd import UsageError +from zeroinstall.injector import model, autopolicy, selections, handler +from zeroinstall.injector.iface_cache import iface_cache + +syntax = "URI" + +def add_options(parser): + parser.add_option("", "--before", help=_("choose a version before this"), metavar='VERSION') + parser.add_option("", "--command", help=_("command to select"), metavar='COMMAND') + parser.add_option("", "--cpu", help=_("target CPU type"), metavar='CPU') + parser.add_option("", "--message", help=_("message to display when interacting with user")) + parser.add_option("", "--not-before", help=_("minimum version to choose"), metavar='VERSION') + parser.add_option("-o", "--offline", help=_("try to avoid using the network"), action='store_true') + parser.add_option("", "--os", help=_("target operation system type"), metavar='OS') + parser.add_option("-r", "--refresh", help=_("refresh all used interfaces"), action='store_true') + parser.add_option("-s", "--source", help=_("select source code"), action='store_true') + parser.add_option("", "--xml", help=_("write selected versions as XML"), action='store_true') + +def get_selections(options, iface_uri, select_only, download_only, test_callback): + """Get selections for iface_uri, according to the options passed. + Will switch to GUI mode if necessary. + @param options: options from OptionParser + @param iface_uri: canonical URI of the interface + @param select_only: return immediately even if the selected versions aren't cached + @param download_only: wait for stale feeds, and display GUI button as Download, not Run + @return: the selected versions, or None if the user cancels + @rtype: L{selections.Selections} | None + """ + root_iface = iface_cache.get_interface(iface_uri) + + if os.isatty(1): + h = handler.ConsoleHandler() + else: + h = handler.Handler() + + command_name = options.command + if command_name is None: + command_name = 'run' + elif command_name == '': + command_name = None + policy = autopolicy.AutoPolicy(iface_uri, + handler = h, + download_only = True, # unused? + src = options.source, + command = command_name) + + if options.before or options.not_before: + policy.solver.extra_restrictions[root_iface] = [ + model.VersionRangeRestriction(model.parse_version(options.before), + model.parse_version(options.not_before))] + + if options.os or options.cpu: + from zeroinstall.injector import arch + policy.target_arch = arch.get_architecture(options.os, options.cpu) + + if options.offline: + policy.network_use = model.network_offline + + # Note that need_download() triggers a solve + if options.refresh or options.gui: + # We could run immediately, but the user asked us not to + can_run_immediately = False + else: + if select_only: + # --select-only: we only care that we've made a selection, not that we've cached the implementations + policy.need_download() + can_run_immediately = policy.ready + else: + can_run_immediately = not policy.need_download() + + stale_feeds = [feed for feed in policy.solver.feeds_used if + not feed.startswith('distribution:') and # Ignore (memory-only) PackageKit feeds + policy.is_stale(iface_cache.get_feed(feed))] + + if download_only and stale_feeds: + can_run_immediately = False + + if can_run_immediately: + if stale_feeds: + if policy.network_use == model.network_offline: + logging.debug(_("No doing background update because we are in off-line mode.")) + else: + # There are feeds we should update, but we can run without them. + # Do the update in the background while the program is running. + from zeroinstall.injector import background + background.spawn_background_update(policy, options.verbose > 0) + return policy.solver.selections + + # If the user didn't say whether to use the GUI, choose for them. + if options.gui is None and os.environ.get('DISPLAY', None): + options.gui = True + # If we need to download anything, we might as well + # refresh all the feeds first. + options.refresh = True + logging.info(_("Switching to GUI mode... (use --console to disable)")) + + if options.gui: + gui_args = [] + if download_only: + # Just changes the button's label + gui_args.append('--download-only') + if options.refresh: + gui_args.append('--refresh') + if options.not_before: + gui_args.insert(0, options.not_before) + gui_args.insert(0, '--not-before') + if options.before: + gui_args.insert(0, options.before) + gui_args.insert(0, '--before') + if options.source: + gui_args.insert(0, '--source') + if options.message: + gui_args.insert(0, options.message) + gui_args.insert(0, '--message') + if options.verbose: + gui_args.insert(0, '--verbose') + if options.verbose > 1: + gui_args.insert(0, '--verbose') + if options.cpu: + gui_args.insert(0, options.cpu) + gui_args.insert(0, '--cpu') + if options.os: + gui_args.insert(0, options.os) + gui_args.insert(0, '--os') + if options.with_store: + for x in options.with_store: + gui_args += ['--with-store', x] + if select_only: + gui_args.append('--select-only') + if command_name is not None: + gui_args.append('--command') + gui_args.append(command_name) + + from zeroinstall import helpers + sels = helpers.get_selections_gui(iface_uri, gui_args, test_callback) + + if not sels: + return None # Aborted + else: + # Note: --download-only also makes us stop and download stale feeds first. + downloaded = policy.solve_and_download_impls(refresh = options.refresh or download_only or False, + select_only = select_only) + if downloaded: + policy.handler.wait_for_blocker(downloaded) + sels = selections.Selections(policy) + + return sels + +def handle(options, args): + if len(args) != 1: + raise UsageError() + uri = args[0] + iface_uri = model.canonical_iface_uri(args[0]) + + sels = get_selections(options, iface_uri, + select_only = True, download_only = False, test_callback = None) + if not sels: + sys.exit(1) # Aborted by user + + if options.xml: + doc = sels.toDOM() + doc.writexml(sys.stdout) + sys.stdout.write('\n') + else: + from zeroinstall import zerostore + done = set() # detect cycles + def print_node(uri, command, indent): + if uri in done: return + done.add(uri) + impl = sels.selections.get(uri, None) + print indent + "- URI:", uri + if impl: + print indent + " Version:", impl.version + try: + if impl.id.startswith('package:'): + path = "(" + impl.id + ")" + else: + path = impl.local_path or iface_cache.stores.lookup_any(impl.digests) + except zerostore.NotStored: + path = "(not cached)" + print indent + " Path:", path + indent += " " + deps = impl.dependencies + if command is not None: + deps += sels.commands[command].requires + for child in deps: + if isinstance(child, model.InterfaceDependency): + if child.qdom.name == 'runner': + child_command = command + 1 + else: + child_command = None + print_node(child.interface, child_command, indent) + else: + print indent + " No selected version" + + + if sels.commands: + print_node(sels.interface, 0, "") + else: + print_node(sels.interface, None, "") -- 2.11.4.GIT