From f18a45450d8b761a9ce841937432b7d95036a805 Mon Sep 17 00:00:00 2001 From: Thomas Leonard Date: Sat, 30 Oct 2010 12:20:19 +0100 Subject: [PATCH] Added support for element This is a generalisation of the 'main', 'self-test' and 'compile:command' attributes. Some side effects of this: - You can no longer get selections if the root has no main command. Before, this was only detected when running. - We now select the best candidate *that can be run*. Before, we selected the best candidate and then failed if it wasn't runnable. - We only check that the main path is relative when running, not when loading (this is so that other runners can have other rules) --- tests/Binary.xml | 2 +- tests/Command.xml | 17 ++++ tests/Compiler.xml | 2 +- tests/Langs.xml | 136 +++++++++++++++---------------- tests/MultiArch.xml | 2 +- tests/Ranking.xml | 24 +++--- tests/Recursive.xml | 2 +- tests/Source.xml | 3 +- tests/testautopolicy.py | 14 ++-- tests/testdownload.py | 4 +- tests/testlaunch.py | 27 ++++++- tests/testmodel.py | 17 ++++ tests/testpolicy.py | 1 + tests/testreader.py | 22 ------ tests/testsat.py | 1 + tests/testselections.py | 30 ++++++- tests/testsolver.py | 9 ++- zeroinstall/injector/model.py | 86 ++++++++++++++++---- zeroinstall/injector/policy.py | 22 ++++-- zeroinstall/injector/qdom.py | 37 ++++++++- zeroinstall/injector/run.py | 91 ++++++++++++--------- zeroinstall/injector/sat.py | 3 +- zeroinstall/injector/selections.py | 46 ++++++----- zeroinstall/injector/solver.py | 158 +++++++++++++++++++++++++++++++------ 24 files changed, 527 insertions(+), 229 deletions(-) create mode 100644 tests/Command.xml rewrite tests/Langs.xml (91%) rewrite tests/Ranking.xml (69%) diff --git a/tests/Binary.xml b/tests/Binary.xml index 38f35f7..30c89e2 100644 --- a/tests/Binary.xml +++ b/tests/Binary.xml @@ -5,7 +5,7 @@ Binary Binary - + diff --git a/tests/Command.xml b/tests/Command.xml new file mode 100644 index 0000000..37c3ac9 --- /dev/null +++ b/tests/Command.xml @@ -0,0 +1,17 @@ + + + Commands + Local feed + + + + + + + + + + + + diff --git a/tests/Compiler.xml b/tests/Compiler.xml index 23c0b3e..ee507b7 100644 --- a/tests/Compiler.xml +++ b/tests/Compiler.xml @@ -4,7 +4,7 @@ Compiler Compiler Compiler - + diff --git a/tests/Langs.xml b/tests/Langs.xml dissimilarity index 91% index 4e6cce1..1b44154 100644 --- a/tests/Langs.xml +++ b/tests/Langs.xml @@ -1,67 +1,69 @@ - - - Langs - Langs - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + + + Langs + Langs + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/tests/MultiArch.xml b/tests/MultiArch.xml index 4deb79e..6dd5825 100644 --- a/tests/MultiArch.xml +++ b/tests/MultiArch.xml @@ -2,7 +2,7 @@ MultiArch MultiArch - + diff --git a/tests/Ranking.xml b/tests/Ranking.xml dissimilarity index 69% index 14627f8..3bb4df5 100644 --- a/tests/Ranking.xml +++ b/tests/Ranking.xml @@ -1,11 +1,13 @@ - - - Ranking - ranking - - - - - - - + + + Ranking + ranking + + + + + + + + + diff --git a/tests/Recursive.xml b/tests/Recursive.xml index 2baea58..084cd8d 100644 --- a/tests/Recursive.xml +++ b/tests/Recursive.xml @@ -4,7 +4,7 @@ Recursive Recursive Recursive - + diff --git a/tests/Source.xml b/tests/Source.xml index de66157..729c97a 100644 --- a/tests/Source.xml +++ b/tests/Source.xml @@ -1,5 +1,6 @@ Source Source @@ -10,7 +11,7 @@ - + diff --git a/tests/testautopolicy.py b/tests/testautopolicy.py index f330986..6571d43 100755 --- a/tests/testautopolicy.py +++ b/tests/testautopolicy.py @@ -54,7 +54,7 @@ class TestAutoPolicy(BaseTest): Foo Foo Foo - + """ % foo_iface_uri) @@ -105,7 +105,7 @@ class TestAutoPolicy(BaseTest): policy.download_and_execute(['Hello']) assert 0 except model.SafeException, ex: - assert "library" in str(ex) + assert "library" in str(ex), ex tmp.close() def testNeedDL(self): @@ -227,7 +227,7 @@ class TestAutoPolicy(BaseTest): Bar Bar Bar - + """ % foo_iface_uri) @@ -307,7 +307,7 @@ class TestAutoPolicy(BaseTest): Foo Foo Foo - + """ % foo_iface_uri) policy = autopolicy.AutoPolicy(foo_iface_uri, download_only = False) @@ -318,7 +318,7 @@ class TestAutoPolicy(BaseTest): policy.download_and_execute([]) assert False except model.SafeException, ex: - assert "Can't find all required implementations" in str(ex) + assert "has no usable implementations" in str(ex), ex def testNoArchives(self): self.cache_iface(foo_iface_uri, @@ -329,7 +329,7 @@ class TestAutoPolicy(BaseTest): Foo Foo Foo - + """ % foo_iface_uri) policy = autopolicy.AutoPolicy(foo_iface_uri, download_only = False) @@ -385,7 +385,7 @@ class TestAutoPolicy(BaseTest): Foo Foo Foo - + diff --git a/tests/testdownload.py b/tests/testdownload.py index 662b596..598185a 100755 --- a/tests/testdownload.py +++ b/tests/testdownload.py @@ -109,7 +109,7 @@ class TestDownload(BaseTest): policy.download_and_execute(['Hello']) assert 0 except model.SafeException, ex: - if "Can't find all required implementations" not in str(ex): + if "has no usable implementations" not in str(ex): raise ex if "Not signed with a trusted key" not in str(policy.handler.ex): raise ex @@ -125,7 +125,7 @@ class TestDownload(BaseTest): policy.download_and_execute(['Hello']) assert 0 except model.SafeException, ex: - if "Can't find all required implementations" not in str(ex): + if "has no usable implementations" not in str(ex): raise ex if "Not signed with a trusted key" not in str(policy.handler.ex): raise diff --git a/tests/testlaunch.py b/tests/testlaunch.py index 03da8a7..3da9573 100755 --- a/tests/testlaunch.py +++ b/tests/testlaunch.py @@ -8,6 +8,7 @@ import logging foo_iface_uri = 'http://foo' sys.path.insert(0, '..') +from zeroinstall import SafeException from zeroinstall.injector import autopolicy, model, cli, namespaces, qdom, selections from zeroinstall.zerostore import Store; Store._add_with_helper = lambda *unused: False from zeroinstall.support import basedir @@ -98,11 +99,33 @@ class TestLaunch(BaseTest): def testRun(self): out, err = self.run_0launch(['Local.xml']) self.assertEquals("", out) - assert "test-echo' does not exist" in err + assert "test-echo' does not exist" in err, err + + def testAbsMain(self): + tmp = tempfile.NamedTemporaryFile(prefix = 'test-') + tmp.write( +""" + + Foo + Foo + Foo + + + +""" % foo_iface_uri) + tmp.flush() + policy = autopolicy.AutoPolicy(tmp.name) + try: + policy.download_and_execute([]) + assert False + except SafeException, ex: + assert 'Command path must be relative' in str(ex), ex def testOffline(self): out, err = self.run_0launch(['--offline', 'http://foo/d']) - self.assertEquals("Can't find all required implementations:\n- -> None\n", err) + self.assertEquals("Interface 'http://foo/d' has no usable implementations\n", err) self.assertEquals("", out) def testDisplay(self): diff --git a/tests/testmodel.py b/tests/testmodel.py index 7025aad..e7b954e 100755 --- a/tests/testmodel.py +++ b/tests/testmodel.py @@ -116,6 +116,23 @@ class TestModel(BaseTest): finally: basetest.test_locale = (None, None) + def testCommand(self): + local_path = os.path.join(mydir, 'Command.xml') + dom = qdom.parse(open(local_path)) + feed = model.ZeroInstallFeed(dom, local_path = local_path) + + assert feed.implementations['a'].main == 'foo' + assert feed.implementations['a'].commands['run'].path == 'foo' + assert feed.implementations['a'].commands['test'].path == 'test-foo' + + assert feed.implementations['b'].main == 'bar' + assert feed.implementations['b'].commands['run'].path == 'bar' + assert feed.implementations['b'].commands['test'].path == 'test-foo' + + assert feed.implementations['c'].main == 'baz' + assert feed.implementations['c'].commands['run'].path == 'baz' + assert feed.implementations['c'].commands['test'].path == 'test-baz' + def testStabPolicy(self): i = model.Interface('http://foo') self.assertEquals(None, i.stability_policy) diff --git a/tests/testpolicy.py b/tests/testpolicy.py index c853b91..eb247cc 100755 --- a/tests/testpolicy.py +++ b/tests/testpolicy.py @@ -31,6 +31,7 @@ class TestPolicy(BaseTest): # Now ask for source instead p.src = True + p.command = 'compile' p.recalculate() assert p.implementation[foo].id == 'sha1=234' # The source assert p.implementation[compiler].id == 'sha1=345' # A binary needed to compile it diff --git a/tests/testreader.py b/tests/testreader.py index ec15613..304c982 100755 --- a/tests/testreader.py +++ b/tests/testreader.py @@ -156,28 +156,6 @@ class TestReader(BaseTest): impl = feed.implementations['sha1=123'] assert impl.version == [[1, 0], -1, [3], -2] - def testAbsMain(self): - tmp = tempfile.NamedTemporaryFile(prefix = 'test-') - tmp.write( -""" - - Foo - Foo - Foo - - - -""" % foo_iface_uri) - tmp.flush() - iface = model.Interface(foo_iface_uri) - try: - reader.update(iface, tmp.name) - assert False - except reader.InvalidInterface, ex: - assert 'main' in str(ex) - def testAttrs(self): tmp = tempfile.NamedTemporaryFile(prefix = 'test-') tmp.write( diff --git a/tests/testsat.py b/tests/testsat.py index e6a5169..86ca39d 100755 --- a/tests/testsat.py +++ b/tests/testsat.py @@ -55,6 +55,7 @@ class Program: attrs = { 'id': str(i), 'version': str(version.n), + 'main': 'dummy', } if version.arch: attrs['arch'] = version.arch diff --git a/tests/testselections.py b/tests/testselections.py index 7dbe288..eae5be1 100755 --- a/tests/testselections.py +++ b/tests/testselections.py @@ -67,24 +67,29 @@ class TestSelections(BaseTest): assertSel(s2) def testLocalPath(self): + # 0launch --get-selections Local.xml iface = os.path.join(mydir, "Local.xml") p = policy.Policy(iface) p.need_download() s1 = selections.Selections(p) xml = s1.toDOM().toxml("utf-8") + + # Reload selections and check they're the same root = qdom.parse(StringIO(xml)) s2 = selections.Selections(root) local_path = s2.selections[iface].local_path assert os.path.isdir(local_path), local_path assert not s2.selections[iface].digests, s2.selections[iface].digests + # Add a newer implementation and try again feed = iface_cache.iface_cache.get_feed(iface) impl = model.ZeroInstallImplementation(feed, "foo bar=123", local_path = None) impl.version = model.parse_version('1.0') + impl.commands["run"] = model.Command(qdom.Element(namespaces.XMLNS_IFACE, 'command', {'path': 'dummy'})) impl.add_download_source('http://localhost/bar.tgz', 1000, None) feed.implementations = {impl.id: impl} assert p.need_download() - assert p.ready + assert p.ready, p.solver.get_failure_reason() s1 = selections.Selections(p) xml = s1.toDOM().toxml("utf-8") root = qdom.parse(StringIO(xml)) @@ -95,5 +100,28 @@ class TestSelections(BaseTest): assert not s2.selections[iface].digests, s2.selections[iface].digests assert s2.selections[iface].id == 'foo bar=123' + def testCommands(self): + iface = os.path.join(mydir, "Command.xml") + p = policy.Policy(iface) + p.need_download() + + impl, = p.solver.selections.values() + assert impl.id == 'c' + assert impl.main == 'baz' + + s1 = selections.Selections(p) + assert s1.command.path == 'baz' + xml = s1.toDOM().toxml("utf-8") + root = qdom.parse(StringIO(xml)) + s2 = selections.Selections(root) + + assert s2.command.path == 'baz' + impl, = s2.selections.values() + assert impl.id == 'c' + + assert s2.command.qdom.attrs['http://custom attr'] == 'namespaced' + custom_element, = s2.command.qdom.childNodes + assert custom_element.name == 'child' + if __name__ == '__main__': unittest.main() diff --git a/tests/testsolver.py b/tests/testsolver.py index 643f2bc..b479e6b 100755 --- a/tests/testsolver.py +++ b/tests/testsolver.py @@ -33,8 +33,9 @@ class TestSolver(BaseTest): # Now ask for source instead s.solve('http://foo/Binary.xml', - arch.SourceArchitecture(binary_arch)) - assert s.ready + arch.SourceArchitecture(binary_arch), + command_name = 'compile') + assert s.ready, s.get_failure_reason() assert s.feeds_used == set([foo.uri, foo_src.uri, compiler.uri]), s.feeds_used assert s.selections[foo].id == 'sha1=234' # The source assert s.selections[compiler].id == 'sha1=345' # A binary needed to compile it @@ -53,8 +54,8 @@ class TestSolver(BaseTest): binary_arch = arch.Architecture({None: 1}, {None: 1}) s.record_details = True - s.solve('http://foo/Binary.xml', arch.SourceArchitecture(binary_arch)) - assert s.ready + s.solve('http://foo/Binary.xml', arch.SourceArchitecture(binary_arch), command_name = 'compile') + assert s.ready, s.get_failure_reason() foo_src_impls = iface_cache.get_feed(foo_src.uri).implementations foo_impls = iface_cache.get_feed(foo.uri).implementations diff --git a/zeroinstall/injector/model.py b/zeroinstall/injector/model.py index ad9c053..d9bbba1 100644 --- a/zeroinstall/injector/model.py +++ b/zeroinstall/injector/model.py @@ -18,6 +18,7 @@ import os, re, locale from logging import info, debug, warn from zeroinstall import SafeException, version from zeroinstall.injector.namespaces import XMLNS_IFACE +from zeroinstall.injector import qdom # Element names for bindings in feed files binding_names = frozenset(['environment', 'overlay']) @@ -400,6 +401,32 @@ class DistributionSource(RetrievalMethod): self.install = install self.needs_confirmation = needs_confirmation +class Command(object): + """A Command is a way of running an Implementation as a program.""" + + __slots__ = ['qdom', '_depends'] + + def __init__(self, qdom): + assert qdom.name == 'command', 'not : %s' % qdom + self.qdom = qdom + self._depends = None + + path = property(lambda self: self.qdom.attrs.get("path", None)) + + def _toxml(self, doc, prefixes): + return self.qdom.toDOM(doc, prefixes) + + @property + def requires(self): + if self._depends is None: + depends = [] + for child in self.qdom.childNodes: + if child.name == 'requires': + dep = process_depends(child) + depends.append(dep) + self._depends = depends + return self._depends + class Implementation(object): """An Implementation is a package which implements an Interface. @ivar download_sources: list of methods of getting this implementation @@ -416,7 +443,8 @@ class Implementation(object): @type langs: str @ivar requires: interfaces this package depends on @type requires: [L{Dependency}] - @ivar main: the default file to execute when running as a program + @ivar commands: ways to execute as a program + @type commands: {str: Command} @ivar metadata: extra metadata from the feed @type metadata: {"[URI ]localName": str} @ivar id: a unique identifier for this Implementation @@ -431,14 +459,13 @@ class Implementation(object): # Note: user_stability shouldn't really be here __slots__ = ['upstream_stability', 'user_stability', 'langs', - 'requires', 'main', 'metadata', 'download_sources', + 'requires', 'metadata', 'download_sources', 'commands', 'id', 'feed', 'version', 'released', 'bindings', 'machine'] def __init__(self, feed, id): assert id self.feed = feed self.id = id - self.main = None self.user_stability = None self.upstream_stability = None self.metadata = {} # [URI + " "] + localName -> value @@ -449,6 +476,7 @@ class Implementation(object): self.langs = "" self.machine = None self.bindings = [] + self.commands = {} def get_stability(self): return self.user_stability or self.upstream_stability or testing @@ -482,6 +510,21 @@ class Implementation(object): digests = None requires_root_install = False + def _get_main(self): + """"@deprecated: use commands["run"] instead""" + main = self.commands.get("run", None) + if main is not None: + return main.path + return None + def _set_main(self, path): + """"@deprecated: use commands["run"] instead""" + if path is None: + if "run" in self.commands: + del self.commands["run"] + else: + self.commands["run"] = Command(qdom.Element(XMLNS_IFACE, 'command', {'path': path})) + main = property(_get_main, _set_main) + class DistributionImplementation(Implementation): """An implementation provided by the distribution. Information such as the version comes from the package manager. @@ -724,7 +767,7 @@ class ZeroInstallFeed(object): if not self.summary: raise InvalidInterface(_("Missing in feed")) - def process_group(group, group_attrs, base_depends, base_bindings): + def process_group(group, group_attrs, base_depends, base_bindings, base_commands): for item in group.childNodes: if item.uri != XMLNS_IFACE: continue @@ -733,6 +776,7 @@ class ZeroInstallFeed(object): depends = base_depends[:] bindings = base_bindings[:] + commands = base_commands.copy() item_attrs = _merge_attrs(group_attrs, item) @@ -747,13 +791,28 @@ class ZeroInstallFeed(object): if child.name == 'requires': dep = process_depends(child) depends.append(dep) + elif child.name == 'command': + command_name = child.attrs.get('name', None) + if not command_name: + raise InvalidInterface('Missing name for ') + commands[command_name] = Command(child) elif child.name in binding_names: bindings.append(process_binding(child)) + for attr, command in [('main', 'run'), + ('self-test', 'test')]: + value = item.attrs.get(attr, None) + if value is not None: + commands[command] = Command(qdom.Element(XMLNS_IFACE, 'command', {'path': value})) + + compile_command = item.attrs.get('http://zero-install.sourceforge.net/2006/namespaces/0compile command') + if compile_command is not None: + commands['compile'] = Command(qdom.Element(XMLNS_IFACE, 'command', {'shell-command': compile_command})) + if item.name == 'group': - process_group(item, item_attrs, depends, bindings) + process_group(item, item_attrs, depends, bindings, commands) elif item.name == 'implementation': - process_impl(item, item_attrs, depends, bindings) + process_impl(item, item_attrs, depends, bindings, commands) elif item.name == 'package-implementation': if depends: warn("A with dependencies in %s!", self.url) @@ -761,7 +820,7 @@ class ZeroInstallFeed(object): else: assert 0 - def process_impl(item, item_attrs, depends, bindings): + def process_impl(item, item_attrs, depends, bindings, commands): id = item.getAttribute('id') if id is None: raise InvalidInterface(_("Missing 'id' attribute on %s") % item) @@ -793,11 +852,8 @@ class ZeroInstallFeed(object): raise InvalidInterface(_("Missing version attribute")) impl.version = parse_version(version) - item_main = item_attrs.get('main', None) - if item_main and item_main.startswith('/'): - raise InvalidInterface(_("'main' attribute must be relative, but '%s' starts with '/'!") % - item_main) - impl.main = item_main + item_main = commands.get('run', None) + impl.commands = commands impl.released = item_attrs.get('released', None) impl.langs = item_attrs.get('langs', '') @@ -858,9 +914,11 @@ class ZeroInstallFeed(object): impl.download_sources.append(recipe) root_attrs = {'stability': 'testing'} + root_commands = {} if main: - root_attrs['main'] = main - process_group(feed_element, root_attrs, [], []) + info("Note: @main on document element is deprecated in %s", self) + root_commands['run'] = Command(qdom.Element(XMLNS_IFACE, 'command', {'path': main})) + process_group(feed_element, root_attrs, [], [], root_commands) def get_distro_feed(self): """Does this feed contain any elements? diff --git a/zeroinstall/injector/policy.py b/zeroinstall/injector/policy.py index 16210b2..4db437d 100644 --- a/zeroinstall/injector/policy.py +++ b/zeroinstall/injector/policy.py @@ -50,7 +50,7 @@ class Policy(object): @ivar stale_feeds: set of feeds which are present but haven't been checked for a long time @type stale_feeds: set """ - __slots__ = ['root', 'watchers', + __slots__ = ['root', 'watchers', 'command', 'freshness', 'handler', '_warned_offline', 'target_arch', 'src', 'stale_feeds', 'solver', '_fetcher'] @@ -64,17 +64,25 @@ class Policy(object): ready = property(lambda self: self.solver.ready) - def __init__(self, root, handler = None, src = False): + def __init__(self, root, handler = None, src = False, command = None): """ @param root: The URI of the root interface (the program we want to run). @param handler: A handler for main-loop integration. @type handler: L{zeroinstall.injector.handler.Handler} @param src: Whether we are looking for source code. @type src: bool + @param command: The name of the command to run (e.g. 'run', 'test', 'compile', etc) + @type command: str """ self.watchers = [] self.src = src # Root impl must be a "src" machine type self.stale_feeds = set() + if command is None: + if src: + command = 'compile' + else: + command = 'run' + self.command = command from zeroinstall.injector.solver import DefaultSolver self.solver = DefaultSolver(network_full, iface_cache, iface_cache.stores) @@ -150,7 +158,7 @@ class Policy(object): host_arch = self.target_arch if self.src: host_arch = arch.SourceArchitecture(host_arch) - self.solver.solve(self.root, host_arch) + self.solver.solve(self.root, host_arch, command_name = self.command) if self.network_use == network_offline: fetch_stale_interfaces = False @@ -324,7 +332,7 @@ class Policy(object): try_quick_exit = not (force or update_local) while True: - self.solver.solve(self.root, host_arch) + self.solver.solve(self.root, host_arch, command_name = self.command) for w in self.watchers: w() if try_quick_exit and self.solver.ready: @@ -382,9 +390,7 @@ class Policy(object): tasks.check(refreshed) if not self.solver.ready: - raise SafeException(_("Can't find all required implementations:") + '\n' + - '\n'.join(["- %s -> %s" % (iface, self.solver.selections[iface]) - for iface in self.solver.selections])) + raise self.solver.get_failure_reason() if not select_only: downloaded = self.download_uncached_implementations() @@ -399,7 +405,7 @@ class Policy(object): host_arch = self.target_arch if self.src: host_arch = arch.SourceArchitecture(host_arch) - self.solver.solve(self.root, host_arch) + self.solver.solve(self.root, host_arch, command_name = self.command) for w in self.watchers: w() if not self.solver.ready: diff --git a/zeroinstall/injector/qdom.py b/zeroinstall/injector/qdom.py index 7cf5426..0c15f02 100644 --- a/zeroinstall/injector/qdom.py +++ b/zeroinstall/injector/qdom.py @@ -16,7 +16,7 @@ class Element(object): @type uri: str @ivar name: the element's localName @type name: str - @ivar attrs: the element's attributes (key is in the form [namespace " "] localName + @ivar attrs: the element's attributes (key is in the form [namespace " "] localName) @type attrs: {str: str} @ivar childNodes: children @type childNodes: [L{Element}] @@ -27,6 +27,7 @@ class Element(object): self.uri = uri self.name = name self.attrs = attrs.copy() + self.content = None self.childNodes = [] def __str__(self): @@ -42,6 +43,25 @@ class Element(object): def getAttribute(self, name): return self.attrs.get(name, None) + def toDOM(self, doc, prefixes): + """Create a DOM Element for this qdom.Element. + @param doc: document to use to create the element + @return: the new element + """ + elem = doc.createElementNS(self.uri, self.name) + for fullname, value in self.attrs.iteritems(): + if ' ' in fullname: + ns, localName = fullname.split(' ', 1) + name = prefixes.get(ns) + ':' + localName + else: + ns, name = None, fullname + elem.setAttributeNS(ns, name, value) + for child in self.childNodes: + elem.appendChild(child.toDOM(doc, prefixes)) + if self.content: + elem.appendChild(doc.createTextNode(self.content)) + return elem + class QSAXhandler: """SAXHandler that builds a tree of L{Element}s""" def __init__(self): @@ -83,3 +103,18 @@ def parse(source): parser.ParseFile(source) return handler.doc + +class Prefixes: + """Keep track of namespace prefixes. Used when serialising a document. + @since: 0.51 + """ + def __init__(self): + self.prefixes = {} + + def get(self, ns): + prefix = self.prefixes.get(ns, None) + if prefix: + return prefix + prefix = 'ns%d' % len(self.prefixes) + self.prefixes[ns] = prefix + return prefix diff --git a/zeroinstall/injector/run.py b/zeroinstall/injector/run.py index 7031b73..05424ee 100644 --- a/zeroinstall/injector/run.py +++ b/zeroinstall/injector/run.py @@ -47,33 +47,6 @@ def _do_bindings(impl, bindings): def _get_implementation_path(impl): return impl.local_path or iface_cache.stores.lookup_any(impl.digests) -def execute_selections(selections, prog_args, dry_run = False, main = None, wrapper = None): - """Execute program. On success, doesn't return. On failure, raises an Exception. - Returns normally only for a successful dry run. - @param selections: the selected versions - @type selections: L{selections.Selections} - @param prog_args: arguments to pass to the program - @type prog_args: [str] - @param dry_run: if True, just print a message about what would have happened - @type dry_run: bool - @param main: the name of the binary to run, or None to use the default - @type main: str - @param wrapper: a command to use to actually run the binary, or None to run the binary directly - @type wrapper: str - @since: 0.27 - @precondition: All implementations are in the cache. - """ - sels = selections.selections - for selection in sels.values(): - _do_bindings(selection, selection.bindings) - for dep in selection.dependencies: - dep_impl = sels[dep.interface] - if not dep_impl.id.startswith('package:'): - _do_bindings(dep_impl, dep.bindings) - - root_impl = sels[selections.interface] - _execute(root_impl, prog_args, dry_run, main, wrapper) - def test_selections(selections, prog_args, dry_run, main, wrapper = None): """Run the program in a child process, collecting stdout and stderr. @return: the output produced by the process @@ -113,31 +86,71 @@ def test_selections(selections, prog_args, dry_run, main, wrapper = None): return results -def _execute(root_impl, prog_args, dry_run, main, wrapper): - assert root_impl is not None +def execute_selections(selections, prog_args, dry_run = False, main = None, wrapper = None): + """Execute program. On success, doesn't return. On failure, raises an Exception. + Returns normally only for a successful dry run. + @param selections: the selected versions + @type selections: L{selections.Selections} + @param prog_args: arguments to pass to the program + @type prog_args: [str] + @param dry_run: if True, just print a message about what would have happened + @type dry_run: bool + @param main: the name of the binary to run, or None to use the default + @type main: str + @param wrapper: a command to use to actually run the binary, or None to run the binary directly + @type wrapper: str + @since: 0.27 + @precondition: All implementations are in the cache. + """ + sels = selections.selections + for selection in sels.values(): + _do_bindings(selection, selection.bindings) + for dep in selection.dependencies: + dep_impl = sels[dep.interface] + if not dep_impl.id.startswith('package:'): + _do_bindings(dep_impl, dep.bindings) + + root_sel = sels[selections.interface] + + assert root_sel is not None + + command = selections.command - if root_impl.id.startswith('package:'): - main = main or root_impl.main + # Process command's dependencies' bindings + for dep in command.requires: + dep_impl = sels[dep.interface] + if not dep_impl.id.startswith('package:'): + _do_bindings(dep_impl, dep.bindings) + + command_path = command.path + + if root_sel.id.startswith('package:'): + main = main or command_path prog_path = main else: + if command_path and command_path.startswith('/'): + raise SafeException(_("Command path must be relative, but '%s' starts with '/'!") % + command_path) if main is None: - main = root_impl.main + main = command_path # User didn't specify a main path elif main.startswith('/'): - main = main[1:] - elif root_impl.main: - main = os.path.join(os.path.dirname(root_impl.main), main) + main = main[1:] # User specified a path relative to the package root + elif command_path: + main = os.path.join(os.path.dirname(command_path), main) # User main is relative to command's name + if main is not None: - prog_path = os.path.join(_get_implementation_path(root_impl), main) + prog_path = os.path.join(_get_implementation_path(root_sel), main) if main is None: - raise SafeException(_("Implementation '%s' cannot be executed directly; it is just a library " + # This shouldn't happen, because the solve would fail first. + raise Exception(_("Implementation '%s' cannot be executed directly; it is just a library " "to be used by other programs (or missing 'main' attribute)") % - root_impl) + root_sel) if not os.path.exists(prog_path): raise SafeException(_("File '%(program_path)s' does not exist.\n" "(implementation '%(implementation_id)s' + program '%(main)s')") % - {'program_path': prog_path, 'implementation_id': root_impl.id, + {'program_path': prog_path, 'implementation_id': root_sel.id, 'main': main}) if wrapper: prog_args = ['-c', wrapper + ' "$@"', '-', prog_path] + list(prog_args) diff --git a/zeroinstall/injector/sat.py b/zeroinstall/injector/sat.py index 850fed8..0ac6670 100644 --- a/zeroinstall/injector/sat.py +++ b/zeroinstall/injector/sat.py @@ -23,7 +23,7 @@ from logging import warn def debug(msg, *args): return - #print "SAT:", msg % args + print "SAT:", msg % args # variables are numbered from 0 # literals have the same number as the corresponding variable, @@ -606,6 +606,7 @@ class SATProblem(object): # If it leads to a conflict, we'll backtrack and # try it the other way. lit = decide() + #print "TRYING:", self.name_lit(lit) assert lit is not None, "decide function returned None!" assert self.lit_value(lit) is None self.trail_lim.append(len(self.trail)) diff --git a/zeroinstall/injector/selections.py b/zeroinstall/injector/selections.py index 8fe862f..9d202af 100644 --- a/zeroinstall/injector/selections.py +++ b/zeroinstall/injector/selections.py @@ -8,9 +8,9 @@ Load and save a set of chosen implementations. from zeroinstall import _ from zeroinstall.injector.policy import Policy -from zeroinstall.injector.model import process_binding, process_depends, binding_names +from zeroinstall.injector.model import process_binding, process_depends, binding_names, Command from zeroinstall.injector.namespaces import XMLNS_IFACE -from zeroinstall.injector.qdom import Element +from zeroinstall.injector.qdom import Element, Prefixes from zeroinstall.support import tasks class Selection(object): @@ -86,10 +86,12 @@ class Selections(object): A selected set of components which will make up a complete program. @ivar interface: the interface of the program @type interface: str + @ivar command: how to run this selection + @type command: {L{Command}} @ivar selections: the selected implementations @type selections: {str: L{Selection}} """ - __slots__ = ['interface', 'selections'] + __slots__ = ['interface', 'selections', 'command'] def __init__(self, source): """Constructor. @@ -113,17 +115,21 @@ class Selections(object): @param policy: the policy giving the selected implementations.""" self.interface = policy.root self.selections = policy.solver.selections.selections + self.command = policy.solver.selections.command def _init_from_qdom(self, root): """Parse and load a selections document. @param root: a saved set of selections.""" self.interface = root.getAttribute('interface') assert self.interface + self.command = None for selection in root.childNodes: if selection.uri != XMLNS_IFACE: continue if selection.name != 'selection': + if selection.name == 'command': + self.command = Command(selection) continue requires = [] @@ -149,8 +155,14 @@ class Selections(object): if alg in ('sha1', 'sha1new', 'sha256'): digests.append(sel_id) + iface_uri = selection.attrs['interface'] + if iface_uri == self.interface: + main = selection.attrs.get('main', None) + if main is not None: + self.command = Command(Element(XMLNS_IFACE, 'command', {'path': main})) + s = XMLSelection(requires, bindings, selection.attrs, digests) - self.selections[selection.attrs['interface']] = s + self.selections[iface_uri] = s def toDOM(self): """Create a DOM document for the selected implementations. @@ -171,15 +183,7 @@ class Selections(object): root.setAttributeNS(None, 'interface', self.interface) - def ensure_prefix(prefixes, ns): - prefix = prefixes.get(ns, None) - if prefix: - return prefix - prefix = 'ns%d' % len(prefixes) - prefixes[ns] = prefix - return prefix - - prefixes = {} + prefixes = Prefixes() for iface, selection in sorted(self.selections.items()): selection_elem = doc.createElementNS(XMLNS_IFACE, 'selection') @@ -189,11 +193,12 @@ class Selections(object): for name, value in selection.attrs.iteritems(): if ' ' in name: ns, localName = name.split(' ', 1) - selection_elem.setAttributeNS(ns, ensure_prefix(prefixes, ns) + ':' + localName, value) - elif name != 'from-feed': - selection_elem.setAttributeNS(None, name, value) - elif value != selection.attrs['interface']: + selection_elem.setAttributeNS(ns, prefixes.get(ns) + ':' + localName, value) + elif name == 'from-feed': # Don't bother writing from-feed attr if it's the same as the interface + if value != selection.attrs['interface']: + selection_elem.setAttributeNS(None, name, value) + elif name not in ('main', 'self-test'): # (replaced by ) selection_elem.setAttributeNS(None, name, value) if selection.digests: @@ -220,12 +225,15 @@ class Selections(object): dep_elem.setAttributeNS(None, localName, dep.metadata[m]) else: ns, localName = parts - dep_elem.setAttributeNS(ns, ensure_prefix(prefixes, ns) + ':' + localName, dep.metadata[m]) + dep_elem.setAttributeNS(ns, prefixes.get(ns) + ':' + localName, dep.metadata[m]) for b in dep.bindings: dep_elem.appendChild(b._toxml(doc)) - for ns, prefix in prefixes.items(): + if self.command: + root.appendChild(self.command._toxml(doc, prefixes)) + + for ns, prefix in prefixes.prefixes.items(): root.setAttributeNS(XMLNS_NAMESPACE, 'xmlns:' + prefix, ns) return doc diff --git a/zeroinstall/injector/solver.py b/zeroinstall/injector/solver.py index 74875b2..bc8106d 100644 --- a/zeroinstall/injector/solver.py +++ b/zeroinstall/injector/solver.py @@ -36,6 +36,17 @@ def _get_cached(stores, impl): except NotStored: return False +class CommandInfo: + def __init__(self, name, command, impl, arch): + self.name = name + self.command = command + self.impl = impl + self.arch = arch + + def __repr__(self): + name = "%s_%s_%s_%s" % (self.impl.feed.get_name(), self.impl.get_version(), self.impl.arch, self.name) + return name.replace('-', '_').replace('.', '_') + class ImplInfo: is_dummy = False @@ -54,6 +65,7 @@ class _DummyImpl(object): requires = [] version = None arch = None + commands = {} def __repr__(self): return "dummy" @@ -91,16 +103,21 @@ class Solver(object): self.record_details = False self.ready = False - def solve(self, root_interface, root_arch): + def solve(self, root_interface, root_arch, command_name = 'run'): """Get the best implementation of root_interface and all of its dependencies. @param root_interface: the URI of the program to be solved @type root_interface: str @param root_arch: the desired target architecture @type root_arch: L{arch.Architecture} + @param command_name: which element to select + @type command_name: str @postcondition: self.ready, self.selections and self.feeds_used are updated""" raise NotImplementedError("Abstract") class SATSolver(Solver): + __slots__ = ['_failure_reason', 'network_use', 'iface_cache', 'stores', 'help_with_testing', 'extra_restrictions', + 'langs'] + """Converts the problem to a set of pseudo-boolean constraints and uses a PB solver to solve them. @ivar langs: the preferred languages (e.g. ["es_ES", "en"]). Initialised to the current locale. @type langs: str""" @@ -201,7 +218,7 @@ class SATSolver(Solver): return cmp(a.id, b.id) - def solve(self, root_interface, root_arch, closest_match = False): + def solve(self, root_interface, root_arch, command_name = 'run', closest_match = False): # closest_match is used internally. It adds a lowest-ranked # by valid implementation to every interface, so we can always # select something. Useful for diagnostics. @@ -222,6 +239,7 @@ class SATSolver(Solver): self.details = self.record_details and {} self.selections = None + self._failure_reason = None ifaces_processed = set() @@ -232,10 +250,24 @@ class SATSolver(Solver): impls_for_iface = {} # Iface -> [impl] group_clause_for = {} # Iface URI -> AtMostOneClause | bool + group_clause_for_command = {} # (Iface URI, command name) -> AtMostOneClause | bool + + # Return the dependencies of impl that we should consider. + # Skips dependencies if the use flag isn't what we need. + # (note: impl may also be a model.Command) + def deps_in_use(impl, arch): + for dep in impl.requires: + use = dep.metadata.get("use", None) + if use not in arch.use: + continue + yield dep + # Add a clause so that if requiring_impl_var is True then an implementation + # matching 'dependency' must also be selected. + # Must have already done add_iface on dependency.interface. def find_dependency_candidates(requiring_impl_var, dependency): dep_iface = self.iface_cache.get_interface(dependency.interface) - dep_union = [sat.neg(requiring_impl_var)] + dep_union = [sat.neg(requiring_impl_var)] # Either requiring_impl_var is False, or ... for candidate in impls_for_iface[dep_iface]: for r in dependency.restrictions: if candidate.__class__ is not _DummyImpl and not r.meets_restriction(candidate): @@ -250,6 +282,7 @@ class SATSolver(Solver): if dep_union: problem.add_clause(dep_union) else: + assert 0 # XXX: how can this happen? problem.assign(requiring_impl_var, 0) def is_unusable(impl, restrictions, arch): @@ -298,7 +331,7 @@ class SATSolver(Solver): {'feed': f, 'os': f.os, 'machine': f.machine}) def add_iface(uri, arch): - """Name implementations from feed, assign costs and assert that one one can be selected.""" + """Name implementations from feed and assert that only one can be selected.""" if uri in ifaces_processed: return ifaces_processed.add(uri) iface_name = 'i%d' % len(ifaces_processed) @@ -354,12 +387,8 @@ class SATSolver(Solver): if impl.machine and impl.machine != 'src': impls_for_machine_group[machine_groups.get(impl.machine, 0)].append(v) - for d in impl.requires: + for d in deps_in_use(impl, arch): debug(_("Considering dependency %s"), d) - use = d.metadata.get("use", None) - if use not in arch.use: - info("Skipping dependency; use='%s' not in %s", use, arch.use) - continue add_iface(d.interface, arch.child_arch) @@ -393,7 +422,59 @@ class SATSolver(Solver): if clause is not False: group_clause_for[uri] = clause - add_iface(root_interface, root_arch) + def add_command_iface(uri, arch, command_name): + """Add every in interface 'uri' with this name. + Each one depends on the corresponding implementation and only + one can be selected.""" + + # First ensure that the interface itself has been processed + # We'll reuse the ordering of the implementations to order + # the commands too. + add_iface(uri, arch) + + iface = self.iface_cache.get_interface(uri) + filtered_impls = impls_for_iface[iface] + + var_names = [] + for impl in filtered_impls: + command = impl.commands.get(command_name, None) + if not command: continue + + # We have a candidate . Require that if it's selected + # then we select the corresponding too. + command_var = problem.add_variable(CommandInfo(command_name, command, impl, arch)) + problem.add_clause([impl_to_var[impl], sat.neg(command_var)]) + + var_names.append(command_var) + + for d in deps_in_use(command, arch): + debug(_("Considering command dependency %s"), d) + + add_iface(d.interface, arch.child_arch) + + # Must choose one version of d if impl is selected + find_dependency_candidates(command_var, d) + + return var_names + + commands = add_command_iface(root_interface, root_arch, command_name) + if commands: + problem.add_clause(commands) # At least one + group_clause_for_command[(root_interface, command_name)] = problem.at_most_one(commands) + else: + # (note: might be because we haven't cached it yet) + info("No %s in %s", command_name, root_interface) + + impls = impls_for_iface[self.iface_cache.get_interface(root_interface)] + if impls == [] or (len(impls) == 1 and isinstance(impls[0], _DummyImpl)): + # There were no candidates at all. + self._failure_reason = _("Interface '%s' has no usable implementations") % root_interface + else: + # We had some candidates implementations, but none for the command we need + self._failure_reason = _("Interface '%s' cannot be executed directly; it is just a library " + "to be used by other programs (or missing '%s' command)") % (root_interface, command_name) + + problem.impossible() # Require m to be true if we select an implementation in that group m_groups = [] @@ -413,6 +494,14 @@ class SATSolver(Solver): """Recurse through the current selections until we get to an interface with no chosen version, then tell the solver to try the best version from that.""" + def find_undecided_dep(impl_or_command, arch): + # Check for undecided dependencies of impl_or_command + for dep in deps_in_use(impl_or_command, arch): + dep_lit = find_undecided(dep.interface) + if dep_lit is not None: + return dep_lit + return None + seen = set() def find_undecided(uri): if uri in seen: @@ -428,19 +517,24 @@ class SATSolver(Solver): # Check for undecided dependencies lit_info = problem.get_varinfo_for_lit(lit).obj + return find_undecided_dep(lit_info.impl, lit_info.arch) - for dep in lit_info.impl.requires: - use = dep.metadata.get("use", None) - if use not in lit_info.arch.use: - continue - dep_lit = find_undecided(dep.interface) - if dep_lit is not None: - return dep_lit + def find_undecided_command(uri, name): + if name is None: return find_undecided(uri) - # This whole sub-tree is decided - return None + group = group_clause_for_command[(uri, name)] + lit = group.current + if lit is None: + return group.best_undecided() + # else we've already chosen which to use + + # Check for undecided command-specific dependencies, and then for + # implementation dependencies. + lit_info = problem.get_varinfo_for_lit(lit).obj + return find_undecided_dep(lit_info.command, lit_info.arch) or \ + find_undecided_dep(lit_info.impl, lit_info.arch) - best = find_undecided(root_interface) + best = find_undecided_command(root_interface, command_name) if best is not None: return best @@ -460,7 +554,7 @@ class SATSolver(Solver): # We failed while trying to do a real solve. # Try a closest match solve to get a better # error report for the user. - self.solve(root_interface, root_arch, closest_match = True) + self.solve(root_interface, root_arch, command_name = command_name, closest_match = True) else: self.ready = ready and not closest_match self.selections = selections.Selections(None) @@ -477,12 +571,24 @@ class SATSolver(Solver): impl = lit_info.impl deps = self.requires[lit_info.iface] = [] - for dep in impl.requires: - use = dep.metadata.get("use", None) - if use not in lit_info.arch.use: - continue + for dep in deps_in_use(lit_info.impl, lit_info.arch): deps.append(dep) - + sels[lit_info.iface.uri] = selections.ImplSelection(lit_info.iface.uri, impl, deps) + root_sel = sels.get(root_interface, None) + if root_sel: + self.selections.command = root_sel.impl.commands[command_name] + + def get_failure_reason(self): + """Return an exception explaining why the solve failed.""" + assert not self.ready + + if self._failure_reason: + return model.SafeException(self._failure_reason) + + return model.SafeException(_("Can't find all required implementations:") + '\n' + + '\n'.join(["- %s -> %s" % (iface, self.selections[iface]) + for iface in self.selections])) + DefaultSolver = SATSolver -- 2.11.4.GIT