def testCommands(self): iface = os.path.join(mydir, "Command.xml") p = policy.Policy(iface, config=self.config) p.need_download() assert p.ready impl = p.solver.selections[self.config.iface_cache.get_interface( iface)] assert impl.id == 'c' assert impl.main == 'runnable/missing' dep_impl_uri = impl.commands['run'].requires[0].interface dep_impl = p.solver.selections[self.config.iface_cache.get_interface( dep_impl_uri)] assert dep_impl.id == 'sha1=256' s1 = selections.Selections(p) assert s1.commands[0].path == 'runnable/missing' xml = s1.toDOM().toxml("utf-8") root = qdom.parse(StringIO(xml)) s2 = selections.Selections(root) assert s2.commands[0].path == 'runnable/missing' impl = s2.selections[iface] assert impl.id == 'c' assert s2.commands[0].qdom.attrs['http://custom attr'] == 'namespaced' custom_element = s2.commands[0].qdom.childNodes[0] assert custom_element.name == 'child' dep_impl = s2.selections[dep_impl_uri] assert dep_impl.id == 'sha1=256'
def get_selections(self, prompt = False): if self._selections: assert not prompt return self._selections selections_file = self.config.get('compile', 'selections') if selections_file: if prompt: raise SafeException("Selections are fixed by %s" % selections_file) stream = file(selections_file) try: self._selections = selections.Selections(qdom.parse(stream)) finally: stream.close() from zeroinstall.injector import handler from zeroinstall.injector.config import load_config if os.isatty(1): h = handler.ConsoleHandler() else: h = handler.Handler() config = load_config(h) blocker = self._selections.download_missing(config) if blocker: print "Waiting for selected implementations to be downloaded..." h.wait_for_blocker(blocker) else: command = install_prog + ['download', '--source', '--xml'] if prompt and '--console' not in install_prog: if os.name == 'nt': command[0] += '-win' command.append('--gui') command.append(self.interface) child = subprocess.Popen(command, stdout = subprocess.PIPE) try: self._selections = selections.Selections(qdom.parse(child.stdout)) finally: if child.wait(): raise SafeException(' '.join(repr(x) for x in command) + " failed (exit code %d)" % child.returncode) self.root_impl = self._selections.selections[self.interface] self.orig_srcdir = os.path.realpath(lookup(self.root_impl)) self.user_srcdir = None if os.path.isdir('src'): self.user_srcdir = os.path.realpath('src') if self.user_srcdir == self.orig_srcdir or \ self.user_srcdir.startswith(os.path.join(self.orig_srcdir, '')) or \ self.orig_srcdir.startswith(os.path.join(self.user_srcdir, '')): info("Ignoring 'src' directory because it coincides with %s", self.orig_srcdir) self.user_srcdir = None return self._selections
def testOldCommands(self): command_feed = os.path.join(mydir, 'old-selections.xml') with open(command_feed, 'rb') as stream: s1 = selections.Selections(qdom.parse(stream)) self.assertEqual("run", s1.command) self.assertEqual(2, len(s1.commands)) self.assertEqual("bin/java", s1.commands[1].path) xml = s1.toDOM().toxml("utf-8") root = qdom.parse(BytesIO(xml)) s2 = selections.Selections(root) self.assertEqual("run", s2.command) self.assertEqual(2, len(s2.commands)) self.assertEqual("bin/java", s2.commands[1].path)
def get_selections(self, snapshot_date=None, may_update=False, use_gui=None): """Load the selections. If may_update is True then the returned selections will be cached and available. @param snapshot_date: get a historical snapshot @type snapshot_date: (as returned by L{get_history}) | None @param may_update: whether to check for updates @type may_update: bool @param use_gui: whether to use the GUI for foreground updates @type use_gui: bool | None (never/always/if possible) @return: the selections @rtype: L{selections.Selections}""" if snapshot_date: assert may_update is False, "Can't update a snapshot!" sels_file = os.path.join(self.path, 'selections-' + snapshot_date + '.xml') else: sels_file = os.path.join(self.path, 'selections.xml') try: with open(sels_file, 'rb') as stream: sels = selections.Selections(qdom.parse(stream)) except IOError as ex: if may_update and ex.errno == errno.ENOENT: logger.info("App selections missing: %s", ex) sels = None else: raise if may_update: sels = self._check_for_updates(sels, use_gui) return sels
def handle(config, options, args): if len(args) != 1: raise UsageError() app = config.app_mgr.lookup_app(args[0], missing_ok=True) if app is not None: sels = app.get_selections() r = app.get_requirements() if r.extra_restrictions and not options.xml: print("User-provided restrictions in force:") for uri, expr in r.extra_restrictions.items(): print(" {uri}: {expr}".format(uri=uri, expr=expr)) print() elif os.path.exists(args[0]): with open(args[0], 'rb') as stream: sels = selections.Selections(qdom.parse(stream)) else: raise SafeException(_("Neither an app nor a file: '%s'") % args[0]) if options.root_uri: print(sels.interface) elif options.xml: select.show_xml(sels) else: select.show_human(sels, config.stores)
def ensure_cached(uri, command='run', config=None): """Ensure that an implementation of uri is cached. If not, it downloads one. It uses the GUI if a display is available, or the console otherwise. @param uri: the required interface @type uri: str @return: the selected implementations, or None if the user cancelled @rtype: L{zeroinstall.injector.selections.Selections} """ from zeroinstall.injector import policy, selections if config is None: from zeroinstall.injector.config import load_config config = load_config() p = policy.Policy(uri, command=command, config=config) p.freshness = 0 # Don't check for updates if p.need_download() or not p.ready: if os.environ.get('DISPLAY', None): return get_selections_gui(uri, ['--command', command]) else: done = p.solve_and_download_impls() tasks.wait_for_blocker(done) return selections.Selections(p)
def load_feed(source, local=False, selections_ok=False): """Load a feed from a local file. @param source: the name of the file to read @type source: str @param local: this is a local feed @type local: bool @param selections_ok: if it turns out to be a local selections document, return that instead @type selections_ok: bool @raise InvalidInterface: if the source's syntax is incorrect @return: the new feed @since: 0.48 @see: L{iface_cache.iface_cache}, which uses this to load the feeds""" try: root = qdom.parse(open(source)) except IOError as ex: if ex.errno == errno.ENOENT and local: raise MissingLocalFeed( _("Feed not found. Perhaps this is a local feed that no longer exists? You can remove it from the list of feeds in that case." )) raise InvalidInterface(_("Can't read file"), ex) except Exception as ex: raise InvalidInterface(_("Invalid XML"), ex) if local: if selections_ok and root.uri == XMLNS_IFACE and root.name == 'selections': from zeroinstall.injector import selections return selections.Selections(root) local_path = source else: local_path = None feed = ZeroInstallFeed(root, local_path) feed.last_modified = int(os.stat(source).st_mtime) return feed
def testSelect(self): out, err = self.run_0install(['select']) assert out.lower().startswith("usage:") assert '--xml' in out out, err = self.run_0install(['select', 'Local.xml']) assert not err, err assert 'Version: 0.1' in out out, err = self.run_0install(['select', 'Local.xml', '--command=']) assert not err, err assert 'Version: 0.1' in out local_uri = model.canonical_iface_uri('Local.xml') out, err = self.run_0install(['select', 'Local.xml']) assert not err, err assert 'Version: 0.1' in out out, err = self.run_0install(['select', 'Local.xml', '--xml']) sels = selections.Selections( qdom.parse(BytesIO(str(out).encode('utf-8')))) assert sels.selections[local_uri].version == '0.1' out, err = self.run_0install(['select', 'selections.xml']) assert not err, err assert 'Version: 1\n' in out assert '(not cached)' in out out, err = self.run_0install(['select', 'runnable/RunExec.xml']) assert not err, err assert 'Runner' in out, out
def testSelections(self): from zeroinstall.injector import cli root = qdom.parse(file("selections.xml")) sels = selections.Selections(root) class Options: dry_run = False with output_suppressed(): self.child = server.handle_requests( 'Hello.xml', '6FCF121BE2390E0B.gpg', '/key-info/key/DE937DD411906ACF7C263B396FCF121BE2390E0B', 'HelloWorld.tgz') sys.stdin = Reply("Y\n") try: self.config.stores.lookup_any( sels.selections['http://example.com:8000/Hello.xml']. digests) assert False except NotStored: pass cli.main(['--download-only', 'selections.xml'], config=self.config) path = self.config.stores.lookup_any( sels.selections['http://example.com:8000/Hello.xml'].digests) assert os.path.exists(os.path.join(path, 'HelloWorld', 'main')) assert sels.download_missing(self.config) is None
def report_bug(policy, iface): assert iface # TODO: Check the interface to decide where to send bug reports issue_file = '/etc/issue' if os.path.exists(issue_file): issue = file(issue_file).read().strip() else: issue = "(file '%s' not found)" % issue_file text = 'Problem with %s\n' % iface.uri if iface.uri != policy.root: text = ' (while attempting to run %s)\n' % policy.root text += '\n' text += 'Zero Install: Version %s, with Python %s\n' % ( zeroinstall.version, sys.version) text += '\nChosen implementations:\n' if not policy.ready: text += ' Failed to select all required implementations\n' for chosen_iface_uri, impl in policy.solver.selections.selections.iteritems( ): text += '\n Interface: %s\n' % chosen_iface_uri if impl: text += ' Version: %s\n' % impl.version feed_url = impl.attrs['from-feed'] if feed_url != chosen_iface_uri: text += ' From feed: %s\n' % feed_url text += ' ID: %s\n' % impl.id else: chosen_iface = policy.config.iface_cache.get_interface( chosen_iface_uri) impls = policy.solver.details.get(chosen_iface, None) if impls: best, reason = impls[0] note = 'best was %s, but: %s' % (best, reason) else: note = 'not considered; %d available' % len( chosen_iface.implementations) text += ' No implementation selected (%s)\n' % note if hasattr(os, 'uname'): text += '\nSystem:\n %s\n\nIssue:\n %s\n' % ('\n '.join( os.uname()), issue) else: text += '\nSystem without uname()\n' if policy.solver.ready: sels = selections.Selections(policy) text += "\n" + sels.toDOM().toprettyxml(encoding='utf-8') reporter = BugReporter(policy, iface, text) reporter.show()
def load_built_selections(self): path = join(self.metadir, 'build-environment.xml') if os.path.exists(path): stream = file(path) try: return selections.Selections(qdom.parse(stream)) finally: stream.close() return None
def testCommands(self): iface = os.path.join(mydir, "Command.xml") driver = Driver(requirements=Requirements(iface), config=self.config) driver.need_download() assert driver.solver.ready impl = driver.solver.selections[self.config.iface_cache.get_interface( iface)] assert impl.id == 'c' assert impl.main == 'test-gui' dep_impl_uri = impl.commands['run'].requires[0].interface dep_impl = driver.solver.selections[ self.config.iface_cache.get_interface(dep_impl_uri)] assert dep_impl.id == 'sha1=256' s1 = driver.solver.selections assert s1.commands[0].path == 'test-gui' xml = s1.toDOM().toxml("utf-8") root = qdom.parse(BytesIO(xml)) s2 = selections.Selections(root) assert s2.commands[0].path == 'test-gui' impl = s2.selections[iface] assert impl.id == 'c' assert s2.commands[0].qdom.attrs['http://custom attr'] == 'namespaced' custom_element = s2.commands[0].qdom.childNodes[0] assert custom_element.name == 'child' dep_impl = s2.selections[dep_impl_uri] assert dep_impl.id == 'sha1=256' d = Driver(self.config, requirements.Requirements(runexec)) need_download = d.need_download() assert need_download == False xml = d.solver.selections.toDOM().toxml("utf-8") root = qdom.parse(BytesIO(xml)) s3 = selections.Selections(root) runnable_impl = s3.selections[runnable] assert 'foo' in runnable_impl.commands assert 'run' in runnable_impl.commands
def testLocalPath(self): # 0launch --get-selections Local.xml iface = os.path.join(mydir, "Local.xml") driver = Driver(requirements=Requirements(iface), config=self.config) driver.need_download() assert driver.solver.ready s1 = driver.solver.selections xml = s1.toDOM().toxml("utf-8") # Reload selections and check they're the same root = qdom.parse(BytesIO(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 = self.config.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', 'name': 'run' }), None) impl.add_download_source('http://localhost/bar.tgz', 1000, None) feed.implementations = {impl.id: impl} assert driver.need_download() assert driver.solver.ready, driver.solver.get_failure_reason() s1 = driver.solver.selections xml = s1.toDOM().toxml("utf-8") root = qdom.parse(BytesIO(xml)) s2 = selections.Selections(root) xml = s2.toDOM().toxml("utf-8") qdom.parse(BytesIO(xml)) assert s2.selections[iface].local_path is None assert not s2.selections[iface].digests, s2.selections[iface].digests assert s2.selections[iface].id == 'foo bar=123'
def collect_output(self, buffer): iter = buffer.get_end_iter() buffer.place_cursor(iter) if not self.policy.ready: missing = [ iface.uri for iface in self.policy.implementation if self.policy.implementation[iface] is None ] buffer.insert_at_cursor( "Can't run: no version has been selected for:\n- " + "\n- ".join(missing)) return uncached = self.policy.get_uncached_implementations() if uncached: buffer.insert_at_cursor( "Can't run: the chosen versions have not been downloaded yet. I need:\n\n- " + "\n\n- ".join([ '%s version %s\n (%s)' % (x[0].uri, x[1].get_version(), x[1].id) for x in uncached ])) return from zeroinstall.injector import selections sels = selections.Selections(self.policy) doc = sels.toDOM() self.hide() try: gtk.gdk.flush() iter = buffer.get_end_iter() buffer.place_cursor(iter) # Tell 0launch to run the program doc.documentElement.setAttribute('run-test', 'true') payload = doc.toxml('utf-8') sys.stdout.write(('Length:%8x\n' % len(payload)) + payload) sys.stdout.flush() reply = support.read_bytes(0, len('Length:') + 9) assert reply.startswith('Length:') test_output = support.read_bytes(0, int(reply.split(':', 1)[1], 16)) # Cope with invalid UTF-8 import codecs decoder = codecs.getdecoder('utf-8') data = decoder(test_output, 'replace')[0] buffer.insert_at_cursor(data) finally: self.show()
def get_selections(self, snapshot_date=None): """Load the selections. Does not check whether they are cached, nor trigger updates. @param snapshot_date: get a historical snapshot @type snapshot_date: (as returned by L{get_history}) | None @return: the selections @rtype: L{selections.Selections}""" if snapshot_date: sels_file = os.path.join(self.path, 'selections-' + snapshot_date + '.xml') else: sels_file = os.path.join(self.path, 'selections.xml') with open(sels_file, 'rb') as stream: return selections.Selections(qdom.parse(stream))
def testSelectOnly(self): os.environ['DISPLAY'] = ':foo' out, err = self.run_0install(['select', '--xml', 'Hello.xml']) self.assertEqual("", err) assert out.endswith("Finished\n"), out out = out[:-len("Finished\n")] root = qdom.parse(BytesIO(str(out).encode('utf-8'))) self.assertEqual(namespaces.XMLNS_IFACE, root.uri) sels = selections.Selections(root) sel, = sels.selections.values() self.assertEqual("sha1=3ce644dc725f1d21cfcf02562c76f375944b266a", sel.id)
def testSelectOnly(self): os.environ['DISPLAY'] = ':foo' out, err = self.run_0launch( ['--get-selections', '--select-only', 'Hello.xml']) self.assertEquals("", err) assert out.endswith("Finished\n") out = out[:-len("Finished\n")] root = qdom.parse(StringIO(str(out))) self.assertEquals(namespaces.XMLNS_IFACE, root.uri) sels = selections.Selections(root) sel, = sels.selections.values() self.assertEquals("sha1=3ce644dc725f1d21cfcf02562c76f375944b266a", sel.id)
def testSelectionsWithFeed(self): with open("selections.xml", 'rb') as stream: root = qdom.parse(stream) sels = selections.Selections(root) with output_suppressed(): run_server('Hello.xml', '6FCF121BE2390E0B.gpg', '/key-info/key/DE937DD411906ACF7C263B396FCF121BE2390E0B', 'HelloWorld.tgz') sys.stdin = Reply("Y\n") tasks.wait_for_blocker(self.config.fetcher.download_and_import_feed('http://example.com:8000/Hello.xml', self.config.iface_cache)) out, err = self.run_ocaml(['download', 'selections.xml']) assert not out, out assert not err, err path = self.config.stores.lookup_any(sels.selections['http://example.com:8000/Hello.xml'].digests) assert os.path.exists(os.path.join(path, 'HelloWorld', 'main')) assert sels.download_missing(self.config) is None
def build(selections_xml): # Get the chosen versions sels = selections.Selections(qdom.parse(BytesIO(selections_xml))) impl = sels.selections[interface_uri] min_version = impl.attrs.get(XMLNS_0COMPILE + ' min-version', our_min_version) # Check the syntax is valid and the version is high enough if model.parse_version(min_version) < model.parse_version(our_min_version): min_version = our_min_version # Do the whole build-and-register-feed c = Command() c.run(("0launch", '--message', _('Download the 0compile tool, to compile the source code'), '--not-before=' + min_version, "http://0install.net/2006/interfaces/0compile.xml", 'gui', interface_uri), lambda unused: on_success())
def load_feed(source, local = False, selections_ok = False, generate_sizes = False, implementation_id_alg=None, config=None): """Load a feed from a local file. @param source: the name of the file to read @type source: str @param local: this is a local feed @type local: bool @param selections_ok: if it turns out to be a local selections document, return that instead @type selections_ok: bool @param generate_sizes: if True, sizes of archives with missing size attributes will be generated @type generate_sizes: bool @param implementation_id_alg: if specified, missing impl ids will be generated with this alg @type implementation_id_alg: L{Algorithm} @raise InvalidInterface: if the source's syntax is incorrect @return: the new feed @since: 0.48 @see: L{iface_cache.iface_cache}, which uses this to load the feeds""" try: root = qdom.parse(file(source)) except IOError as ex: if ex.errno == 2 and local: raise MissingLocalFeed(_("Feed not found. Perhaps this is a local feed that no longer exists? You can remove it from the list of feeds in that case.")) raise InvalidInterface(_("Can't read file"), ex) except Exception as ex: raise InvalidInterface(_("Invalid XML"), ex) if local: if selections_ok and root.uri == XMLNS_IFACE and root.name == 'selections': from zeroinstall.injector import selections return selections.Selections(root) local_path = source else: local_path = None if implementation_id_alg or generate_sizes: from zeroinstall.injector.config import load_config if config is None: config = load_config() feed = ZeroInstallFeed(root, local_path, None, generate_sizes, implementation_id_alg, config.fetcher, config.stores) else: feed = ZeroInstallFeed(root, local_path) feed.last_modified = int(os.stat(source).st_mtime) return feed
def get_selections(self, snapshot_date=None, may_update=False): """Load the selections. @param may_update: whether to check for updates @type may_update: bool @param snapshot_date: get a historical snapshot @type snapshot_date: (as returned by L{get_history}) | None @return: the selections @rtype: L{selections.Selections}""" if snapshot_date: sels_file = os.path.join(self.path, 'selections-' + snapshot_date + '.xml') else: sels_file = os.path.join(self.path, 'selections.xml') with open(sels_file, 'rb') as stream: sels = selections.Selections(qdom.parse(stream)) if may_update: sels = self._check_for_updates(sels) return sels
def testSelections(self): with open("selections.xml", 'rb') as stream: root = qdom.parse(stream) sels = selections.Selections(root) class Options: dry_run = False with output_suppressed(): run_server('Hello.xml', '6FCF121BE2390E0B.gpg', '/key-info/key/DE937DD411906ACF7C263B396FCF121BE2390E0B', 'HelloWorld.tgz') try: self.config.stores.lookup_any(sels.selections['http://example.com:8000/Hello.xml'].digests) assert False except NotStored: pass out, err = self.run_ocaml(['download', 'selections.xml'], stdin = "Y\n") assert not out, out assert not err, err path = self.config.stores.lookup_any(sels.selections['http://example.com:8000/Hello.xml'].digests) assert os.path.exists(os.path.join(path, 'HelloWorld', 'main')) assert sels.download_missing(self.config) is None
def testDownload(self): out, err = self.run_0install(['download']) assert out.lower().startswith("usage:") assert '--show' in out assert 'file\n' in self.complete(["download", ""], 2) out, err = self.run_0install(['download', 'Local.xml', '--show']) assert not err, err assert 'Version: 0.1' in out local_uri = model.canonical_iface_uri('Local.xml') out, err = self.run_0install(['download', 'Local.xml', '--xml']) assert not err, err sels = selections.Selections( qdom.parse(BytesIO(str(out).encode('utf-8')))) assert sels.selections[local_uri].version == '0.1' out, err = self.run_0install( ['download', 'Local.xml', '--show', '--with-store=/foo']) assert not err, err assert self.config.stores.stores[-1].dir == '/foo' out, err = self.run_0install( ['download', '--offline', 'selections.xml']) assert 'Would download' in err self.config.network_use = model.network_full self.config.stores = TestStores() digest = 'sha1=3ce644dc725f1d21cfcf02562c76f375944b266a' self.config.fetcher.allow_download(digest) out, err = self.run_0install(['download', 'Hello.xml', '--show']) assert not err, err assert self.config.stores.lookup_any([digest]).startswith('/fake') assert 'Version: 1\n' in out out, err = self.run_0install( ['download', '--offline', 'selections.xml', '--show']) assert '/fake_store' in out self.config.network_use = model.network_full
def testSelect(self): out, err = self.run_0install(['select']) assert out.lower().startswith("usage:") assert '--xml' in out out, err = self.run_0install(['select', 'Local.xml']) assert not err, err assert 'Version: 0.1' in out local_uri = model.canonical_iface_uri('Local.xml') out, err = self.run_0install(['select', 'Local.xml']) assert not err, err assert 'Version: 0.1' in out out, err = self.run_0install(['select', 'Local.xml', '--xml']) sels = selections.Selections(qdom.parse(StringIO(str(out)))) assert sels.selections[local_uri].version == '0.1' out, err = self.run_0install(['select', 'selections.xml']) assert not err, err assert 'Version: 1\n' in out assert '(not cached)' in out
def ensure_cached(uri): """Ensure that an implementation of uri is cached. If not, it downloads one. It uses the GUI if a display is available, or the console otherwise. @param uri: the required interface @type uri: str @return: a new policy for this program, or None if the user cancelled @rtype: L{zeroinstall.injector.selections.Selections} """ from zeroinstall.injector import autopolicy, selections p = autopolicy.AutoPolicy(uri, download_only=True) p.freshness = 0 # Don't check for updates if p.need_download() or not p.ready: if os.environ.get('DISPLAY', None): return get_selections_gui(uri, []) else: p.recalculate_with_dl() p.start_downloading_impls() p.handler.wait_for_downloads() return selections.Selections(p)
def testSelectionsWithFeed(self): from zeroinstall.injector import cli root = qdom.parse(file("selections.xml")) sels = selections.Selections(root) with output_suppressed(): self.child = server.handle_requests( 'Hello.xml', '6FCF121BE2390E0B.gpg', '/key-info/key/DE937DD411906ACF7C263B396FCF121BE2390E0B', 'HelloWorld.tgz') sys.stdin = Reply("Y\n") self.config.handler.wait_for_blocker( self.config.fetcher.download_and_import_feed( 'http://example.com:8000/Hello.xml', self.config.iface_cache)) cli.main(['--download-only', 'selections.xml'], config=self.config) path = self.config.stores.lookup_any( sels.selections['http://example.com:8000/Hello.xml'].digests) assert os.path.exists(os.path.join(path, 'HelloWorld', 'main')) assert sels.download_missing(self.config) is None
def download_and_run(self, run_button, cancelled): try: if not self.select_only: downloaded = self.policy.download_uncached_implementations() if downloaded: # We need to wait until everything is downloaded... blockers = [downloaded, cancelled] yield blockers tasks.check(blockers) if cancelled.happened: return uncached = self.policy.get_uncached_implementations() else: uncached = None # (we don't care) if uncached: missing = '\n- '.join([_('%(iface_name)s %(impl_version)s') % {'iface_name': iface.get_name(), 'impl_version': impl.get_version()} for iface, impl in uncached]) dialog.alert(self.window, _('Not all downloads succeeded; cannot run program.\n\nFailed to get:') + '\n- ' + missing) else: from zeroinstall.injector import selections sels = selections.Selections(self.policy) doc = sels.toDOM() reply = doc.toxml('utf-8') sys.stdout.write(('Length:%8x\n' % len(reply)) + reply) self.window.destroy() sys.exit(0) # Success except SystemExit: raise except download.DownloadAborted as ex: run_button.set_active(False) # Don't bother reporting this to the user except Exception as ex: run_button.set_active(False) self.report_exception(ex)
def get_selections_gui(iface_uri, gui_args, test_callback = None, use_gui = True): """Run the GUI to choose and download a set of implementations. The user may ask the GUI to submit a bug report about the program. In that case, the GUI may ask us to test it. test_callback is called in that case with the implementations to be tested; the callback will typically call L{zeroinstall.injector.run.test_selections} and return the result of that. @param iface_uri: the required program, or None to show just the preferences dialog @type iface_uri: str @param gui_args: any additional arguments for the GUI itself @type gui_args: [str] @param test_callback: function to use to try running the program @type test_callback: L{zeroinstall.injector.selections.Selections} -> str @param use_gui: if True, raise a SafeException if the GUI is not available. If None, returns DontUseGUI if the GUI cannot be started. If False, returns DontUseGUI always. (since 1.11) @param use_gui: bool | None @return: the selected implementations @rtype: L{zeroinstall.injector.selections.Selections} @since: 0.28 """ if use_gui is False: return DontUseGUI if 'DISPLAY' not in os.environ: if use_gui is None: return DontUseGUI else: raise SafeException("Can't use GUI because $DISPLAY is not set") from zeroinstall.injector import selections, qdom from io import BytesIO from os.path import join, dirname gui_exe = join(dirname(__file__), '0launch-gui', '0launch-gui') import socket cli, gui = socket.socketpair() try: child = os.fork() if child == 0: # We are the child (GUI) try: try: cli.close() # We used to use pipes to support Python2.3... os.dup2(gui.fileno(), 1) os.dup2(gui.fileno(), 0) if use_gui is True: gui_args = ['-g'] + gui_args if iface_uri is not None: gui_args = gui_args + ['--', iface_uri] os.execvp(sys.executable, [sys.executable, gui_exe] + gui_args) except: import traceback traceback.print_exc(file = sys.stderr) finally: sys.stderr.flush() os._exit(1) # We are the parent (CLI) gui.close() gui = None while True: logger.info("Waiting for selections from GUI...") reply = support.read_bytes(cli.fileno(), len('Length:') + 9, null_ok = True) if reply: if not reply.startswith(b'Length:'): raise Exception("Expected Length:, but got %s" % repr(reply)) reply = reply.decode('ascii') xml = support.read_bytes(cli.fileno(), int(reply.split(':', 1)[1], 16)) dom = qdom.parse(BytesIO(xml)) sels = selections.Selections(dom) if dom.getAttribute('run-test'): logger.info("Testing program, as requested by GUI...") if test_callback is None: output = b"Can't test: no test_callback was passed to get_selections_gui()\n" else: output = test_callback(sels) logger.info("Sending results to GUI...") output = ('Length:%8x\n' % len(output)).encode('utf-8') + output logger.debug("Sending: %s", repr(output)) while output: sent = cli.send(output) output = output[sent:] continue else: sels = None pid, status = os.waitpid(child, 0) assert pid == child if status == 1 << 8: logger.info("User cancelled the GUI; aborting") return None # Aborted elif status == 100 << 8: if use_gui is None: return DontUseGUI else: raise SafeException("No GUI available") if status != 0: raise Exception("Error from GUI: code = %d" % status) break finally: for sock in [cli, gui]: if sock is not None: sock.close() return sels
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. # The basic plan is this: # 1. Scan the root interface and all dependencies recursively, building up a SAT problem. # 2. Solve the SAT problem. Whenever there are multiple options, try the most preferred one first. # 3. Create a Selections object from the results. # # All three involve recursively walking the tree in a similar way: # 1) we follow every dependency of every implementation (order not important) # 2) we follow every dependency of every selected implementation (better versions first) # 3) we follow every dependency of every selected implementation (order doesn't matter) # # In all cases, a dependency may be on an <implementation> or on a specific <command>. # TODO: We need some way to figure out which feeds to include. # Currently, we include any feed referenced from anywhere but # this is probably too much. We could insert a dummy optimial # implementation in stale/uncached feeds and see whether it # selects that. iface_cache = self.config.iface_cache problem = sat.SATProblem() impl_to_var = {} # Impl -> sat var self.feeds_used = set() self.requires = {} self.ready = False self.details = self.record_details and {} self.selections = None self._failure_reason = None ifaces_processed = set() impls_for_machine_group = { 0: [] } # Machine group (e.g. "64") to [impl] in that group for machine_group in machine_groups.values(): impls_for_machine_group[machine_group] = [] 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 # Must have already done add_iface on dependency.interface. # If dependency is essential: # Add a clause so that if requiring_impl_var is True then an implementation # matching 'dependency' must also be selected. # If dependency is optional: # Require that no incompatible version is selected. # This ignores any 'command' required. Handle that separately. def find_dependency_candidates(requiring_impl_var, dependency): def meets_restrictions(candidate): for r in dependency.restrictions: if not r.meets_restriction(candidate): #warn("%s rejected due to %s", candidate.get_version(), r) return False return True essential = dependency.importance == model.Dependency.Essential dep_iface = iface_cache.get_interface(dependency.interface) dep_union = [sat.neg(requiring_impl_var) ] # Either requiring_impl_var is False, or ... for candidate in impls_for_iface[dep_iface]: if (candidate.__class__ is _DummyImpl) or meets_restrictions(candidate): if essential: c_var = impl_to_var.get(candidate, None) if c_var is not None: dep_union.append(c_var) # else we filtered that version out, so ignore it else: # Candidate doesn't meet our requirements # If the dependency is optional add a rule to make sure we don't # select this candidate. # (for essential dependencies this isn't necessary because we must # select a good version and we can't select two) if not essential: c_var = impl_to_var.get(candidate, None) if c_var is not None: problem.add_clause(dep_union + [sat.neg(c_var)]) # else we filtered that version out, so ignore it if essential: problem.add_clause(dep_union) def is_unusable(impl, restrictions, arch): """@return: whether this implementation is unusable. @rtype: bool""" return get_unusable_reason(impl, restrictions, arch) != None def get_unusable_reason(impl, restrictions, arch): """ @param impl: Implementation to test. @type restrictions: [L{model.Restriction}] @return: The reason why this impl is unusable, or None if it's OK. @rtype: str @note: The restrictions are for the interface being requested, not the feed of the implementation; they may be different when feeds are being used.""" for r in restrictions: if not r.meets_restriction(impl): return _( "Incompatible with another selected implementation") stability = impl.get_stability() if stability <= model.buggy: return stability.name if (self.config.network_use == model.network_offline or not impl.download_sources) and not impl.is_available( self.config.stores): if not impl.download_sources: return _("No retrieval methods") return _("Not cached and we are off-line") if impl.os not in arch.os_ranks: return _("Unsupported OS") if impl.machine not in arch.machine_ranks: if impl.machine == 'src': return _("Source code") return _("Unsupported machine type") return None def usable_feeds(iface, arch): """Return all feeds for iface that support arch. @rtype: generator(ZeroInstallFeed)""" yield iface.uri for f in iface_cache.get_feed_imports(iface): # Note: when searching for src, None is not in machine_ranks if f.os in arch.os_ranks and \ (f.machine is None or f.machine in arch.machine_ranks): yield f.uri else: debug( _("Skipping '%(feed)s'; unsupported architecture %(os)s-%(machine)s" ), { 'feed': f, 'os': f.os, 'machine': f.machine }) # If requiring_var is True then all of requirer's dependencies must be satisfied. # requirer can be a <command> or an <implementation> def process_dependencies(requiring_var, requirer, arch): for d in deps_in_use(requirer, arch): debug(_("Considering command dependency %s"), d) add_iface(d.interface, arch.child_arch) for c in d.get_required_commands(): # We depend on a specific command within the implementation. command_vars = add_command_iface(d.interface, arch.child_arch, c) # If the parent command/impl is chosen, one of the candidate commands # must be too. If there aren't any, then this command is unselectable. problem.add_clause([sat.neg(requiring_var)] + command_vars) # Must choose one version of d if impl is selected find_dependency_candidates(requiring_var, d) def add_iface(uri, arch): """Name implementations from feed and assert that only one can be selected.""" if uri in ifaces_processed: return ifaces_processed.add(uri) iface = iface_cache.get_interface(uri) impls = [] for f in usable_feeds(iface, arch): self.feeds_used.add(f) debug(_("Processing feed %s"), f) try: feed = iface_cache.get_feed(f) if feed is None: continue #if feed.name and iface.uri != feed.url and iface.uri not in feed.feed_for: # info(_("Missing <feed-for> for '%(uri)s' in '%(feed)s'"), {'uri': iface.uri, 'feed': f}) if feed.implementations: impls.extend(feed.implementations.values()) distro_feed_url = feed.get_distro_feed() if distro_feed_url: self.feeds_used.add(distro_feed_url) distro_feed = iface_cache.get_feed(distro_feed_url) if distro_feed.implementations: impls.extend(distro_feed.implementations.values()) except MissingLocalFeed as ex: warn( _("Missing local feed; if it's no longer required, remove it with:" ) + '\n0install remove-feed ' + iface.uri + ' ' + f, { 'feed': f, 'interface': iface, 'exception': ex }) except Exception as ex: warn( _("Failed to load feed %(feed)s for %(interface)s: %(exception)s" ), { 'feed': f, 'interface': iface, 'exception': ex }) #raise impls.sort(key=lambda impl: self.get_rating(iface, impl, arch), reverse=True) impls_for_iface[iface] = filtered_impls = [] my_extra_restrictions = self.extra_restrictions.get(iface, []) if self.record_details: self.details[iface] = [ (impl, get_unusable_reason(impl, my_extra_restrictions, arch)) for impl in impls ] var_names = [] for impl in impls: if is_unusable(impl, my_extra_restrictions, arch): continue filtered_impls.append(impl) if impl in impl_to_var: # TODO If the same impl comes from original feed and # the feed with "feed" tag continue v = problem.add_variable(ImplInfo(iface, impl, arch)) impl_to_var[impl] = v var_names.append(v) if impl.machine and impl.machine != 'src': impls_for_machine_group[machine_groups.get( impl.machine, 0)].append(v) process_dependencies(v, impl, arch) if closest_match: dummy_impl = _DummyImpl() dummy_var = problem.add_variable( ImplInfo(iface, dummy_impl, arch, dummy=True)) var_names.append(dummy_var) impl_to_var[dummy_impl] = dummy_var filtered_impls.append(dummy_impl) # Only one implementation of this interface can be selected if uri == root_interface: if var_names: clause = problem.at_most_one(var_names) problem.add_clause(var_names) # at least one else: problem.impossible() clause = False elif var_names: clause = problem.at_most_one(var_names) else: # Don't need to add to group_clause_for because we should # never get a possible selection involving this. return assert clause is not True assert clause is not None if clause is not False: group_clause_for[uri] = clause def add_command_iface(uri, arch, command_name): """Add every <command> in interface 'uri' with this name. Each one depends on the corresponding implementation and only one can be selected. If closest_match is on, include a dummy command that can always be selected.""" # Check whether we've already processed this (interface,command) pair existing = group_clause_for_command.get((uri, command_name), None) if existing is not None: return existing.lits # 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 = 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: if not isinstance(impl, _DummyImpl): # Mark implementation as unselectable problem.add_clause([sat.neg(impl_to_var[impl])]) continue # We have a candidate <command>. Require that if it's selected # then we select the corresponding <implementation> 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) process_dependencies(command_var, command, arch) # Tell the user why we couldn't use this version if self.record_details: def new_reason(impl, old_reason): if command_name in impl.commands: return old_reason return old_reason or (_('No %s command') % command_name) self.details[iface] = [(impl, new_reason(impl, reason)) for (impl, reason) in self.details[iface]] if closest_match: dummy_command = problem.add_variable(None) var_names.append(dummy_command) if var_names: # Can't select more than one of them. assert (uri, command_name) not in group_clause_for_command group_clause_for_command[( uri, command_name)] = problem.at_most_one(var_names) return var_names if command_name is None: add_iface(root_interface, root_arch) else: commands = add_command_iface(root_interface, root_arch, command_name) if len(commands) > int(closest_match): # (we have at least one non-dummy command) problem.add_clause(commands) # At least one else: # (note: might be because we haven't cached it yet) info("No %s <command> in %s", command_name, root_interface) impls = impls_for_iface[iface_cache.get_interface( root_interface)] if impls == [] or (len(impls) == 1 and isinstance(impls[0], _DummyImpl)): # There were no candidates at all. if self.config.network_use == model.network_offline: self._failure_reason = _( "Interface '%s' has no usable implementations in the cache (and 0install is in off-line mode)" ) % root_interface else: 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<group> to be true if we select an implementation in that group m_groups = [] for machine_group, impls in impls_for_machine_group.iteritems(): m_group = 'm%d' % machine_group group_var = problem.add_variable(m_group) if impls: for impl in impls: problem.add_clause([group_var, sat.neg(impl)]) m_groups.append(group_var) if m_groups: m_groups_clause = problem.at_most_one(m_groups) else: m_groups_clause = None def decide(): """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): for c in dep.get_required_commands(): dep_lit = find_undecided_command(dep.interface, c) if dep_lit is not None: return dep_lit 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: return # Break cycles seen.add(uri) if uri not in group_clause_for: # TODO Looks like Sweets patch introduced this issue return group = group_clause_for[uri] #print "Group for %s = %s" % (uri, group) lit = group.current if lit is None: return group.best_undecided() # else there was only one choice anyway # Check for undecided dependencies lit_info = problem.get_varinfo_for_lit(lit).obj return find_undecided_dep(lit_info.impl, lit_info.arch) def find_undecided_command(uri, name): if name is None: return find_undecided(uri) group = group_clause_for_command[(uri, name)] lit = group.current if lit is None: return group.best_undecided() # else we've already chosen which <command> to use # Check for undecided command-specific dependencies, and then for # implementation dependencies. lit_info = problem.get_varinfo_for_lit(lit).obj if lit_info is None: assert closest_match return None # (a dummy command added for better diagnostics; has no dependencies) return find_undecided_dep(lit_info.command, lit_info.arch) or \ find_undecided_dep(lit_info.impl, lit_info.arch) best = find_undecided_command(root_interface, command_name) if best is not None: return best # If we're chosen everything we need, we can probably # set everything else to False. for group in group_clause_for.values( ) + group_clause_for_command.values() + [m_groups_clause]: if group.current is None: best = group.best_undecided() if best is not None: return sat.neg(best) return None # Failed to find any valid combination ready = problem.run_solver(decide) is True if not ready and not closest_match: # 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, command_name=command_name, closest_match=True) else: self.ready = ready and not closest_match self.selections = selections.Selections(None) self.selections.interface = root_interface sels = self.selections.selections commands_needed = [] # Popular sels with the selected implementations. # Also, note down all the commands we need. for uri, group in group_clause_for.iteritems(): if group.current is not None: lit_info = problem.get_varinfo_for_lit(group.current).obj if lit_info.is_dummy: sels[lit_info.iface.uri] = None else: # We selected an implementation for interface 'uri' impl = lit_info.impl for b in impl.bindings: c = b.command if c is not None: commands.append((uri, c)) deps = self.requires[lit_info.iface] = [] for dep in deps_in_use(lit_info.impl, lit_info.arch): deps.append(dep) for c in dep.get_required_commands(): commands_needed.append((dep.interface, c)) sels[lit_info.iface.uri] = selections.ImplSelection( lit_info.iface.uri, impl, deps) # Now all all the commands in too. def add_command(iface, name): sel = sels.get(iface, None) if sel: command = sel.impl.commands[name] if name in sel._used_commands: return # Already added sel._used_commands.add(name) for dep in command.requires: for dep_command_name in dep.get_required_commands(): add_command(dep.interface, dep_command_name) # A <command> can depend on another <command> in the same interface # (e.g. the tests depending on the main program) for b in command.bindings: c = b.command if c is not None: add_command(iface, c) for iface, command in commands_needed: add_command(iface, command) if command_name is not None: self.selections.command = command_name add_command(root_interface, command_name)
def testBackgroundUnsolvable(self): my_dbus.system_services = {"org.freedesktop.NetworkManager": {"/org/freedesktop/NetworkManager": NetworkManager()}} trust.trust_db.trust_key('DE937DD411906ACF7C263B396FCF121BE2390E0B', 'example.com:8000') # Create new app run_server('Hello.xml', '6FCF121BE2390E0B.gpg', 'HelloWorld.tgz') out, err = self.run_ocaml(['add', 'test-app', 'http://example.com:8000/Hello.xml']) kill_server_process() assert not out, out assert not err, err # Delete cached implementation so we need to download it again out, err = self.run_ocaml(['select', '--xml', 'test-app'], binary = True) sels = selections.Selections(qdom.parse(BytesIO(out))) stored = sels.selections['http://example.com:8000/Hello.xml'].get_path(self.config.stores) assert os.path.basename(stored).startswith('sha1') ro_rmtree(stored) out, err = self.run_ocaml(['select', '--xml', 'test-app'], binary = True) assert not err, err sels = selections.Selections(qdom.parse(BytesIO(out))) # Replace the selection with a bogus and unusable <package-implementation> sel, = sels.selections.values() sel.attrs['id'] = "package:dummy:badpackage" sel.attrs['from-feed'] = "distribution:http://example.com:8000/Hello.xml" sel.attrs['package'] = "badpackage" sel.get_command('run').qdom.attrs['path'] = '/i/dont/exist' app = basedir.load_first_config(namespaces.config_site, "apps", 'test-app') with open(os.path.join(app, 'selections.xml'), 'wt') as stream: doc = sels.toDOM() doc.writexml(stream, addindent=" ", newl="\n", encoding = 'utf-8') # Not time for a background update yet, but the missing binary should trigger # an update anyway. self.config.freshness = 0 # Check we try to launch the GUI... os.environ['DISPLAY'] = 'dummy' run_server('Hello.xml', 'HelloWorld.tgz') out, err = self.run_ocaml(['download', '--xml', '-v', 'test-app'], binary = True) kill_server_process() err = err.decode('utf-8') assert 'get new selections; current ones are not usable' in err, err assert 'get-selections-gui' in err, err sels = selections.Selections(qdom.parse(BytesIO(out))) # Check we can also work without the GUI... del os.environ['DISPLAY'] # Delete cached implementation so we need to download it again out, err = self.run_ocaml(['select', '--xml', 'test-app'], binary = True) sels = selections.Selections(qdom.parse(BytesIO(out))) stored = sels.selections['http://example.com:8000/Hello.xml'].get_path(self.config.stores) assert os.path.basename(stored).startswith('sha1') ro_rmtree(stored) run_server('Hello.xml', 'HelloWorld.tgz') out, err = self.run_ocaml(['download', '--xml', '-v', 'test-app'], binary = True) kill_server_process() err = err.decode('utf-8') assert 'get new selections; current ones are not usable' in err, err assert 'get-selections-gui' not in err, err sels = selections.Selections(qdom.parse(BytesIO(out))) # Now trigger a background update which discovers that no solution is possible timestamp = os.path.join(app, 'last-checked') last_check_attempt = os.path.join(app, 'last-check-attempt') selections_path = os.path.join(app, 'selections.xml') def reset_timestamps(): global ran_gui ran_gui = False os.utime(timestamp, (1, 1)) # 1970 os.utime(selections_path, (1, 1)) if os.path.exists(last_check_attempt): os.unlink(last_check_attempt) reset_timestamps() out, err = self.run_ocaml(['destroy', 'test-app']) assert not out, out assert not err, err run_server('Hello.xml') out, err = self.run_ocaml(['add', '--source', 'test-app', 'http://example.com:8000/Hello.xml']) assert not out, out assert 'We want source and this is a binary' in err, err