From 56f113822de379b30d570e8306e43a9f6664c408 Mon Sep 17 00:00:00 2001 From: Thomas Leonard Date: Sat, 15 Jan 2011 20:03:16 +0000 Subject: [PATCH] Large-scale API cleanup Added a Requirements object to hold information about what the user wants (e.g. source code, binary, OS, version restrictions). This allows code that parses options to be shared (e.g. between the command-line and the GUI option parsing) and allows requirements to be passed around more easily. Added a Config object to store user preferences. This is also used to get hold of various singletons such as the iface_cache, stores, fetcher, etc. Objects that need these things now take a 'config' argument. There are some backwards compatibility hacks to try and keep old users of the API working for the time being. These will eventually go away. Added unit-tests for 0install and all its sub-commands, and fixed various bugs they found. --- tests/basetest.py | 116 ++++++++++- tests/testarch.py | 4 +- tests/testautopolicy.py | 83 ++++---- tests/testdownload.py | 135 ++++++------- tests/testifacecache.py | 7 +- tests/testinstall.py | 324 +++++++++++++++++++++++++++++++ tests/testlaunch.py | 15 +- tests/testmanifest.py | 12 +- tests/testmodel.py | 6 +- tests/testpolicy.py | 18 +- tests/testreader.py | 19 +- tests/testrun.py | 32 ++- tests/testsat.py | 24 +-- tests/testselections.py | 22 +-- tests/testsolver.py | 52 +++-- tests/testwriter.py | 8 +- zeroinstall/0launch-gui/bugs.py | 13 +- zeroinstall/0launch-gui/combo_compat.py | 71 ------- zeroinstall/0launch-gui/compile.py | 87 +++++---- zeroinstall/0launch-gui/freshness.py | 2 +- zeroinstall/0launch-gui/gui.py | 1 - zeroinstall/0launch-gui/iface_browser.py | 42 ++-- zeroinstall/0launch-gui/main.py | 42 ++-- zeroinstall/0launch-gui/mainwindow.py | 2 +- zeroinstall/0launch-gui/preferences.py | 44 ++--- zeroinstall/0launch-gui/properties.py | 17 +- zeroinstall/cmd/__init__.py | 11 +- zeroinstall/cmd/add_feed.py | 19 +- zeroinstall/cmd/config.py | 22 ++- zeroinstall/cmd/download.py | 8 +- zeroinstall/cmd/import.py | 9 +- zeroinstall/cmd/run.py | 8 +- zeroinstall/cmd/select.py | 68 ++----- zeroinstall/cmd/update.py | 9 +- zeroinstall/gtkui/icon.py | 2 +- zeroinstall/helpers.py | 5 +- zeroinstall/injector/autopolicy.py | 23 +-- zeroinstall/injector/background.py | 26 ++- zeroinstall/injector/cli.py | 25 ++- zeroinstall/injector/distro.py | 6 +- zeroinstall/injector/fetch.py | 6 +- zeroinstall/injector/handler.py | 8 +- zeroinstall/injector/iface_cache.py | 8 +- zeroinstall/injector/model.py | 10 +- zeroinstall/injector/policy.py | 135 +++++++------ zeroinstall/injector/reader.py | 10 +- zeroinstall/injector/requirements.py | 72 +++++++ zeroinstall/injector/run.py | 5 +- zeroinstall/injector/sat.py | 5 - zeroinstall/injector/selections.py | 34 ++-- zeroinstall/injector/solver.py | 10 +- zeroinstall/support/tasks.py | 1 - zeroinstall/zerostore/__init__.py | 5 +- 53 files changed, 1055 insertions(+), 693 deletions(-) create mode 100755 tests/testinstall.py delete mode 100644 zeroinstall/0launch-gui/combo_compat.py create mode 100644 zeroinstall/injector/requirements.py diff --git a/tests/basetest.py b/tests/basetest.py index 5de15ad..e30618b 100755 --- a/tests/basetest.py +++ b/tests/basetest.py @@ -3,6 +3,7 @@ import sys, tempfile, os, shutil, StringIO import unittest import logging import warnings +from xml.dom import minidom warnings.filterwarnings("ignore", message = 'The CObject type') # Catch silly mistakes... @@ -11,10 +12,10 @@ os.environ['LANGUAGE'] = 'C' sys.path.insert(0, '..') from zeroinstall.injector import qdom -from zeroinstall.injector import iface_cache, download, distro, model -from zeroinstall.zerostore import Store; Store._add_with_helper = lambda *unused: False -from zeroinstall import support, helpers -from zeroinstall.support import basedir +from zeroinstall.injector import iface_cache, download, distro, model, handler, policy, reader +from zeroinstall.zerostore import NotStored, Store, Stores; Store._add_with_helper = lambda *unused: False +from zeroinstall import support +from zeroinstall.support import basedir, tasks dpkgdir = os.path.join(os.path.dirname(__file__), 'dpkg') @@ -56,9 +57,102 @@ class DummyPackageKit: def get_candidates(self, package, factory, prefix): pass +class DummyHandler(handler.Handler): + __slots__ = ['ex', 'tb', 'allow_downloads'] + + def __init__(self): + handler.Handler.__init__(self) + self.ex = None + self.allow_downloads = False + + def get_download(self, url, force = False, hint = None, factory = None): + if self.allow_downloads: + return handler.Handler.get_download(self, url, force, hint, factory) + raise model.SafeException("DummyHandler: " + url) + + def wait_for_blocker(self, blocker): + self.ex = None + handler.Handler.wait_for_blocker(self, blocker) + if self.ex: + raise self.ex, None, self.tb + + def report_error(self, ex, tb = None): + assert self.ex is None, self.ex + self.ex = ex + self.tb = tb + + #import traceback + #traceback.print_exc() + +class DummyKeyInfo: + def __init__(self, fpr): + self.fpr = fpr + self.info = [minidom.parseString('')] + self.blocker = None + +class TestFetcher: + def __init__(self, config): + self.allowed_downloads = set() + self.allowed_feed_downloads = {} + self.config = config + + def allow_download(self, digest): + assert isinstance(self.config.stores, TestStores) + self.allowed_downloads.add(digest) + + def allow_feed_download(self, url, feed): + self.allowed_feed_downloads[url] = feed + + def download_impls(self, impls, stores): + @tasks.async + def fake_download(): + yield + for impl in impls: + assert impl.id in self.allowed_downloads, impl + self.allowed_downloads.remove(impl.id) + self.config.stores.add_fake(impl.id) + return fake_download() + + def download_and_import_feed(self, feed_url, iface_cache, force = False): + @tasks.async + def fake_download(): + yield + assert feed_url in self.allowed_feed_downloads, feed_url + self.config.iface_cache._feeds[feed_url] = self.allowed_feed_downloads[feed_url] + del self.allowed_feed_downloads[feed_url] + return fake_download() + + def fetch_key_info(self, fingerprint): + return DummyKeyInfo(fingerprint) + +class TestStores: + def __init__(self): + self.fake_impls = set() + + def add_fake(self, digest): + self.fake_impls.add(digest) + + def lookup_any(self, digests): + for d in digests: + if d in self.fake_impls: + return '/fake_store/' + d + raise NotStored() + +class TestConfig: + freshness = 0 + help_with_testing = False + network_use = model.network_full + + def __init__(self): + self.iface_cache = iface_cache.IfaceCache() + self.handler = DummyHandler() + self.stores = Stores() + self.fetcher = TestFetcher(self) + class BaseTest(unittest.TestCase): def setUp(self): warnings.resetwarnings() + self.config_home = tempfile.mktemp() self.cache_home = tempfile.mktemp() self.cache_system = tempfile.mktemp() @@ -70,7 +164,6 @@ class BaseTest(unittest.TestCase): os.environ['XDG_CACHE_DIRS'] = self.cache_system reload(basedir) assert basedir.xdg_config_home == self.config_home - #iface_cache.iface_cache.__init__() os.mkdir(self.config_home, 0700) os.mkdir(self.cache_home, 0700) @@ -80,6 +173,10 @@ class BaseTest(unittest.TestCase): if os.environ.has_key('DISPLAY'): del os.environ['DISPLAY'] + self.config = TestConfig() + policy._config = self.config # XXX + iface_cache.iface_cache = self.config.iface_cache + logging.getLogger().setLevel(logging.WARN) download._downloads = {} @@ -92,11 +189,18 @@ class BaseTest(unittest.TestCase): distro._host_distribution._packagekit = DummyPackageKit() my_dbus.system_services = {} - + def tearDown(self): + assert self.config.handler.ex is None, self.config.handler.ex + shutil.rmtree(self.config_home) support.ro_rmtree(self.cache_home) shutil.rmtree(self.cache_system) shutil.rmtree(self.gnupg_home) os.environ['PATH'] = self.old_path + + def import_feed(self, url, path): + iface_cache = self.config.iface_cache + iface_cache.get_interface(url) + iface_cache._feeds[url] = reader.load_feed(path) diff --git a/tests/testarch.py b/tests/testarch.py index e30e05c..f5248f3 100644 --- a/tests/testarch.py +++ b/tests/testarch.py @@ -1,6 +1,6 @@ #!/usr/bin/env python -from basetest import BaseTest, empty_feed -import sys, os +from basetest import BaseTest +import sys import unittest sys.path.insert(0, '..') diff --git a/tests/testautopolicy.py b/tests/testautopolicy.py index 6571d43..7e5a373 100755 --- a/tests/testautopolicy.py +++ b/tests/testautopolicy.py @@ -6,8 +6,8 @@ import unittest sys.path.insert(0, '..') -from zeroinstall import NeedDownload -from zeroinstall.injector import model, autopolicy, gpg, iface_cache, namespaces, reader, handler +from zeroinstall.injector import model, gpg, namespaces, reader, run, fetch +from zeroinstall.injector.policy import Policy from zeroinstall.support import basedir import data @@ -18,6 +18,12 @@ logger = logging.getLogger() def recalculate(policy): policy.need_download() +def download_and_execute(policy, prog_args, main = None, dry_run = True): + downloaded = policy.solve_and_download_impls() + if downloaded: + policy.config.handler.wait_for_blocker(downloaded) + run.execute_selections(policy.solver.selections, prog_args, stores = policy.config.stores, main = main, dry_run = dry_run) + class TestAutoPolicy(BaseTest): def setUp(self): BaseTest.setUp(self) @@ -35,13 +41,11 @@ class TestAutoPolicy(BaseTest): f.close() def testNoNeedDl(self): - policy = autopolicy.AutoPolicy(foo_iface_uri, - download_only = False) + policy = Policy(foo_iface_uri, config = self.config) policy.freshness = 0 assert policy.need_download() - policy = autopolicy.AutoPolicy(os.path.abspath('Foo.xml'), - download_only = False) + policy = Policy(os.path.abspath('Foo.xml'), config = self.config) assert not policy.need_download() assert policy.ready @@ -58,12 +62,12 @@ class TestAutoPolicy(BaseTest): """ % foo_iface_uri) - policy = autopolicy.AutoPolicy(foo_iface_uri, - download_only = False) + self.config.fetcher = fetch.Fetcher(self.config.handler) + policy = Policy(foo_iface_uri, config = self.config) policy.freshness = 0 try: assert policy.need_download() - policy.execute([]) + download_and_execute(policy, []) except model.SafeException, ex: assert 'Unknown digest algorithm' in str(ex) @@ -80,9 +84,9 @@ class TestAutoPolicy(BaseTest): """) tmp.flush() - policy = autopolicy.AutoPolicy(tmp.name, False, False) + policy = Policy(tmp.name, config = self.config) try: - policy.download_and_execute(['Hello']) + download_and_execute(policy, ['Hello']) assert 0 except model.SafeException, ex: assert "ThisBetterNotExist" in str(ex) @@ -100,9 +104,9 @@ class TestAutoPolicy(BaseTest): """) tmp.flush() - policy = autopolicy.AutoPolicy(tmp.name, False, False) + policy = Policy(tmp.name, config = self.config) try: - policy.download_and_execute(['Hello']) + download_and_execute(policy, ['Hello']) assert 0 except model.SafeException, ex: assert "library" in str(ex), ex @@ -122,17 +126,12 @@ class TestAutoPolicy(BaseTest): """ % foo_iface_uri) - policy = autopolicy.AutoPolicy(foo_iface_uri, False, True) + policy = Policy(foo_iface_uri, config = self.config) policy.freshness = 0 policy.network_use = model.network_full recalculate(policy) assert policy.need_download() assert policy.ready - try: - policy.execute([], main = 'NOTHING') - assert False - except NeedDownload, ex: - pass def testBinding(self): local_impl = os.path.dirname(os.path.abspath(__file__)) @@ -171,13 +170,12 @@ class TestAutoPolicy(BaseTest): cached_impl = basedir.save_cache_path('0install.net', 'implementations', 'sha1=123') - policy = autopolicy.AutoPolicy(tmp.name, False, - dry_run = True) + policy = Policy(tmp.name, config = self.config) policy.network_use = model.network_offline os.environ['FOO_PATH'] = "old" old, sys.stdout = sys.stdout, StringIO() try: - policy.download_and_execute(['Hello']) + download_and_execute(policy, ['Hello']) finally: sys.stdout = old self.assertEquals(cached_impl + '/.:old', @@ -194,7 +192,7 @@ class TestAutoPolicy(BaseTest): os.environ['BAR_PATH'] = '/old' old, sys.stdout = sys.stdout, StringIO() try: - policy.download_and_execute(['Hello']) + download_and_execute(policy, ['Hello']) finally: sys.stdout = old self.assertEquals(cached_impl + '/.', @@ -203,9 +201,6 @@ class TestAutoPolicy(BaseTest): os.environ['BAR_PATH']) self.assertEquals(cached_impl + '/.:/usr/local/share:/usr/share', os.environ['XDG_DATA_DIRS']) - - policy.download_only = True - policy.download_and_execute(['Hello']) def testFeeds(self): self.cache_iface(foo_iface_uri, @@ -231,13 +226,12 @@ class TestAutoPolicy(BaseTest): """ % foo_iface_uri) - policy = autopolicy.AutoPolicy(foo_iface_uri, False, - dry_run = True) + policy = Policy(foo_iface_uri, config = self.config) policy.freshness = 0 policy.network_use = model.network_full recalculate(policy) assert policy.ready - foo_iface = iface_cache.iface_cache.get_interface(foo_iface_uri) + foo_iface = self.config.iface_cache.get_interface(foo_iface_uri) self.assertEquals('sha1=123', policy.implementation[foo_iface].id) def testBadConfig(self): @@ -250,8 +244,7 @@ class TestAutoPolicy(BaseTest): stream.close() logger.setLevel(logging.ERROR) - policy = autopolicy.AutoPolicy(foo_iface_uri, - download_only = False) + Policy(foo_iface_uri, config = self.config) logger.setLevel(logging.WARN) def testNoLocal(self): @@ -265,11 +258,9 @@ class TestAutoPolicy(BaseTest): Foo """ % foo_iface_uri) - policy = autopolicy.AutoPolicy(foo_iface_uri, - download_only = False) - policy.network_use = model.network_offline + self.config.network_use = model.network_offline try: - iface_cache.iface_cache.get_interface(foo_iface_uri) + self.config.iface_cache.get_interface(foo_iface_uri) assert False except reader.InvalidInterface, ex: assert 'Invalid feed URL' in str(ex) @@ -285,13 +276,13 @@ class TestAutoPolicy(BaseTest): Foo """ % foo_iface_uri) - policy = autopolicy.AutoPolicy(foo_iface_uri, dry_run = True) + policy = Policy(foo_iface_uri, config = self.config) policy.network_use = model.network_full policy.freshness = 0 assert policy.need_download() - feed = iface_cache.iface_cache.get_feed(foo_iface_uri) + feed = self.config.iface_cache.get_feed(foo_iface_uri) feed.feeds = [model.Feed('/BadFeed', None, False)] logger.setLevel(logging.ERROR) @@ -309,13 +300,12 @@ class TestAutoPolicy(BaseTest): Foo """ % foo_iface_uri) - policy = autopolicy.AutoPolicy(foo_iface_uri, - download_only = False) + policy = Policy(foo_iface_uri, config = self.config) policy.network_use = model.network_offline recalculate(policy) assert not policy.ready, policy.implementation try: - policy.download_and_execute([]) + download_and_execute(policy, []) assert False except model.SafeException, ex: assert "has no usable implementations" in str(ex), ex @@ -331,8 +321,7 @@ class TestAutoPolicy(BaseTest): Foo """ % foo_iface_uri) - policy = autopolicy.AutoPolicy(foo_iface_uri, - download_only = False) + policy = Policy(foo_iface_uri, config = self.config) policy.freshness = 0 recalculate(policy) assert not policy.ready @@ -353,8 +342,7 @@ class TestAutoPolicy(BaseTest): """ % (foo_iface_uri, foo_iface_uri)) - policy = autopolicy.AutoPolicy(foo_iface_uri, - download_only = False) + policy = Policy(foo_iface_uri, config = self.config) policy.freshness = 0 recalculate(policy) @@ -394,15 +382,14 @@ class TestAutoPolicy(BaseTest): """ % foo_iface_uri) - policy = autopolicy.AutoPolicy(foo_iface_uri, - download_only = False) + policy = Policy(foo_iface_uri, config = self.config) policy.network_use = model.network_full policy.freshness = 0 #logger.setLevel(logging.DEBUG) recalculate(policy) #logger.setLevel(logging.WARN) - foo_iface = iface_cache.iface_cache.get_interface(foo_iface_uri) - bar_iface = iface_cache.iface_cache.get_interface('http://bar') + foo_iface = self.config.iface_cache.get_interface(foo_iface_uri) + bar_iface = self.config.iface_cache.get_interface('http://bar') assert policy.implementation[bar_iface].id == 'sha1=200' dep = policy.implementation[foo_iface].dependencies['http://bar'] diff --git a/tests/testdownload.py b/tests/testdownload.py index aef3b20..0b04dcd 100755 --- a/tests/testdownload.py +++ b/tests/testdownload.py @@ -11,7 +11,8 @@ sys.path.insert(0, '..') os.environ["http_proxy"] = "localhost:8000" -from zeroinstall.injector import model, autopolicy, gpg, iface_cache, download, reader, trust, handler, background, arch, selections, qdom +from zeroinstall.injector import model, gpg, download, trust, background, arch, selections, qdom, run +from zeroinstall.injector.policy import Policy from zeroinstall.zerostore import Store, NotStored; Store._add_with_helper = lambda *unused: False from zeroinstall.support import basedir, tasks from zeroinstall.injector import fetch @@ -54,26 +55,11 @@ class Reply: def readline(self): return self.reply -class DummyHandler(handler.Handler): - __slots__ = ['ex', 'tb'] - - def __init__(self): - handler.Handler.__init__(self) - self.ex = None - - def wait_for_blocker(self, blocker): - self.ex = None - handler.Handler.wait_for_blocker(self, blocker) - if self.ex: - raise self.ex, None, self.tb - - def report_error(self, ex, tb = None): - assert self.ex is None, self.ex - self.ex = ex - self.tb = tb - - #import traceback - #traceback.print_exc() +def download_and_execute(policy, prog_args, main = None): + downloaded = policy.solve_and_download_impls() + if downloaded: + policy.config.handler.wait_for_blocker(downloaded) + run.execute_selections(policy.solver.selections, prog_args, stores = policy.config.stores, main = main) class NetworkManager: def state(self): @@ -83,6 +69,9 @@ class TestDownload(BaseTest): def setUp(self): BaseTest.setUp(self) + self.config.handler.allow_downloads = True + self.config.fetcher = fetch.Fetcher(self.config.handler) + stream = tempfile.TemporaryFile() stream.write(data.thomas_key) stream.seek(0) @@ -90,45 +79,45 @@ class TestDownload(BaseTest): self.child = None trust.trust_db.watchers = [] - + def tearDown(self): BaseTest.tearDown(self) if self.child is not None: os.kill(self.child, signal.SIGTERM) os.waitpid(self.child, 0) self.child = None - + def testRejectKey(self): with output_suppressed(): self.child = server.handle_requests('Hello', '6FCF121BE2390E0B.gpg', '/key-info/key/DE937DD411906ACF7C263B396FCF121BE2390E0B') - policy = autopolicy.AutoPolicy('http://localhost:8000/Hello', download_only = False, - handler = DummyHandler()) + policy = Policy('http://localhost:8000/Hello', config = self.config) assert policy.need_download() sys.stdin = Reply("N\n") try: - policy.download_and_execute(['Hello']) + download_and_execute(policy, ['Hello']) assert 0 except model.SafeException, 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 - + self.config.handler.ex = None + def testRejectKeyXML(self): with output_suppressed(): self.child = server.handle_requests('Hello.xml', '6FCF121BE2390E0B.gpg', '/key-info/key/DE937DD411906ACF7C263B396FCF121BE2390E0B') - policy = autopolicy.AutoPolicy('http://example.com:8000/Hello.xml', download_only = False, - handler = DummyHandler()) + policy = Policy('http://example.com:8000/Hello.xml', config = self.config) assert policy.need_download() sys.stdin = Reply("N\n") try: - policy.download_and_execute(['Hello']) + download_and_execute(policy, ['Hello']) assert 0 except model.SafeException, 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 + self.config.handler.ex = None def testImport(self): from zeroinstall.injector import cli @@ -145,7 +134,7 @@ class TestDownload(BaseTest): rootLogger.disabled = False rootLogger.setLevel(WARN) - hello = iface_cache.iface_cache.get_feed('http://localhost:8000/Hello') + hello = self.config.iface_cache.get_feed('http://localhost:8000/Hello') self.assertEquals(None, hello) with output_suppressed(): @@ -157,7 +146,7 @@ class TestDownload(BaseTest): assert trust.trust_db.is_trusted('DE937DD411906ACF7C263B396FCF121BE2390E0B') # Check we imported the interface after trusting the key - hello = iface_cache.iface_cache.get_feed('http://localhost:8000/Hello', force = True) + hello = self.config.iface_cache.get_feed('http://localhost:8000/Hello', force = True) self.assertEquals(1, len(hello.implementations)) # Shouldn't need to prompt the second time @@ -174,15 +163,15 @@ class TestDownload(BaseTest): self.child = server.handle_requests('Hello.xml', '6FCF121BE2390E0B.gpg', '/key-info/key/DE937DD411906ACF7C263B396FCF121BE2390E0B', 'HelloWorld.tgz') sys.stdin = Reply("Y\n") try: - iface_cache.iface_cache.stores.lookup_any(sels.selections['http://example.com:8000/Hello.xml'].digests) + 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']) - path = iface_cache.iface_cache.stores.lookup_any(sels.selections['http://example.com:8000/Hello.xml'].digests) + 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(iface_cache.iface_cache, None) is None + assert sels.download_missing(self.config) is None def testHelpers(self): from zeroinstall import helpers @@ -191,9 +180,9 @@ class TestDownload(BaseTest): self.child = server.handle_requests('Hello.xml', '6FCF121BE2390E0B.gpg', '/key-info/key/DE937DD411906ACF7C263B396FCF121BE2390E0B', 'HelloWorld.tgz') sys.stdin = Reply("Y\n") sels = helpers.ensure_cached('http://example.com:8000/Hello.xml') - path = iface_cache.iface_cache.stores.lookup_any(sels.selections['http://example.com:8000/Hello.xml'].digests) + 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(iface_cache.iface_cache, None) is None + assert sels.download_missing(self.config) is None def testSelectionsWithFeed(self): from zeroinstall.injector import cli @@ -204,56 +193,51 @@ class TestDownload(BaseTest): self.child = server.handle_requests('Hello.xml', '6FCF121BE2390E0B.gpg', '/key-info/key/DE937DD411906ACF7C263B396FCF121BE2390E0B', 'HelloWorld.tgz') sys.stdin = Reply("Y\n") - from zeroinstall.injector.handler import Handler - handler = Handler() - fetcher = fetch.Fetcher(handler) - handler.wait_for_blocker(fetcher.download_and_import_feed('http://example.com:8000/Hello.xml', iface_cache.iface_cache)) + 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']) - path = iface_cache.iface_cache.stores.lookup_any(sels.selections['http://example.com:8000/Hello.xml'].digests) + 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(iface_cache.iface_cache, None) is None + assert sels.download_missing(self.config) is None def testAcceptKey(self): with output_suppressed(): self.child = server.handle_requests('Hello', '6FCF121BE2390E0B.gpg', '/key-info/key/DE937DD411906ACF7C263B396FCF121BE2390E0B', 'HelloWorld.tgz') - policy = autopolicy.AutoPolicy('http://localhost:8000/Hello', download_only = False, - handler = DummyHandler()) + policy = Policy('http://localhost:8000/Hello', config = self.config) assert policy.need_download() sys.stdin = Reply("Y\n") try: - policy.download_and_execute(['Hello'], main = 'Missing') + download_and_execute(policy, ['Hello'], main = 'Missing') assert 0 except model.SafeException, ex: if "HelloWorld/Missing" not in str(ex): - raise ex + raise def testDistro(self): with output_suppressed(): native_url = 'http://example.com:8000/Native.xml' # Initially, we don't have the feed at all... - master_feed = iface_cache.iface_cache.get_feed(native_url) + master_feed = self.config.iface_cache.get_feed(native_url) assert master_feed is None, master_feed trust.trust_db.trust_key('DE937DD411906ACF7C263B396FCF121BE2390E0B', 'example.com:8000') self.child = server.handle_requests('Native.xml', '6FCF121BE2390E0B.gpg', '/key-info/key/DE937DD411906ACF7C263B396FCF121BE2390E0B') - h = DummyHandler() - policy = autopolicy.AutoPolicy(native_url, download_only = False, handler = h) + policy = Policy(native_url, config = self.config) assert policy.need_download() solve = policy.solve_with_downloads() - h.wait_for_blocker(solve) + self.config.handler.wait_for_blocker(solve) tasks.check(solve) - master_feed = iface_cache.iface_cache.get_feed(native_url) + master_feed = self.config.iface_cache.get_feed(native_url) assert master_feed is not None assert master_feed.implementations == {} distro_feed_url = master_feed.get_distro_feed() assert distro_feed_url is not None - distro_feed = iface_cache.iface_cache.get_feed(distro_feed_url) + distro_feed = self.config.iface_cache.get_feed(distro_feed_url) assert distro_feed is not None assert len(distro_feed.implementations) == 2, distro_feed.implementations @@ -261,12 +245,11 @@ class TestDownload(BaseTest): with output_suppressed(): self.child = server.handle_requests('Hello-wrong-size', '6FCF121BE2390E0B.gpg', '/key-info/key/DE937DD411906ACF7C263B396FCF121BE2390E0B', 'HelloWorld.tgz') - policy = autopolicy.AutoPolicy('http://localhost:8000/Hello-wrong-size', download_only = False, - handler = DummyHandler()) + policy = Policy('http://localhost:8000/Hello-wrong-size', config = self.config) assert policy.need_download() sys.stdin = Reply("Y\n") try: - policy.download_and_execute(['Hello'], main = 'Missing') + download_and_execute(policy, ['Hello'], main = 'Missing') assert 0 except model.SafeException, ex: if "Downloaded archive has incorrect size" not in str(ex): @@ -277,9 +260,9 @@ class TestDownload(BaseTest): try: sys.stdout = StringIO() self.child = server.handle_requests(('HelloWorld.tar.bz2', 'dummy_1-1_all.deb')) - policy = autopolicy.AutoPolicy(os.path.abspath('Recipe.xml'), download_only = False) + policy = Policy(os.path.abspath('Recipe.xml'), config = self.config) try: - policy.download_and_execute([]) + download_and_execute(policy, []) assert False except model.SafeException, ex: if "HelloWorld/Missing" not in str(ex): @@ -292,10 +275,9 @@ class TestDownload(BaseTest): try: sys.stdout = StringIO() self.child = server.handle_requests(('HelloWorld.tar.bz2', 'HelloSym.tgz')) - policy = autopolicy.AutoPolicy(os.path.abspath('RecipeSymlink.xml'), download_only = False, - handler = DummyHandler()) + policy = Policy(os.path.abspath('RecipeSymlink.xml'), config = self.config) try: - policy.download_and_execute([]) + download_and_execute(policy, []) assert False except model.SafeException, ex: if 'Attempt to unpack dir over symlink "HelloWorld"' not in str(ex): @@ -309,9 +291,9 @@ class TestDownload(BaseTest): try: sys.stdout = StringIO() self.child = server.handle_requests('HelloWorld.autopackage') - policy = autopolicy.AutoPolicy(os.path.abspath('Autopackage.xml'), download_only = False) + policy = Policy(os.path.abspath('Autopackage.xml'), config = self.config) try: - policy.download_and_execute([]) + download_and_execute(policy, []) assert False except model.SafeException, ex: if "HelloWorld/Missing" not in str(ex): @@ -324,10 +306,9 @@ class TestDownload(BaseTest): try: sys.stdout = StringIO() self.child = server.handle_requests('*') - policy = autopolicy.AutoPolicy(os.path.abspath('Recipe.xml'), download_only = False, - handler = DummyHandler()) + policy = Policy(os.path.abspath('Recipe.xml'), config = self.config) try: - policy.download_and_execute([]) + download_and_execute(policy, []) assert False except download.DownloadError, ex: if "Connection" not in str(ex): @@ -342,7 +323,7 @@ class TestDownload(BaseTest): getLogger().setLevel(ERROR) trust.trust_db.trust_key('DE937DD411906ACF7C263B396FCF121BE2390E0B', 'example.com:8000') self.child = server.handle_requests(server.Give404('/Hello.xml'), 'latest.xml', '/0mirror/keys/6FCF121BE2390E0B.gpg') - policy = autopolicy.AutoPolicy('http://example.com:8000/Hello.xml', download_only = False) + policy = Policy('http://example.com:8000/Hello.xml', config = self.config) policy.fetcher.feed_mirror = 'http://example.com:8000/0mirror' refreshed = policy.solve_with_downloads() @@ -356,21 +337,21 @@ class TestDownload(BaseTest): try: sys.stdout = StringIO() getLogger().setLevel(ERROR) - iface = iface_cache.iface_cache.get_interface('http://example.com:8000/Hello.xml') + iface = self.config.iface_cache.get_interface('http://example.com:8000/Hello.xml') mtime = int(os.stat('Hello-new.xml').st_mtime) - iface_cache.iface_cache.update_feed_from_network(iface.uri, file('Hello-new.xml').read(), mtime + 10000) + self.config.iface_cache.update_feed_from_network(iface.uri, file('Hello-new.xml').read(), mtime + 10000) trust.trust_db.trust_key('DE937DD411906ACF7C263B396FCF121BE2390E0B', 'example.com:8000') self.child = server.handle_requests(server.Give404('/Hello.xml'), 'latest.xml', '/0mirror/keys/6FCF121BE2390E0B.gpg', 'Hello.xml') - policy = autopolicy.AutoPolicy('http://example.com:8000/Hello.xml', download_only = False) + policy = Policy('http://example.com:8000/Hello.xml', config = self.config) policy.fetcher.feed_mirror = 'http://example.com:8000/0mirror' # Update from mirror (should ignore out-of-date timestamp) - refreshed = policy.fetcher.download_and_import_feed(iface.uri, iface_cache.iface_cache) + refreshed = policy.fetcher.download_and_import_feed(iface.uri, self.config.iface_cache) policy.handler.wait_for_blocker(refreshed) # Update from upstream (should report an error) - refreshed = policy.fetcher.download_and_import_feed(iface.uri, iface_cache.iface_cache) + refreshed = policy.fetcher.download_and_import_feed(iface.uri, self.config.iface_cache) try: policy.handler.wait_for_blocker(refreshed) raise Exception("Should have been rejected!") @@ -378,17 +359,17 @@ class TestDownload(BaseTest): assert "New feed's modification time is before old version" in str(ex) # Must finish with the newest version - self.assertEquals(1235911552, iface_cache.iface_cache._get_signature_date(iface.uri)) + self.assertEquals(1235911552, self.config.iface_cache._get_signature_date(iface.uri)) finally: sys.stdout = old_out def testBackground(self, verbose = False): - p = autopolicy.AutoPolicy('http://example.com:8000/Hello.xml') - reader.update(iface_cache.iface_cache.get_interface(p.root), 'Hello.xml') + p = Policy('http://example.com:8000/Hello.xml', config = self.config) + self.import_feed(p.root, 'Hello.xml') p.freshness = 0 p.network_use = model.network_minimal p.solver.solve(p.root, arch.get_host_architecture()) - assert p.ready + assert p.ready, p.solver.get_failure_reason() @tasks.async def choose_download(registed_cb, nid, actions): diff --git a/tests/testifacecache.py b/tests/testifacecache.py index 740de48..a6e8e6e 100755 --- a/tests/testifacecache.py +++ b/tests/testifacecache.py @@ -7,11 +7,12 @@ import data sys.path.insert(0, '..') from zeroinstall.injector import model, gpg, trust from zeroinstall.injector.namespaces import config_site -from zeroinstall.injector.iface_cache import iface_cache, PendingFeed +from zeroinstall.injector.iface_cache import PendingFeed from zeroinstall.support import basedir class TestIfaceCache(BaseTest): def testList(self): + iface_cache = self.config.iface_cache self.assertEquals([], iface_cache.list_all_interfaces()) iface_dir = basedir.save_cache_path(config_site, 'interfaces') file(os.path.join(iface_dir, 'http%3a%2f%2ffoo'), 'w').close() @@ -20,6 +21,7 @@ class TestIfaceCache(BaseTest): # TODO: test overrides def testCheckSigned(self): + iface_cache = self.config.iface_cache trust.trust_db.trust_key( '92429807C9853C0744A68B9AAE07828059A53CC1') feed_url = 'http://foo' @@ -58,6 +60,7 @@ class TestIfaceCache(BaseTest): self.assertEquals(1154850229, feed.last_modified) def testXMLupdate(self): + iface_cache = self.config.iface_cache trust.trust_db.trust_key( '92429807C9853C0744A68B9AAE07828059A53CC1') stream = tempfile.TemporaryFile() @@ -106,6 +109,7 @@ class TestIfaceCache(BaseTest): pass def testTimes(self): + iface_cache = self.config.iface_cache stream = tempfile.TemporaryFile() stream.write(data.thomas_key) stream.seek(0) @@ -139,6 +143,7 @@ class TestIfaceCache(BaseTest): assert signed == None def testCheckAttempt(self): + iface_cache = self.config.iface_cache self.assertEquals(None, iface_cache.get_last_check_attempt("http://foo/bar.xml")) start_time = time.time() - 5 # Seems to be some odd rounding here diff --git a/tests/testinstall.py b/tests/testinstall.py new file mode 100755 index 0000000..9462b71 --- /dev/null +++ b/tests/testinstall.py @@ -0,0 +1,324 @@ +#!/usr/bin/env python +from basetest import BaseTest, TestStores +import sys +from StringIO import StringIO +import unittest + +sys.path.insert(0, '..') +from zeroinstall import cmd +from zeroinstall.injector import model, selections, qdom, reader, policy, handler, gpg + +class Reply: + def __init__(self, reply): + self.reply = reply + + def readline(self): + return self.reply + +class TestInstall(BaseTest): + def run_0install(self, args): + old_stdout = sys.stdout + old_stderr = sys.stderr + try: + sys.stdout = StringIO() + sys.stderr = StringIO() + ex = None + try: + cmd.main(args, config = self.config) + except NameError: + raise + except SystemExit: + pass + except TypeError: + raise + except AttributeError: + raise + except AssertionError: + raise + except Exception, ex: + pass + out = sys.stdout.getvalue() + err = sys.stderr.getvalue() + if ex is not None: + err += str(ex.__class__) + finally: + sys.stdout = old_stdout + sys.stderr = old_stderr + return (out, err) + + def testHelp(self): + out, err = self.run_0install([]) + assert out.lower().startswith("usage:") + assert 'add-feed' in out + assert '--version' in out + assert not err, err + + out2, err = self.run_0install(['--help']) + assert not err, err + assert out2 == out + + out, err = self.run_0install(['--version']) + assert 'Thomas Leonard' in out + assert not err, err + + out, err = self.run_0install(['foobar']) + assert 'Unknown sub-command' in err, err + + 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 testDownload(self): + out, err = self.run_0install(['download']) + assert out.lower().startswith("usage:") + assert '--show' in out + + 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(StringIO(str(out)))) + 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 testDownloadSelections(self): + self.config.stores = TestStores() + digest = 'sha1=3ce644dc725f1d21cfcf02562c76f375944b266a' + self.config.fetcher.allow_download(digest) + hello = reader.load_feed('Hello.xml') + self.config.fetcher.allow_feed_download('http://example.com:8000/Hello.xml', hello) + out, err = self.run_0install(['download', 'selections.xml', '--show']) + assert not err, err + assert self.config.stores.lookup_any([digest]).startswith('/fake') + assert 'Version: 1\n' in out + + def testUpdate(self): + out, err = self.run_0install(['update']) + assert out.lower().startswith("usage:") + assert '--message' in out, out + + # Updating a local feed with no dependencies + out, err = self.run_0install(['update', 'Local.xml']) + assert not err, err + assert 'No updates found' in out, out + + # Using a remote feed for the first time + self.config.stores = TestStores() + binary_feed = reader.load_feed('Binary.xml') + self.config.fetcher.allow_download('sha1=123') + self.config.fetcher.allow_feed_download('http://foo/Binary.xml', binary_feed) + out, err = self.run_0install(['update', 'http://foo/Binary.xml']) + assert not err, err + assert 'Binary.xml: new -> 1.0' in out, out + + # No updates. + self.config.fetcher.allow_feed_download('http://foo/Binary.xml', binary_feed) + out, err = self.run_0install(['update', 'http://foo/Binary.xml']) + assert not err, err + assert 'No updates found' in out, out + + # New binary release available. + new_binary_feed = reader.load_feed('Binary.xml') + new_binary_feed.implementations['sha1=123'].version = model.parse_version('1.1') + self.config.fetcher.allow_feed_download('http://foo/Binary.xml', new_binary_feed) + out, err = self.run_0install(['update', 'http://foo/Binary.xml']) + assert not err, err + assert 'Binary.xml: 1.0 -> 1.1' in out, out + + # Compiling from source for the first time. + source_feed = reader.load_feed('Source.xml') + compiler_feed = reader.load_feed('Compiler.xml') + self.config.fetcher.allow_download('sha1=234') + self.config.fetcher.allow_download('sha1=345') + self.config.fetcher.allow_feed_download('http://foo/Compiler.xml', compiler_feed) + self.config.fetcher.allow_feed_download('http://foo/Binary.xml', binary_feed) + self.config.fetcher.allow_feed_download('http://foo/Source.xml', source_feed) + out, err = self.run_0install(['update', 'http://foo/Binary.xml', '--source']) + assert not err, err + assert 'Binary.xml: new -> 1.0' in out, out + assert 'Compiler.xml: new -> 1.0' in out, out + + # New compiler released. + new_compiler_feed = reader.load_feed('Compiler.xml') + new_compiler_feed.implementations['sha1=345'].version = model.parse_version('1.1') + self.config.fetcher.allow_feed_download('http://foo/Compiler.xml', new_compiler_feed) + self.config.fetcher.allow_feed_download('http://foo/Binary.xml', binary_feed) + self.config.fetcher.allow_feed_download('http://foo/Source.xml', source_feed) + out, err = self.run_0install(['update', 'http://foo/Binary.xml', '--source']) + assert not err, err + assert 'Compiler.xml: 1.0 -> 1.1' in out, out + + # A dependency disappears. + new_source_feed = reader.load_feed('Source.xml') + new_source_feed.implementations['sha1=234'].requires = [] + self.config.fetcher.allow_feed_download('http://foo/Compiler.xml', new_compiler_feed) + self.config.fetcher.allow_feed_download('http://foo/Binary.xml', binary_feed) + self.config.fetcher.allow_feed_download('http://foo/Source.xml', new_source_feed) + out, err = self.run_0install(['update', 'http://foo/Binary.xml', '--source']) + assert not err, err + assert 'No longer used: http://foo/Compiler.xml' in out, out + + def testConfig(self): + out, err = self.run_0install(['config', '--help']) + assert out.lower().startswith("usage:") + assert '--console' in out + + out, err = self.run_0install(['config']) + assert not err, err + assert 'full' in out, out + assert 'freshness = 0' in out, out + assert 'help_with_testing = False' in out, out + + out, err = self.run_0install(['config', 'help_with_testing']) + assert out == 'False\n', out + + file_config = policy.load_config(handler.Handler()) + def get_value(name): + old_stdout = sys.stdout + sys.stdout = StringIO() + try: + cmd.config.handle(file_config, None, [name]) + cmd_output = sys.stdout.getvalue() + finally: + sys.stdout = old_stdout + return cmd_output + + assert get_value('freshness') == '30d\n' + assert get_value('network_use') == 'full\n' + assert get_value('help_with_testing') == 'False\n' + + cmd.config.handle(file_config, None, ['freshness', '5m']) + cmd.config.handle(file_config, None, ['help_with_testing', 'True']) + cmd.config.handle(file_config, None, ['network_use', 'minimal']) + assert file_config.freshness == 5 * 60 + assert file_config.network_use == model.network_minimal + assert file_config.help_with_testing == True + + file_config2 = policy.load_config(handler.Handler()) + assert file_config2.freshness == 5 * 60 + assert file_config2.network_use == model.network_minimal + assert file_config2.help_with_testing == True + + cmd.config.handle(file_config, None, ['help_with_testing', 'falsE']) + assert file_config.help_with_testing == False + + for period in ['1s', '2d', '3.5m', '4h', '5d']: + secs = cmd.config.TimeInterval.parse(period) + assert cmd.config.TimeInterval.format(secs) == period + + def testAddFeed(self): + out, err = self.run_0install(['add-feed']) + assert out.lower().startswith("usage:") + assert 'NEW-FEED' in out + + sys.stdin = Reply('1') + binary_iface = self.config.iface_cache.get_interface('http://foo/Binary.xml') + assert binary_iface.extra_feeds == [] + + out, err = self.run_0install(['add-feed', 'Source.xml']) + assert not err, err + assert "Add as feed for 'http://foo/Binary.xml'" in out, out + assert len(binary_iface.extra_feeds) == 1 + + + out, err = self.run_0install(['remove-feed', 'Source.xml']) + assert not err, err + assert "Remove as feed for 'http://foo/Binary.xml'" in out, out + assert len(binary_iface.extra_feeds) == 0 + + source_feed = reader.load_feed('Source.xml') + self.config.fetcher.allow_feed_download('http://foo/Source.xml', source_feed) + out, err = self.run_0install(['add-feed', 'http://foo/Source.xml']) + assert not err, err + assert 'Downloading feed; please wait' in out, out + assert len(binary_iface.extra_feeds) == 1 + + def testImport(self): + out, err = self.run_0install(['import']) + assert out.lower().startswith("usage:") + assert 'FEED' in out + + stream = file('6FCF121BE2390E0B.gpg') + gpg.import_key(stream) + stream.close() + sys.stdin = Reply('Y\n') + out, err = self.run_0install(['import', 'Hello.xml']) + assert not out, out + assert 'Trusting DE937DD411906ACF7C263B396FCF121BE2390E0B for example.com:8000' in err, out + + def testList(self): + out, err = self.run_0install(['list', 'foo', 'bar']) + assert out.lower().startswith("usage:") + assert 'PATTERN' in out + + out, err = self.run_0install(['list']) + assert not err, err + assert '' == out, repr(out) + + self.testImport() + + out, err = self.run_0install(['list']) + assert not err, err + assert 'http://example.com:8000/Hello.xml\n' == out, repr(out) + + out, err = self.run_0install(['list', 'foo']) + assert not err, err + assert '' == out, repr(out) + + out, err = self.run_0install(['list', 'hello']) + assert not err, err + assert 'http://example.com:8000/Hello.xml\n' == out, repr(out) + + def testRun(self): + out, err = self.run_0install(['run']) + assert out.lower().startswith("usage:") + assert 'URI' in out, out + + out, err = self.run_0install(['run', '--dry-run', 'runnable/Runnable.xml', '--help']) + assert not err, err + assert 'arg-for-runner' in out, out + assert '--help' in out, out + +if __name__ == '__main__': + unittest.main() diff --git a/tests/testlaunch.py b/tests/testlaunch.py index f59b132..21d0fbb 100755 --- a/tests/testlaunch.py +++ b/tests/testlaunch.py @@ -9,9 +9,9 @@ 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.injector.policy import Policy +from zeroinstall.injector import run, cli, namespaces, qdom, selections from zeroinstall.zerostore import Store; Store._add_with_helper = lambda *unused: False -from zeroinstall.support import basedir mydir = os.path.abspath(os.path.dirname(__file__)) @@ -118,9 +118,12 @@ class TestLaunch(BaseTest): """ % foo_iface_uri) tmp.flush() - policy = autopolicy.AutoPolicy(tmp.name) + policy = Policy(tmp.name, config = self.config) try: - policy.download_and_execute([]) + downloaded = policy.solve_and_download_impls() + if downloaded: + policy.handler.wait_for_blocker(downloaded) + run.execute_selections(policy.solver.selections, [], stores = policy.config.stores) assert False except SafeException, ex: assert 'Command path must be relative' in str(ex), ex @@ -151,16 +154,12 @@ class TestLaunch(BaseTest): self.assertEquals("", err) def testNeedDownload(self): - policy = autopolicy.AutoPolicy(foo_iface_uri) - policy.save_config() os.environ['DISPLAY'] = ':foo' out, err = self.run_0launch(['--download-only', '--dry-run', 'Foo.xml']) self.assertEquals("", err) self.assertEquals("Finished\n", out) def testSelectOnly(self): - policy = autopolicy.AutoPolicy(foo_iface_uri) - policy.save_config() os.environ['DISPLAY'] = ':foo' out, err = self.run_0launch(['--get-selections', '--select-only', 'Hello.xml']) self.assertEquals("", err) diff --git a/tests/testmanifest.py b/tests/testmanifest.py index 2e3d064..a7dbe73 100755 --- a/tests/testmanifest.py +++ b/tests/testmanifest.py @@ -39,7 +39,7 @@ class TestManifest(BaseTest): def testOldSHA(self): mydir = os.path.join(self.tmpdir, 'MyDir') os.mkdir(mydir) - myfile = self.write('MyDir/Hello', 'Hello World', 30) + self.write('MyDir/Hello', 'Hello World', 30) myexec = self.write('MyDir/Run me', 'Bang!', 40) os.symlink('Hello', os.path.join(self.tmpdir, 'MyDir/Sym link')) os.chmod(myexec, 0700) @@ -54,7 +54,7 @@ class TestManifest(BaseTest): def testNewSHA1(self): mydir = os.path.join(self.tmpdir, 'MyDir') os.mkdir(mydir) - myfile = self.write('MyDir/Hello', 'Hello World', 30) + self.write('MyDir/Hello', 'Hello World', 30) myexec = self.write('MyDir/Run me', 'Bang!', 40) os.symlink('Hello', os.path.join(self.tmpdir, 'MyDir/Sym link')) os.chmod(myexec, 0700) @@ -69,8 +69,8 @@ class TestManifest(BaseTest): def testOrderingSHA1(self): mydir = os.path.join(self.tmpdir, 'Dir') os.mkdir(mydir) - myfile = self.write('Hello', 'Hello World', 30) - myfile = self.write('Dir/Hello', 'Hello World', 30) + self.write('Hello', 'Hello World', 30) + self.write('Dir/Hello', 'Hello World', 30) os.utime(mydir, (10, 20)) self.assertEquals([ 'F 0a4d55a8d778e5022fab701977c5d840bbc486d0 30 11 Hello', @@ -81,7 +81,7 @@ class TestManifest(BaseTest): def testNewSHA256(self): mydir = os.path.join(self.tmpdir, 'MyDir') os.mkdir(mydir) - myfile = self.write('MyDir/Hello', 'Hello World', 30) + self.write('MyDir/Hello', 'Hello World', 30) myexec = self.write('MyDir/Run me', 'Bang!', 40) os.symlink('Hello', os.path.join(self.tmpdir, 'MyDir/Sym link')) os.chmod(myexec, 0700) @@ -95,7 +95,7 @@ class TestManifest(BaseTest): def testOrdering(self): mydir = os.path.join(self.tmpdir, 'Dir') os.mkdir(mydir) - myfile = self.write('Hello', 'Hello World', 30) + self.write('Hello', 'Hello World', 30) os.utime(mydir, (10, 20)) self.assertEquals([ 'F a591a6d40bf420404a011733cfb7b190d62c65bf0bcda32b57b277d9ad9f146e 30 11 Hello', diff --git a/tests/testmodel.py b/tests/testmodel.py index a4fe424..b4f9ea8 100755 --- a/tests/testmodel.py +++ b/tests/testmodel.py @@ -2,13 +2,13 @@ # -*- coding: utf-8 -*- import basetest from basetest import BaseTest, empty_feed -import sys, os, locale +import sys, os from xml.dom import minidom import unittest from StringIO import StringIO sys.path.insert(0, '..') -from zeroinstall.injector import model, qdom, iface_cache, namespaces +from zeroinstall.injector import model, qdom, namespaces mydir = os.path.dirname(__file__) @@ -285,7 +285,7 @@ class TestModel(BaseTest): try: pv(v) assert False - except model.SafeException, ex: + except model.SafeException: pass invalid('.') invalid('hello') diff --git a/tests/testpolicy.py b/tests/testpolicy.py index eb247cc..511deb7 100755 --- a/tests/testpolicy.py +++ b/tests/testpolicy.py @@ -4,8 +4,8 @@ import sys import unittest sys.path.insert(0, '..') -from zeroinstall.injector import autopolicy, reader, model -from zeroinstall.injector.iface_cache import iface_cache +from zeroinstall.injector import model +from zeroinstall.injector.policy import Policy import warnings import logging @@ -14,25 +14,27 @@ logger = logging.getLogger() class TestPolicy(BaseTest): def testSource(self): + iface_cache = self.config.iface_cache warnings.filterwarnings("ignore", category = DeprecationWarning) foo = iface_cache.get_interface('http://foo/Binary.xml') - reader.update(foo, 'Binary.xml') + self.import_feed(foo.uri, 'Binary.xml') foo_src = iface_cache.get_interface('http://foo/Source.xml') - reader.update(foo_src, 'Source.xml') + self.import_feed(foo_src.uri, 'Source.xml') compiler = iface_cache.get_interface('http://foo/Compiler.xml') - reader.update(compiler, 'Compiler.xml') + self.import_feed(compiler.uri, 'Compiler.xml') - p = autopolicy.AutoPolicy('http://foo/Binary.xml', dry_run = True) + p = Policy('http://foo/Binary.xml', config = self.config) p.freshness = 0 p.network_use = model.network_full p.recalculate() # Deprecated assert p.implementation[foo].id == 'sha1=123' # Now ask for source instead - p.src = True - p.command = 'compile' + p.requirements.source = True + p.requirements.command = 'compile' p.recalculate() + assert p.solver.ready, p.solver.get_failure_reason() 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 304c982..30dc77c 100755 --- a/tests/testreader.py +++ b/tests/testreader.py @@ -6,7 +6,6 @@ import unittest sys.path.insert(0, '..') from zeroinstall.injector import model, gpg, reader -from zeroinstall.injector.iface_cache import iface_cache import data foo_iface_uri = 'http://foo' @@ -73,8 +72,8 @@ class TestReader(BaseTest): """ % (foo_iface_uri, bar_iface_uri, bar_iface_uri)) tmp.flush() iface = model.Interface(foo_iface_uri) - reader.update(iface, tmp.name) - feed = iface_cache.get_feed(foo_iface_uri) + reader.update(iface, tmp.name, iface_cache = self.config.iface_cache) + feed = self.config.iface_cache.get_feed(foo_iface_uri) impl = feed.implementations['sha1=123'] assert len(impl.dependencies) == 2 @@ -113,9 +112,9 @@ class TestReader(BaseTest): """) tmp.flush() iface = model.Interface(foo_iface_uri) - reader.update(iface, tmp.name, local = True) + reader.update(iface, tmp.name, local = True, iface_cache = self.config.iface_cache) - feed = iface_cache.get_feed(foo_iface_uri) + feed = self.config.iface_cache.get_feed(foo_iface_uri) impl = feed.implementations['sha1=123'] @@ -151,12 +150,13 @@ class TestReader(BaseTest): """ % foo_iface_uri) tmp.flush() iface = model.Interface(foo_iface_uri) - reader.update(iface, tmp.name) - feed = iface_cache.get_feed(foo_iface_uri) + reader.update(iface, tmp.name, iface_cache = self.config.iface_cache) + feed = self.config.iface_cache.get_feed(foo_iface_uri) impl = feed.implementations['sha1=123'] assert impl.version == [[1, 0], -1, [3], -2] def testAttrs(self): + iface_cache = self.config.iface_cache tmp = tempfile.NamedTemporaryFile(prefix = 'test-') tmp.write( """ @@ -173,7 +173,7 @@ class TestReader(BaseTest): """ % foo_iface_uri) tmp.flush() iface = model.Interface(foo_iface_uri) - reader.update(iface, tmp.name) + reader.update(iface, tmp.name, iface_cache = self.config.iface_cache) feed = iface_cache.get_feed(foo_iface_uri) @@ -187,6 +187,7 @@ class TestReader(BaseTest): assert feed.implementations['sha1=124'].metadata['main'] == 'next' def testNative(self): + iface_cache = self.config.iface_cache tmp = tempfile.NamedTemporaryFile(prefix = 'test-') tmp.write( """ @@ -200,7 +201,7 @@ class TestReader(BaseTest): tmp.flush() iface = model.Interface(foo_iface_uri) - reader.update(iface, tmp.name, True) + reader.update(iface, tmp.name, True, iface_cache = self.config.iface_cache) master_feed = iface_cache.get_feed(foo_iface_uri) assert len(master_feed.implementations) == 0 diff --git a/tests/testrun.py b/tests/testrun.py index 5425f55..fd67ecb 100755 --- a/tests/testrun.py +++ b/tests/testrun.py @@ -6,7 +6,7 @@ from StringIO import StringIO sys.path.insert(0, '..') -from zeroinstall.injector import policy, run, handler, model +from zeroinstall.injector import policy, run from zeroinstall import SafeException mydir = os.path.abspath(os.path.dirname(__file__)) @@ -22,26 +22,24 @@ class TestRun(BaseTest): assert 'Runner: script=A test script: args=command-arg -- user-arg' in stdout, stdout def testCommandBindings(self): - h = handler.Handler() - p = policy.Policy(command_feed, handler = h) - h.wait_for_blocker(p.solve_with_downloads()) + p = policy.Policy(command_feed, config = self.config) + self.config.handler.wait_for_blocker(p.solve_with_downloads()) old_stdout = sys.stdout try: sys.stdout = StringIO() - run.execute_selections(p.solver.selections, [], main = 'runner', dry_run = True) + run.execute_selections(p.solver.selections, [], main = 'runner', dry_run = True, stores = self.config.stores) finally: sys.stdout = old_stdout assert 'local' in os.environ['LOCAL'], os.environ['LOCAL'] def testAbsMain(self): - h = handler.Handler() - p = policy.Policy(command_feed, handler = h) - h.wait_for_blocker(p.solve_with_downloads()) + p = policy.Policy(command_feed, config = self.config) + self.config.handler.wait_for_blocker(p.solve_with_downloads()) old_stdout = sys.stdout try: sys.stdout = StringIO() - run.execute_selections(p.solver.selections, [], main = '/runnable/runner', dry_run = True) + run.execute_selections(p.solver.selections, [], main = '/runnable/runner', dry_run = True, stores = self.config.stores) finally: sys.stdout = old_stdout @@ -49,33 +47,31 @@ class TestRun(BaseTest): old_stdout = sys.stdout try: sys.stdout = StringIO() - run.execute_selections(p.solver.selections, [], main = '/runnable/not-there', dry_run = True) + run.execute_selections(p.solver.selections, [], main = '/runnable/not-there', dry_run = True, stores = self.config.stores) finally: sys.stdout = old_stdout except SafeException, ex: assert 'not-there' in unicode(ex) def testArgs(self): - h = handler.Handler() - p = policy.Policy(runnable, handler = h) - h.wait_for_blocker(p.solve_with_downloads()) + p = policy.Policy(runnable, config = self.config) + self.config.handler.wait_for_blocker(p.solve_with_downloads()) old_stdout = sys.stdout try: sys.stdout = StringIO() - run.execute_selections(p.solver.selections, [], dry_run = True) + run.execute_selections(p.solver.selections, [], dry_run = True, stores = self.config.stores) out = sys.stdout.getvalue() finally: sys.stdout = old_stdout assert 'runner-arg' in out, out def testWrapper(self): - h = handler.Handler() - p = policy.Policy(runnable, handler = h) - h.wait_for_blocker(p.solve_with_downloads()) + p = policy.Policy(runnable, config = self.config) + self.config.handler.wait_for_blocker(p.solve_with_downloads()) old_stdout = sys.stdout try: sys.stdout = StringIO() - run.execute_selections(p.solver.selections, [], wrapper = 'echo', dry_run = True) + run.execute_selections(p.solver.selections, [], wrapper = 'echo', dry_run = True, stores = self.config.stores) out = sys.stdout.getvalue() finally: sys.stdout = old_stdout diff --git a/tests/testsat.py b/tests/testsat.py index 3848182..579654f 100755 --- a/tests/testsat.py +++ b/tests/testsat.py @@ -1,7 +1,6 @@ #!/usr/bin/env python from basetest import BaseTest -import ConfigParser -import sys, os +import sys import unittest sys.path.insert(0, '..') @@ -132,12 +131,13 @@ def assertSelection(expected, repo): root = uri_prefix + expected[0][0] - test_config = ConfigParser.RawConfigParser() - test_config.add_section('global') - test_config.set('global', 'help_with_testing', 'False') - test_config.set('global', 'network_use', model.network_offline) + class TestConfig: + help_with_testing = False + network_use = model.network_offline + stores = stores + iface_cache = cache - s = Solver(test_config, cache, stores) + s = Solver(TestConfig()) s.solve(root, arch.get_architecture('Linux', 'x86_64')) if expected[0][1] == 'FAIL': @@ -146,8 +146,8 @@ def assertSelection(expected, repo): assert s.ready actual = [] - for iface, impl in s.selections.iteritems(): - actual.append(((iface.uri.rsplit('/', 1)[1]), impl.get_version())) + for iface_uri, impl in s.selections.selections.iteritems(): + actual.append(((iface_uri.rsplit('/', 1)[1]), impl.version)) expected.sort() actual.sort() @@ -286,9 +286,9 @@ class TestSAT(BaseTest): """) assert not s.ready selected = {} - for iface, impl in s.selections.iteritems(): - if impl is not None: impl = impl.get_version() - selected[iface.uri.rsplit('/', 1)[1]] = impl + for iface_uri, impl in s.selections.selections.iteritems(): + if impl is not None: impl = impl.version + selected[iface_uri.rsplit('/', 1)[1]] = impl self.assertEquals({ 'prog': '2', 'liba': '2', diff --git a/tests/testselections.py b/tests/testselections.py index ba04ba9..161d3ff 100755 --- a/tests/testselections.py +++ b/tests/testselections.py @@ -5,17 +5,17 @@ import sys, os import unittest sys.path.insert(0, '..') -from zeroinstall.injector import selections, model, reader, policy, iface_cache, namespaces, qdom +from zeroinstall.injector import selections, model, policy, namespaces, qdom mydir = os.path.dirname(os.path.abspath(__file__)) class TestSelections(BaseTest): def testSelections(self): - p = policy.Policy('http://foo/Source.xml', src = True) - source = iface_cache.iface_cache.get_interface('http://foo/Source.xml') - compiler = iface_cache.iface_cache.get_interface('http://foo/Compiler.xml') - reader.update(source, 'Source.xml') - reader.update(compiler, 'Compiler.xml') + p = policy.Policy('http://foo/Source.xml', src = True, config = self.config) + source = self.config.iface_cache.get_interface('http://foo/Source.xml') + compiler = self.config.iface_cache.get_interface('http://foo/Compiler.xml') + self.import_feed(source.uri, 'Source.xml') + self.import_feed(compiler.uri, 'Compiler.xml') p.freshness = 0 p.network_use = model.network_full @@ -69,7 +69,7 @@ class TestSelections(BaseTest): def testLocalPath(self): # 0launch --get-selections Local.xml iface = os.path.join(mydir, "Local.xml") - p = policy.Policy(iface) + p = policy.Policy(iface, config = self.config) p.need_download() s1 = selections.Selections(p) xml = s1.toDOM().toxml("utf-8") @@ -82,7 +82,7 @@ class TestSelections(BaseTest): 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) + 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'}), None) @@ -102,16 +102,16 @@ class TestSelections(BaseTest): def testCommands(self): iface = os.path.join(mydir, "Command.xml") - p = policy.Policy(iface) + p = policy.Policy(iface, config = self.config) p.need_download() assert p.ready - impl = p.solver.selections[iface_cache.iface_cache.get_interface(iface)] + 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[iface_cache.iface_cache.get_interface(dep_impl_uri)] + dep_impl = p.solver.selections[self.config.iface_cache.get_interface(dep_impl_uri)] assert dep_impl.id == 'sha1=256' s1 = selections.Selections(p) diff --git a/tests/testsolver.py b/tests/testsolver.py index 5ab6af8..ad962eb 100755 --- a/tests/testsolver.py +++ b/tests/testsolver.py @@ -1,34 +1,26 @@ #!/usr/bin/env python from basetest import BaseTest import sys, os, locale -import ConfigParser import unittest sys.path.insert(0, '..') -from zeroinstall.zerostore import Stores -from zeroinstall.injector import solver, reader, arch, model -from zeroinstall.injector.iface_cache import iface_cache +from zeroinstall.injector import solver, arch import logging logger = logging.getLogger() #logger.setLevel(logging.DEBUG) -test_config = ConfigParser.ConfigParser() -test_config.add_section('global') -test_config.set('global', 'help_with_testing', 'False') -test_config.set('global', 'freshness', str(60 * 60 * 24 * 30)) # One month -test_config.set('global', 'network_use', 'full') - class TestSolver(BaseTest): def testSimple(self): - s = solver.DefaultSolver(test_config, iface_cache, Stores()) + iface_cache = self.config.iface_cache + s = solver.DefaultSolver(self.config) foo = iface_cache.get_interface('http://foo/Binary.xml') - reader.update(foo, 'Binary.xml') + self.import_feed(foo.uri, 'Binary.xml') foo_src = iface_cache.get_interface('http://foo/Source.xml') - reader.update(foo_src, 'Source.xml') + self.import_feed(foo_src.uri, 'Source.xml') compiler = iface_cache.get_interface('http://foo/Compiler.xml') - reader.update(compiler, 'Compiler.xml') + self.import_feed(compiler.uri, 'Compiler.xml') binary_arch = arch.Architecture({None: 1}, {None: 1}) assert str(binary_arch).startswith(" Test @@ -17,7 +17,7 @@ class TestWriter(BaseTest): def testFeeds(self): iface = model.Interface('http://test/test') main_feed = model.ZeroInstallFeed(test_feed, local_path = '/Hello') - iface_cache.iface_cache._feeds[iface.uri] = main_feed + self.config.iface_cache._feeds[iface.uri] = main_feed iface.stability_policy = model.developer main_feed.last_checked = 100 iface.extra_feeds.append(model.Feed('http://sys-feed', None, False)) @@ -28,7 +28,7 @@ class TestWriter(BaseTest): iface = model.Interface('http://test/test') self.assertEquals(None, iface.stability_policy) main_feed = model.ZeroInstallFeed(test_feed, local_path = '/Hello') - iface_cache.iface_cache._feeds[iface.uri] = main_feed + self.config.iface_cache._feeds[iface.uri] = main_feed reader.update_user_overrides(iface) reader.update_user_feed_overrides(main_feed) self.assertEquals(model.developer, iface.stability_policy) @@ -48,7 +48,7 @@ class TestWriter(BaseTest): self.assertEquals(None, iface.stability_policy) reader.update_user_overrides(iface) - feed = iface_cache.iface_cache.get_feed(iface.uri) + feed = self.config.iface_cache.get_feed(iface.uri) self.assertEquals(None, feed) def testStoreStability(self): diff --git a/zeroinstall/0launch-gui/bugs.py b/zeroinstall/0launch-gui/bugs.py index 4dbc9af..dcc89b8 100644 --- a/zeroinstall/0launch-gui/bugs.py +++ b/zeroinstall/0launch-gui/bugs.py @@ -32,15 +32,16 @@ def report_bug(policy, iface): if not policy.ready: text += ' Failed to select all required implementations\n' - for chosen_iface in policy.implementation: - text += '\n Interface: %s\n' % chosen_iface.uri - impl = policy.implementation[chosen_iface] + 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.get_version() - if impl.feed.url != chosen_iface.uri: - text += ' From feed: %s\n' % impl.feed.url + 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] diff --git a/zeroinstall/0launch-gui/combo_compat.py b/zeroinstall/0launch-gui/combo_compat.py deleted file mode 100644 index a01df04..0000000 --- a/zeroinstall/0launch-gui/combo_compat.py +++ /dev/null @@ -1,71 +0,0 @@ -# Copyright (C) 2009, Thomas Leonard -# See the README file for details, or visit http://0install.net. - -import gtk, gobject - -class Action(gobject.GObject): - __proxy = None - __sensitive = True - - __gproperties__ = { - 'sensitive' : (gobject.TYPE_BOOLEAN, # type - 'sensitive', # nick name - 'sensitive', # description - True, # default value - gobject.PARAM_READWRITE) # flags - } - - __gsignals__ = { - 'activate' : (gobject.SIGNAL_RUN_LAST, gobject.TYPE_NONE, ()) - } - - def __init__(self, name, label, tooltip, stock_id): - gobject.GObject.__init__(self) - - def do_get_property(self, property): - return getattr(self, property.name) - - def do_set_property(self, property, value): - setattr(self, property.name, value) - - def connect_proxy(self, widget): - assert self.__proxy is None - self.__proxy = widget - self.sensitive = self.__sensitive - widget.connect('clicked', lambda w: self.emit('activate')) - - def set_sensitive(self, value): - if self.__proxy: - self.__proxy.set_sensitive(value) - self.__sensitive = value - - sensitive = property(lambda self: self.__sensitive, set_sensitive) - -gobject.type_register(Action) - -class ComboText(gtk.OptionMenu): - def __init__(self): - gtk.OptionMenu.__init__(self) - self.__menu = gtk.Menu() - self.__model = [] - self.set_menu(self.__menu) - - def append_text(self, text): - item = gtk.MenuItem(text) - self.__model.append([text]) - self.__menu.append(item) - - def set_active(self, i): - self.set_history(i) - - def get_active(self): - return self.get_history() - - def get_model(self): - return self.__model - -def combo_box_new_text(): - return ComboText() - -gtk.combo_box_new_text = combo_box_new_text -gtk.Action = Action diff --git a/zeroinstall/0launch-gui/compile.py b/zeroinstall/0launch-gui/compile.py index 2d534d0..cf0d636 100644 --- a/zeroinstall/0launch-gui/compile.py +++ b/zeroinstall/0launch-gui/compile.py @@ -4,64 +4,78 @@ import os, subprocess import gobject import dialog -from logging import info +from StringIO import StringIO + +from zeroinstall.injector import model, selections, qdom -from zeroinstall.injector import iface_cache, model -from zeroinstall.injector.policy import Policy - XMLNS_0COMPILE = 'http://zero-install.sourceforge.net/2006/namespaces/0compile' class Command: def __init__(self): self.child = None self.error = "" + self.stdout = "" + self.watched_streams = 0 - def run(self, command, success): + def run(self, command, success, get_stdout = False): assert self.child is None self.success = success - self.child = subprocess.Popen(command, - stdout = subprocess.PIPE, - stderr = subprocess.STDOUT) - gobject.io_add_watch(self.child.stdout, gobject.IO_IN | gobject.IO_HUP, self.got_data) - - def got_data(self, src, cond): + if get_stdout: + self.child = subprocess.Popen(command, + stdout = subprocess.PIPE, + stderr = subprocess.PIPE) + gobject.io_add_watch(self.child.stdout, gobject.IO_IN | gobject.IO_HUP, self.got_stdout) + gobject.io_add_watch(self.child.stderr, gobject.IO_IN | gobject.IO_HUP, self.got_errors) + self.watched_streams = 2 + else: + self.child = subprocess.Popen(command, + stdout = subprocess.PIPE, + stderr = subprocess.STDOUT) + gobject.io_add_watch(self.child.stdout, gobject.IO_IN | gobject.IO_HUP, self.got_errors) + self.watched_streams = 1 + + def got_stdout(self, src, cond): data = os.read(src.fileno(), 100) if data: - self.error += data + self.stdout += data return True else: + self.done() + return False + + def done(self): + self.watched_streams -= 1 + if self.watched_streams == 0: status = self.child.wait() self.child = None - if os.WIFEXITED(status) and os.WEXITSTATUS(status) == 0: - self.success() + if status == 0: + self.success(self.stdout) else: - if os.WIFEXITED(status): - status = os.WEXITSTATUS(status) - if status == 1 and not self.error: - return False # Cancelled - dialog.alert(None, _("Command failed with exit code %(status)d:\n%(error)s\n") % - {'status': status, 'error': self.error}) - else: - dialog.alert(None, _("Command failed:\n%s\n") % self.error) + if status == 1 and not self.error: + return False # Cancelled + dialog.alert(None, _("Command failed with exit code %(status)d:\n%(error)s\n") % + {'status': status, 'error': self.error}) + + def got_errors(self, src, cond): + data = os.read(src.fileno(), 100) + if data: + self.error += data + return True + else: + self.done() return False def compile(on_success, interface_uri, autocompile = False): our_min_version = '0.18' # The oldest version of 0compile we support - def build(): + def build(selections_xml): # Get the chosen versions - src_policy = Policy(interface_uri, src = True) - src_policy.freshness = 0 - - src_policy.recalculate() - if not src_policy.ready: - raise Exception(_('Internal error: required source components not found!')) + sels = selections.Selections(qdom.parse(StringIO(selections_xml))) - root_iface = iface_cache.iface_cache.get_interface(src_policy.root) - impl = src_policy.implementation[root_iface] + impl = sels.selections[interface_uri] - min_version = impl.metadata.get(XMLNS_0COMPILE + ' min-version', our_min_version) + 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 @@ -73,8 +87,7 @@ def compile(on_success, interface_uri, autocompile = False): '--not-before=' + min_version, "http://0install.net/2006/interfaces/0compile.xml", 'gui', - '--no-prompt', - interface_uri), on_success) + interface_uri), lambda unused: on_success()) if autocompile: c = Command() @@ -84,10 +97,10 @@ def compile(on_success, interface_uri, autocompile = False): "http://0install.net/2006/interfaces/0compile.xml", 'autocompile', '--gui', - interface_uri), on_success) + interface_uri), lambda unused: on_success()) else: # Prompt user to choose source version c = Command() - c.run(['0launch', + c.run(['0install', 'download', '--xml', '--message', _('Download the source code to be compiled'), - '--gui', '--source', '--download-only', interface_uri], build) + '--gui', '--source', '--', interface_uri], build, get_stdout = True) diff --git a/zeroinstall/0launch-gui/freshness.py b/zeroinstall/0launch-gui/freshness.py index 159ecde..6b66e53 100644 --- a/zeroinstall/0launch-gui/freshness.py +++ b/zeroinstall/0launch-gui/freshness.py @@ -12,7 +12,7 @@ class Freshness(object): return self.text freshness_levels = [ - Freshness(-1, _('No automatic updates')), + Freshness(0, _('No automatic updates')), #Freshness(60, _('Up to one minute old')), #Freshness(60 * 60, _('Up to one hour old')), Freshness(24 * 60 * 60, _('Up to one day old')), diff --git a/zeroinstall/0launch-gui/gui.py b/zeroinstall/0launch-gui/gui.py index 6ba8055..94f229a 100644 --- a/zeroinstall/0launch-gui/gui.py +++ b/zeroinstall/0launch-gui/gui.py @@ -67,7 +67,6 @@ class GUIHandler(handler.Handler): yield self._switch_to_main_window(_('Need to confirm installation of distribution packages')) from zeroinstall.injector.download import DownloadAborted - from zeroinstall.gtkui import gtkutils import dialog import gtk box = gtk.MessageDialog(self.mainwindow.window, diff --git a/zeroinstall/0launch-gui/iface_browser.py b/zeroinstall/0launch-gui/iface_browser.py index 2c95d44..86fa5e8 100644 --- a/zeroinstall/0launch-gui/iface_browser.py +++ b/zeroinstall/0launch-gui/iface_browser.py @@ -4,7 +4,6 @@ import gtk, gobject, pango from zeroinstall.support import tasks, pretty_size -from zeroinstall.injector.iface_cache import iface_cache from zeroinstall.injector import model, reader import properties from zeroinstall.gtkui.icon import load_icon @@ -23,14 +22,14 @@ def _stability(impl): ICON_SIZE = 20.0 CELL_TEXT_INDENT = int(ICON_SIZE) + 4 -def get_tooltip_text(mainwindow, interface, model_column): +def get_tooltip_text(mainwindow, interface, main_feed, model_column): assert interface if model_column == InterfaceBrowser.INTERFACE_NAME: return _("Full name: %s") % interface.uri elif model_column == InterfaceBrowser.SUMMARY: - if not interface.description: + if main_feed is None or not main_feed.description: return _("(no description available)") - first_para = interface.description.split('\n\n', 1)[0] + first_para = main_feed.description.split('\n\n', 1)[0] return first_para.replace('\n', ' ') elif model_column is None: return _("Click here for more options...") @@ -173,7 +172,9 @@ class InterfaceBrowser: else: col = self.columns[col_index][1] row = self.model[path] - tooltip.set_text(get_tooltip_text(self, row[InterfaceBrowser.INTERFACE], col)) + iface = row[InterfaceBrowser.INTERFACE] + main_feed = self.policy.config.iface_cache.get_feed(iface.uri) + tooltip.set_text(get_tooltip_text(self, iface, main_feed, col)) return True else: return False @@ -260,7 +261,7 @@ class InterfaceBrowser: return self.cached_icon[iface.uri] except KeyError: # Try the on-disk cache - iconpath = iface_cache.get_icon_path(iface) + iconpath = self.policy.config.iface_cache.get_icon_path(iface) if iconpath: icon = load_icon(iconpath, ICON_SIZE, ICON_SIZE) @@ -285,7 +286,7 @@ class InterfaceBrowser: # Try to insert new icon into the cache # If it fails, we'll be left with None in the cached_icon so # we don't try again. - iconpath = iface_cache.get_icon_path(iface) + iconpath = self.policy.config.iface_cache.get_icon_path(iface) if iconpath: self.cached_icon[iface.uri] = load_icon(iconpath, ICON_SIZE, ICON_SIZE) self.build_tree() @@ -308,13 +309,14 @@ class InterfaceBrowser: return None def build_tree(self): + iface_cache = self.policy.config.iface_cache + if self.original_implementation is None: self.set_original_implementations() done = {} # Detect cycles self.model.clear() - parent = None commands = self.policy.solver.selections.commands def add_node(parent, iface, command): # (command is the index into commands, if any) @@ -322,10 +324,18 @@ class InterfaceBrowser: return done[iface] = True + main_feed = iface_cache.get_feed(iface.uri) + if main_feed: + name = main_feed.get_name() + summary = main_feed.summary + else: + name = iface.get_name() + summary = None + iter = self.model.append(parent) self.model[iter][InterfaceBrowser.INTERFACE] = iface - self.model[iter][InterfaceBrowser.INTERFACE_NAME] = iface.get_name() - self.model[iter][InterfaceBrowser.SUMMARY] = iface.summary + self.model[iter][InterfaceBrowser.INTERFACE_NAME] = name + self.model[iter][InterfaceBrowser.SUMMARY] = summary self.model[iter][InterfaceBrowser.ICON] = self.get_icon(iface) or self.default_icon sel = self.policy.solver.selections.selections.get(iface.uri, None) @@ -364,7 +374,7 @@ class InterfaceBrowser: # Nothing could be selected, or no command requested add_node(None, self.root, None) self.tree_view.expand_all() - + def show_popup_menu(self, iface, bev): import bugs @@ -402,7 +412,7 @@ class InterfaceBrowser: item.set_sensitive(False) menu.popup(None, None, None, bev.button, bev.time) - + def compile(self, interface, autocompile = False): import compile def on_success(): @@ -410,7 +420,7 @@ class InterfaceBrowser: info(_("0compile command completed successfully. Reloading interface details.")) reader.update_from_cache(interface) for feed in interface.extra_feeds: - iface_cache.get_feed(feed.uri, force = True) + self.policy.config.iface_cache.get_feed(feed.uri, force = True) self.policy.recalculate() compile.compile(on_success, interface.uri, autocompile = autocompile) @@ -431,7 +441,7 @@ class InterfaceBrowser: if dl.hint not in hints: hints[dl.hint] = [] hints[dl.hint].append(dl) - + selections = self.policy.solver.selections def walk(it): @@ -442,9 +452,9 @@ class InterfaceBrowser: for row in walk(self.model.get_iter_root()): iface = row[InterfaceBrowser.INTERFACE] - + # Is this interface the download's hint? - downloads = hints.get(iface, []) # The interface itself + downloads = hints.get(iface, []) # The interface itself downloads += hints.get(iface.uri, []) # The main feed for feed in self.policy.usable_feeds(iface): downloads += hints.get(feed.uri, []) # Other feeds diff --git a/zeroinstall/0launch-gui/main.py b/zeroinstall/0launch-gui/main.py index e1e0c91..5fb679f 100644 --- a/zeroinstall/0launch-gui/main.py +++ b/zeroinstall/0launch-gui/main.py @@ -5,9 +5,8 @@ import os, sys from optparse import OptionParser -from zeroinstall.injector import model, arch -from zeroinstall.injector.policy import Policy -from zeroinstall.injector.iface_cache import iface_cache +from zeroinstall.injector import requirements +from zeroinstall.injector.policy import Policy, load_config from zeroinstall.support import tasks _recalculate = tasks.Blocker('recalculate') @@ -48,11 +47,6 @@ def run_gui(args): else: logger.setLevel(logging.DEBUG) - 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))) - import gui if options.version: @@ -70,20 +64,18 @@ def run_gui(args): print >>sys.stderr, "Failed to connect to display. Aborting." sys.exit(1) - if not hasattr(gtk, 'combo_box_new_text'): - import combo_compat - handler = gui.GUIHandler() + config = load_config(handler) + + if options.with_store: + from zeroinstall import zerostore + for x in options.with_store: + config.stores.stores.append(zerostore.Store(os.path.abspath(x))) + if len(args) < 1: import preferences - # Once we separate configuration from Policy, this hack can go away - class DummyPolicy(Policy): - def recalculate(fetch_stale_interfaces = True): - pass - def solve_with_downloads(force = False): - pass - box = preferences.show_preferences(DummyPolicy('http://localhost/dummy', handler)) + box = preferences.show_preferences(config) box.connect('destroy', gtk.main_quit) gtk.main() sys.exit(0) @@ -96,17 +88,13 @@ def run_gui(args): import mainwindow, dialog - restrictions = [] - if options.before or options.not_before: - restrictions.append(model.VersionRangeRestriction(model.parse_version(options.before), - model.parse_version(options.not_before))) + r = requirements.Requirements(interface_uri) + r.parse_options(options) widgets = dialog.Template('main') - policy = Policy(interface_uri, handler, src = bool(options.source), command = options.command) - policy.target_arch = arch.get_architecture(options.os, options.cpu) - root_iface = iface_cache.get_interface(interface_uri) - policy.solver.extra_restrictions[root_iface] = restrictions + policy = Policy(config = config, requirements = r) + root_iface = config.iface_cache.get_interface(interface_uri) policy.solver.record_details = True window = mainwindow.MainWindow(policy, widgets, download_only = bool(options.download_only), select_only = bool(options.select_only)) @@ -115,7 +103,7 @@ def run_gui(args): if options.message: window.set_message(options.message) - root = iface_cache.get_interface(policy.root) + root = config.iface_cache.get_interface(policy.root) window.browser.set_root(root) window.window.connect('destroy', lambda w: handler.abort_all_downloads()) diff --git a/zeroinstall/0launch-gui/mainwindow.py b/zeroinstall/0launch-gui/mainwindow.py index 5112203..359bbfc 100644 --- a/zeroinstall/0launch-gui/mainwindow.py +++ b/zeroinstall/0launch-gui/mainwindow.py @@ -78,7 +78,7 @@ class MainWindow: gui_help.display() elif resp == SHOW_PREFERENCES: import preferences - preferences.show_preferences(policy) + preferences.show_preferences(policy.config, notify_cb = lambda: policy.solve_with_downloads()) self.window.connect('response', response) self.window.realize() # Make busy pointer work, even with --systray diff --git a/zeroinstall/0launch-gui/preferences.py b/zeroinstall/0launch-gui/preferences.py index c8df5b8..6e9ec53 100644 --- a/zeroinstall/0launch-gui/preferences.py +++ b/zeroinstall/0launch-gui/preferences.py @@ -11,46 +11,46 @@ from freshness import freshness_levels, Freshness SHOW_CACHE = 0 class Preferences: - def __init__(self, policy): + def __init__(self, config, notify_cb = None): + if notify_cb is None: + notify_cb = lambda: None + widgets = Template('preferences_box') self.window = widgets.get_widget('preferences_box') self.window.connect('destroy', lambda w: self.destroyed()) network = widgets.get_widget('network_use') - network.set_active(list(network_levels).index(policy.network_use)) + network.set_active(list(network_levels).index(config.network_use)) def set_network_use(combo): - policy.network_use = network_levels[network.get_active()] - policy.save_config() - policy.solve_with_downloads() + config.network_use = network_levels[network.get_active()] + config.save_globals() + notify_cb() network.connect('changed', set_network_use) # Freshness times = [x.time for x in freshness_levels] - if policy.freshness not in times: - freshness_levels.append(Freshness(policy.freshness, - '%d seconds' % policy.freshness)) - times.append(policy.freshness) - eb = gtk.EventBox() # For the tooltip + if config.freshness not in times: + freshness_levels.append(Freshness(config.freshness, + '%d seconds' % config.freshness)) + times.append(config.freshness) freshness = widgets.get_widget('freshness') for level in freshness_levels: freshness.append_text(str(level)) - freshness.set_active(times.index(policy.freshness)) + freshness.set_active(times.index(config.freshness)) def set_freshness(combo, freshness = freshness): # (pygtk bug?) - policy.freshness = freshness_levels[freshness.get_active()].time - policy.save_config() - import main - main.recalculate() + config.freshness = freshness_levels[freshness.get_active()].time + config.save_globals() + notify_cb() freshness.connect('changed', set_freshness) stable_toggle = widgets.get_widget('help_test') - stable_toggle.set_active(policy.help_with_testing) + stable_toggle.set_active(config.help_with_testing) def toggle_stability(toggle): - policy.help_with_testing = toggle.get_active() - policy.save_config() - import main - main.recalculate() + config.help_with_testing = toggle.get_active() + config.save_globals() + notify_cb() stable_toggle.connect('toggled', toggle_stability) # Keys @@ -147,11 +147,11 @@ class KeyList: tv.connect('button-press-event', trusted_keys_button_press) preferences_box = None -def show_preferences(policy): +def show_preferences(config, notify_cb = None): global preferences_box if preferences_box: preferences_box.destroy() - preferences_box = Preferences(policy) + preferences_box = Preferences(config, notify_cb) preferences_box.window.show() return preferences_box.window diff --git a/zeroinstall/0launch-gui/properties.py b/zeroinstall/0launch-gui/properties.py index b1872a4..2f93af0 100644 --- a/zeroinstall/0launch-gui/properties.py +++ b/zeroinstall/0launch-gui/properties.py @@ -5,7 +5,6 @@ import zeroinstall import os from zeroinstall.support import tasks from zeroinstall.injector.model import Interface, Feed, stable, testing, developer, stability_levels -from zeroinstall.injector.iface_cache import iface_cache from zeroinstall.injector import writer, namespaces, gpg from zeroinstall.gtkui import help_box @@ -30,11 +29,11 @@ def format_para(para): return ' '.join(lines) def have_source_for(policy, interface): + iface_cache = policy.config.iface_cache # Note: we don't want to actually fetch the source interfaces at # this point, so we check whether: # - We have a feed of type 'src' (not fetched), or # - We have a source implementation in a regular feed - have_src = False for f in iface_cache.get_feed_imports(interface): if f.machine == 'src': return True @@ -73,10 +72,10 @@ class Description: try: from locale import nl_langinfo, D_T_FMT return time.strftime(nl_langinfo(D_T_FMT), time.localtime(secs)) - except ImportError, ValueError: + except (ImportError, ValueError): return time.ctime(secs) - def set_details(self, feed): + def set_details(self, iface_cache, feed): buffer = self.buffer heading_style = self.heading_style @@ -206,6 +205,8 @@ class Feeds: sel.select_path((0,)) def build_model(self): + iface_cache = self.policy.config.iface_cache + usable_feeds = frozenset(self.policy.usable_feeds(self.interface)) unusable_feeds = frozenset(iface_cache.get_feed_imports(self.interface)) - usable_feeds @@ -218,6 +219,8 @@ class Feeds: return out def sel_changed(self, sel): + iface_cache = self.policy.config.iface_cache + model, miter = sel.get_selected() if not miter: return # build in progress feed_url = model[miter][Feeds.URI] @@ -229,9 +232,9 @@ class Feeds: enable_remove = True self.remove_feed_button.set_sensitive( enable_remove ) try: - self.description.set_details(iface_cache.get_feed(feed_url)) + self.description.set_details(iface_cache, iface_cache.get_feed(feed_url)) except zeroinstall.SafeException, ex: - self.description.set_details(ex) + self.description.set_details(iface_cache, ex) def updated(self): new_lines = self.build_model() @@ -341,6 +344,8 @@ class Properties: @tasks.async def add_remote_feed(policy, parent, interface): try: + iface_cache = policy.config.iface_cache + d = gtk.MessageDialog(parent, 0, gtk.MESSAGE_QUESTION, gtk.BUTTONS_CANCEL, _('Enter the URL of the new source of implementations of this interface:')) d.add_button(gtk.STOCK_ADD, gtk.RESPONSE_OK) diff --git a/zeroinstall/cmd/__init__.py b/zeroinstall/cmd/__init__.py index 3dce6b8..c47c2df 100644 --- a/zeroinstall/cmd/__init__.py +++ b/zeroinstall/cmd/__init__.py @@ -10,8 +10,7 @@ import os, sys from optparse import OptionParser import logging -from zeroinstall import SafeException, NeedDownload -from zeroinstall.injector import model, autopolicy, selections +from zeroinstall import SafeException valid_commands = ['select', 'download', 'run', 'update', 'config', 'import', 'list', 'add-feed', 'remove-feed'] @@ -66,9 +65,10 @@ def main(command_args, config = None): # The first non-option argument is the command name (or "help" if none is found). command = None - for arg in command_args: + for i, arg in enumerate(command_args): if not arg.startswith('-'): command = arg + del command_args[i] break elif arg == '--': break @@ -94,7 +94,6 @@ def main(command_args, config = None): cmd.add_options(parser) (options, args) = parser.parse_args(command_args) - assert args[0] == command if options.verbose: logger = logging.getLogger() @@ -105,15 +104,13 @@ def main(command_args, config = None): 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: config.stores.stores.append(zerostore.Store(os.path.abspath(x))) logging.info(_("Stores search path is now %s"), config.stores.stores) - h.dry_run = bool(options.dry_run) + config.handler.dry_run = bool(options.dry_run) cmd.handle(config, options, args) except UsageError: diff --git a/zeroinstall/cmd/add_feed.py b/zeroinstall/cmd/add_feed.py index 3d086ea..d417715 100644 --- a/zeroinstall/cmd/add_feed.py +++ b/zeroinstall/cmd/add_feed.py @@ -5,12 +5,9 @@ The B{0install add-feed} command-line interface. # Copyright (C) 2011, Thomas Leonard # See the README file for details, or visit http://0install.net. -import os, sys -import logging - -from zeroinstall import cmd, SafeException, _ +from zeroinstall import SafeException, _ from zeroinstall.cmd import UsageError -from zeroinstall.injector import model, writer, handler +from zeroinstall.injector import model, writer from zeroinstall.injector.policy import Policy syntax = "NEW-FEED" @@ -21,12 +18,6 @@ def add_options(parser): def handle(config, options, args, add_ok = True, remove_ok = False): if not args: raise UsageError() - if os.isatty(1): - h = handler.ConsoleHandler() - else: - h = handler.Handler() - h.dry_run = bool(options.dry_run) - def find_feed_import(iface, feed_url): for f in iface.extra_feeds: if f.uri == feed_url: @@ -36,15 +27,15 @@ def handle(config, options, args, add_ok = True, remove_ok = False): for x in args: print _("Feed '%s':") % x + '\n' x = model.canonical_iface_uri(x) - policy = Policy(x, h) + policy = Policy(x, config = config) if options.offline: - policy.network_use = model.network_offline + config.network_use = model.network_offline feed = config.iface_cache.get_feed(x) if policy.network_use != model.network_offline and policy.is_stale(feed): blocker = policy.fetcher.download_and_import_feed(x, config.iface_cache) print _("Downloading feed; please wait...") - h.wait_for_blocker(blocker) + config.handler.wait_for_blocker(blocker) print _("Done") candidate_interfaces = policy.get_feed_targets(x) diff --git a/zeroinstall/cmd/config.py b/zeroinstall/cmd/config.py index 3a32adc..a30f7b4 100644 --- a/zeroinstall/cmd/config.py +++ b/zeroinstall/cmd/config.py @@ -5,13 +5,10 @@ The B{0install config} command-line interface. # Copyright (C) 2011, Thomas Leonard # See the README file for details, or visit http://0install.net. -import os, sys -import logging -import ConfigParser +import os -from zeroinstall import cmd, SafeException, _ -from zeroinstall.support import basedir -from zeroinstall.injector import policy, namespaces, model +from zeroinstall import SafeException, _ +from zeroinstall.injector import model from zeroinstall.cmd import UsageError syntax = "[NAME [VALUE]]" @@ -31,17 +28,22 @@ class String: class TimeInterval: @staticmethod def format(value): + def s(v): + if int(v) == v: + return str(int(v)) + else: + return str(v) value = float(value) if value < 60: - return str(value) + "s" + return s(value) + "s" value /= 60 if value < 60: - return str(value) + "m" + return s(value) + "m" value /= 60 if value < 24: - return str(value) + "h" + return s(value) + "h" value /= 24 - return str(value) + "d" + return s(value) + "d" @staticmethod def parse(value): diff --git a/zeroinstall/cmd/download.py b/zeroinstall/cmd/download.py index 6aa8454..0a880a9 100644 --- a/zeroinstall/cmd/download.py +++ b/zeroinstall/cmd/download.py @@ -5,13 +5,11 @@ The B{0install download} 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 +import sys -from zeroinstall import cmd, SafeException, _ +from zeroinstall import _ from zeroinstall.cmd import UsageError, select -from zeroinstall.injector import model, autopolicy, selections, handler +from zeroinstall.injector import model syntax = "URI" diff --git a/zeroinstall/cmd/import.py b/zeroinstall/cmd/import.py index 9d2758f..be11131 100644 --- a/zeroinstall/cmd/import.py +++ b/zeroinstall/cmd/import.py @@ -5,12 +5,12 @@ The B{0install import} command-line interface. # Copyright (C) 2011, Thomas Leonard # See the README file for details, or visit http://0install.net. -import os, sys +import os import logging -from zeroinstall import cmd, SafeException, _ +from zeroinstall import SafeException, _ from zeroinstall.cmd import UsageError -from zeroinstall.injector import model, autopolicy, selections, gpg, fetch +from zeroinstall.injector import gpg from zeroinstall.injector.iface_cache import PendingFeed from zeroinstall.support import tasks from xml.dom import minidom @@ -46,8 +46,7 @@ def handle(config, options, args): yield keys_downloaded.finished tasks.check(keys_downloaded.finished) if not config.iface_cache.update_feed_if_trusted(uri, pending.sigs, pending.new_xml): - fetcher = fetch.Fetcher(h) - blocker = h.confirm_keys(pending, fetcher.fetch_key_info) + blocker = h.confirm_keys(pending, config.fetcher.fetch_key_info) if blocker: yield blocker tasks.check(blocker) diff --git a/zeroinstall/cmd/run.py b/zeroinstall/cmd/run.py index 20fc3ec..69d0c69 100644 --- a/zeroinstall/cmd/run.py +++ b/zeroinstall/cmd/run.py @@ -5,13 +5,11 @@ The B{0install run} 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 +import sys -from zeroinstall import cmd, SafeException, _ +from zeroinstall import _ from zeroinstall.cmd import UsageError, select -from zeroinstall.injector import model, autopolicy, selections, handler +from zeroinstall.injector import model syntax = "URI [ARGS]" diff --git a/zeroinstall/cmd/select.py b/zeroinstall/cmd/select.py index 3b51f0a..ff1d7b4 100644 --- a/zeroinstall/cmd/select.py +++ b/zeroinstall/cmd/select.py @@ -5,13 +5,12 @@ 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 import _ from zeroinstall.cmd import UsageError -from zeroinstall.injector import model, selections, handler +from zeroinstall.injector import model, selections, requirements from zeroinstall.injector.policy import Policy syntax = "URI" @@ -27,6 +26,7 @@ def add_generic_select_options(parser): 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("", "--systray", help=_("download in the background"), action='store_true') def add_options(parser): """Options for 'select' and 'download' (but not 'run')""" @@ -43,44 +43,24 @@ def get_selections(config, options, iface_uri, select_only, download_only, test_ @return: the selected versions, or None if the user cancels @rtype: L{selections.Selections} | None """ + if options.offline: + config.network_use = model.network_offline + # Try to load it as a feed. If it is a feed, it'll get cached. If not, it's a # selections document and we return immediately. maybe_selections = config.iface_cache.get_feed(iface_uri, selections_ok = True) if isinstance(maybe_selections, selections.Selections): if not select_only: - from zeroinstall.injector import fetch - fetcher = fetch.Fetcher(h) - blocker = maybe_selections.download_missing(config.iface_cache, fetcher) + blocker = maybe_selections.download_missing(config) if blocker: logging.info(_("Waiting for selected implementations to be downloaded...")) - h.wait_for_blocker(blocker) - _download_missing_selections(h, maybe_selections) + config.handler.wait_for_blocker(blocker) return maybe_selections - root_iface = config.iface_cache.get_interface(iface_uri) - - command_name = options.command - if command_name is None: - command_name = 'run' - elif command_name == '': - command_name = None - policy = Policy(iface_uri, - handler = None, # (use config instead) - src = options.source, - command = command_name, - config = config) + r = requirements.Requirements(iface_uri) + r.parse_options(options) - 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 + policy = Policy(config = config, requirements = r) # Note that need_download() triggers a solve if options.refresh or options.gui: @@ -121,41 +101,23 @@ def get_selections(config, options, iface_uri, select_only, download_only, test_ logging.info(_("Switching to GUI mode... (use --console to disable)")) if options.gui: - gui_args = [] + gui_args = policy.requirements.get_as_options() 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) + if options.systray: + gui_args.append('--systray') from zeroinstall import helpers sels = helpers.get_selections_gui(iface_uri, gui_args, test_callback) @@ -167,7 +129,7 @@ def get_selections(config, options, iface_uri, select_only, download_only, test_ 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) + config.handler.wait_for_blocker(downloaded) sels = selections.Selections(policy) return sels diff --git a/zeroinstall/cmd/update.py b/zeroinstall/cmd/update.py index a0a3eaf..7033317 100644 --- a/zeroinstall/cmd/update.py +++ b/zeroinstall/cmd/update.py @@ -5,11 +5,9 @@ The B{0install download} 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 +import sys -from zeroinstall import cmd, SafeException, _ +from zeroinstall import SafeException, _ from zeroinstall.injector import model from zeroinstall.cmd import UsageError, select @@ -35,7 +33,7 @@ def handle(config, options, args): try: old_sels = select.get_selections(config, options, iface_uri, select_only = True, download_only = False, test_callback = None) - except SafeException, ex: + except SafeException: old_selections = {} else: if old_sels is None: @@ -44,6 +42,7 @@ def handle(config, options, args): old_selections = old_sels.selections # Download in online mode to get the new values + config.network_use = model.network_full options.offline = False options.gui = old_gui options.refresh = True diff --git a/zeroinstall/gtkui/icon.py b/zeroinstall/gtkui/icon.py index fa185ae..f954e76 100644 --- a/zeroinstall/gtkui/icon.py +++ b/zeroinstall/gtkui/icon.py @@ -4,7 +4,7 @@ from zeroinstall import _ import gtk -from logging import warn, debug +from logging import warn import math def load_icon(icon_path, icon_width=None, icon_height=None): diff --git a/zeroinstall/helpers.py b/zeroinstall/helpers.py index c3814ba..a1cca77 100644 --- a/zeroinstall/helpers.py +++ b/zeroinstall/helpers.py @@ -108,9 +108,10 @@ def ensure_cached(uri, command = 'run'): @return: the selected implementations, or None if the user cancelled @rtype: L{zeroinstall.injector.selections.Selections} """ - from zeroinstall.injector import autopolicy, selections + from zeroinstall.injector import policy, selections, handler - p = autopolicy.AutoPolicy(uri, download_only = True, command = command) + config = policy.load_config(handler.Handler()) + p = policy.Policy(uri, command = command, config = config) p.freshness = 0 # Don't check for updates if p.need_download() or not p.ready: diff --git a/zeroinstall/injector/autopolicy.py b/zeroinstall/injector/autopolicy.py index 5529f7f..be4f341 100644 --- a/zeroinstall/injector/autopolicy.py +++ b/zeroinstall/injector/autopolicy.py @@ -14,34 +14,15 @@ is also the policy used to run the injector's GUI. from zeroinstall import _ from logging import info -from zeroinstall.injector import policy, run +from zeroinstall.injector import policy from zeroinstall.injector.handler import Handler class AutoPolicy(policy.Policy): - __slots__ = ['download_only'] - def __init__(self, interface_uri, download_only = False, dry_run = False, src = False, handler = None, command = 'run'): """@param handler: (new in 0.30) handler to use, or None to create a L{Handler}""" + assert download_only is False handler = handler or Handler() if dry_run: info(_("Note: dry_run is deprecated. Pass it to the handler instead!")) handler.dry_run = True policy.Policy.__init__(self, interface_uri, handler, src = src, command = command) - self.download_only = download_only - - def execute(self, prog_args, main = None, wrapper = None): - """@deprecated: use L{solve_and_download_impls} and L{run.execute}""" - downloaded = self.download_uncached_implementations() - if downloaded: - self.handler.wait_for_blocker(downloaded) - if not self.download_only: - run.execute(self, prog_args, dry_run = self.handler.dry_run, main = main, wrapper = wrapper) - else: - info(_("Downloads done (download-only mode)")) - - def download_and_execute(self, prog_args, refresh = False, main = None): - """@deprecated: use L{solve_and_download_impls} instead""" - downloaded = self.solve_and_download_impls(refresh) - if downloaded: - self.handler.wait_for_blocker(downloaded) - self.execute(prog_args, main = main) diff --git a/zeroinstall/injector/background.py b/zeroinstall/injector/background.py index 248a184..c67a3cb 100644 --- a/zeroinstall/injector/background.py +++ b/zeroinstall/injector/background.py @@ -13,14 +13,15 @@ from zeroinstall import _ import sys, os from logging import info, warn from zeroinstall.support import tasks -from zeroinstall.injector.iface_cache import iface_cache from zeroinstall.injector import handler def _escape_xml(s): return s.replace('&', '&').replace('<', '<') def _exec_gui(uri, *args): - os.execvp('0launch', ['0launch', '--download-only', '--gui'] + list(args) + [uri]) + parent_dir = os.path.dirname(os.path.dirname(__file__)) + child_args = [sys.executable, '-u', os.path.join(parent_dir, '0launch-gui/0launch-gui')] + list(args) + [uri] + os.execvp(sys.executable, child_args) class _NetworkState: NM_STATE_UNKNOWN = 0 @@ -88,7 +89,7 @@ class BackgroundHandler(handler.Handler): def confirm_import_feed(self, pending, valid_sigs): """Run the GUI if we need to confirm any keys.""" info(_("Can't update feed; signature not yet trusted. Running GUI...")) - _exec_gui(self.root, '--refresh', '--download-only', '--systray') + _exec_gui(self.root, '--refresh', '--systray') def report_error(self, exception, tb = None): from zeroinstall.injector import download @@ -160,10 +161,14 @@ def _detach(): return False -def _check_for_updates(policy, verbose): - root_iface = iface_cache.get_interface(policy.root).get_name() +def _check_for_updates(old_policy, verbose): + from zeroinstall.injector.policy import load_config, Policy - policy.handler = BackgroundHandler(root_iface, policy.root) + iface_cache = old_policy.config.iface_cache + root_iface = iface_cache.get_interface(old_policy.root).get_name() + + background_config = load_config(BackgroundHandler(root_iface, old_policy.root)) + policy = Policy(config = background_config, requirements = old_policy.requirements) info(_("Checking for updates to '%s' in a background process"), root_iface) if verbose: @@ -198,7 +203,7 @@ def _check_for_updates(policy, verbose): policy.handler.notify("Zero Install", _("Updates ready to download for '%s'.") % root_iface, timeout = 1) - _exec_gui(policy.root, '--refresh', '--download-only', '--systray') + _exec_gui(policy.root, '--refresh', '--systray') sys.exit(1) notification_closed = tasks.Blocker("wait for notification response") @@ -227,12 +232,13 @@ def spawn_background_update(policy, verbose): @type policy: L{policy.Policy} @param verbose: whether to notify the user about minor events @type verbose: bool""" + iface_cache = policy.config.iface_cache # Mark all feeds as being updated. Do this before forking, so that if someone is # running lots of 0launch commands in series on the same program we don't start # huge numbers of processes. - for x in policy.implementation: - iface_cache.mark_as_checking(x.uri) # Main feed - for f in policy.usable_feeds(x): + for uri in policy.solver.selections.selections: + iface_cache.mark_as_checking(uri) # Main feed + for f in policy.usable_feeds(iface_cache.get_interface(uri)): iface_cache.mark_as_checking(f.uri) # Extra feeds if _detach(): diff --git a/zeroinstall/injector/cli.py b/zeroinstall/injector/cli.py index ba573b0..420ab43 100644 --- a/zeroinstall/injector/cli.py +++ b/zeroinstall/injector/cli.py @@ -11,7 +11,7 @@ from optparse import OptionParser import logging from zeroinstall import SafeException, NeedDownload -from zeroinstall.injector import model, policy, autopolicy, selections +from zeroinstall.injector import handler, policy from zeroinstall.cmd import UsageError #def program_log(msg): os.access('MARK: 0launch: ' + msg, os.F_OK) @@ -19,7 +19,7 @@ from zeroinstall.cmd import UsageError #__main__.__builtins__.program_log = program_log #program_log('0launch ' + ' '.join((sys.argv[1:]))) -def main(command_args): +def main(command_args, config = None): """Act as if 0launch was run with the given arguments. @arg command_args: array of arguments (e.g. C{sys.argv[1:]}) @type command_args: [str] @@ -80,7 +80,14 @@ def main(command_args): if options.select_only or options.show: options.download_only = True - config = policy.load_config() + if os.isatty(1): + h = handler.ConsoleHandler() + else: + h = handler.Handler() + h.dry_run = bool(options.dry_run) + + if config is None: + config = policy.load_config(h) if options.with_store: from zeroinstall import zerostore @@ -94,7 +101,7 @@ def main(command_args): try: if options.list: from zeroinstall.cmd import list - list.handle(options, args) + list.handle(config, options, args) elif options.version: import zeroinstall print "0launch (zero-install) " + zeroinstall.version @@ -107,18 +114,18 @@ def main(command_args): elif getattr(options, 'import'): # (import is a keyword) cmd = __import__('zeroinstall.cmd.import', globals(), locals(), ["import"], 0) - cmd.handle(options, args) + cmd.handle(config, options, args) elif options.feed: from zeroinstall.cmd import add_feed - add_feed.handle(options, args, add_ok = True, remove_ok = True) + add_feed.handle(config, options, args, add_ok = True, remove_ok = True) elif options.select_only: from zeroinstall.cmd import select if not options.show: options.quiet = True - select.handle(options, args) + select.handle(config, options, args) elif options.download_only or options.xml or options.show: from zeroinstall.cmd import download - download.handle(options, args) + download.handle(config, options, args) else: if len(args) < 1: if options.gui: @@ -128,7 +135,7 @@ def main(command_args): raise UsageError() else: from zeroinstall.cmd import run - run.handle(options, args) + run.handle(config, options, args) except NeedDownload, ex: # This only happens for dry runs print ex diff --git a/zeroinstall/injector/distro.py b/zeroinstall/injector/distro.py index 9f7cc66..63abfe2 100644 --- a/zeroinstall/injector/distro.py +++ b/zeroinstall/injector/distro.py @@ -7,10 +7,10 @@ Integration with native distribution package managers. # See the README file for details, or visit http://0install.net. from zeroinstall import _ -import os, platform, re, glob, subprocess, sys +import os, platform, re, subprocess, sys from logging import warn, info from zeroinstall.injector import namespaces, model, arch -from zeroinstall.support import basedir, tasks +from zeroinstall.support import basedir _dotted_ints = '[0-9]+(?:\.[0-9]+)*' @@ -65,8 +65,6 @@ class Cache(object): self.cache = cache = {} stream = file(os.path.join(self.cache_dir, self.cache_leaf)) try: - meta = {} - cached_format = False for line in stream: line = line.strip() if not line: diff --git a/zeroinstall/injector/fetch.py b/zeroinstall/injector/fetch.py index b816feb..7eb754d 100644 --- a/zeroinstall/injector/fetch.py +++ b/zeroinstall/injector/fetch.py @@ -109,7 +109,6 @@ class Fetcher(object): # Maybe we're taking this metaphor too far? # Start downloading all the ingredients. - downloads = {} # Downloads that are not yet successful streams = {} # Streams collected from successful downloads # Start a download for each ingredient @@ -306,8 +305,8 @@ class Fetcher(object): try: return self.key_info[fingerprint] except KeyError: - self.key_info[fingerprint] = info = KeyInfoFetcher(self.key_info_server, fingerprint) - return info + self.key_info[fingerprint] = key_info = KeyInfoFetcher(self.key_info_server, fingerprint) + return key_info def download_impl(self, impl, retrieval_method, stores, force = False): """Download an implementation. @@ -362,7 +361,6 @@ class Fetcher(object): def _add_to_cache(self, required_digest, stores, retrieval_method, stream): assert isinstance(retrieval_method, DownloadSource) - url = retrieval_method.url stores.add_archive_to_cache(required_digest, stream, retrieval_method.url, retrieval_method.extract, type = retrieval_method.type, start_offset = retrieval_method.start_offset or 0) diff --git a/zeroinstall/injector/handler.py b/zeroinstall/injector/handler.py index f8d575a..278abf7 100644 --- a/zeroinstall/injector/handler.py +++ b/zeroinstall/injector/handler.py @@ -88,7 +88,7 @@ class Handler(object): def quitter(): yield blocker self._loop.quit() - quit = tasks.Task(quitter(), "quitter") + tasks.Task(quitter(), "quitter") assert self._loop is None # Avoid recursion self._loop = gobject.MainLoop(gobject.main_context_default()) @@ -239,9 +239,9 @@ class Handler(object): if infos: if len(valid_sigs) > 1: print "%s: " % kf.fingerprint - for info in infos: - print >>sys.stderr, "-", text(info) - shown.add(info) + for key_info in infos: + print >>sys.stderr, "-", text(key_info) + shown.add(key_info) if kf.blocker: key_info_fetchers.append(kf) if key_info_fetchers: diff --git a/zeroinstall/injector/iface_cache.py b/zeroinstall/injector/iface_cache.py index b519bc8..d38fd62 100644 --- a/zeroinstall/injector/iface_cache.py +++ b/zeroinstall/injector/iface_cache.py @@ -37,7 +37,7 @@ from zeroinstall.support import basedir from zeroinstall.injector import reader, model from zeroinstall.injector.namespaces import config_site, config_prog from zeroinstall.injector.model import Interface, escape, unescape -from zeroinstall import zerostore, SafeException +from zeroinstall import SafeException def _pretty_time(t): assert isinstance(t, (int, long)), t @@ -193,9 +193,7 @@ class IfaceCache(object): @property def stores(self): - """deprecated""" from zeroinstall.injector import policy - raise Exception("iface_cache.stores") return policy.get_deprecated_singleton_config().stores @property @@ -372,7 +370,7 @@ class IfaceCache(object): debug(_("Initialising new interface object for %s"), uri) self._interfaces[uri] = Interface(uri) - reader.update_from_cache(self._interfaces[uri]) + reader.update_from_cache(self._interfaces[uri], iface_cache = self) return self._interfaces[uri] def list_all_interfaces(self): @@ -499,4 +497,4 @@ class IfaceCache(object): impls += feed.implementations.values() return impls -#iface_cache = IfaceCache() +iface_cache = IfaceCache() diff --git a/zeroinstall/injector/model.py b/zeroinstall/injector/model.py index e096b58..20c9d28 100644 --- a/zeroinstall/injector/model.py +++ b/zeroinstall/injector/model.py @@ -635,9 +635,10 @@ class Interface(object): self.stability_policy = None def get_name(self): - #feed = self._main_feed - #if feed: - # return feed.get_name() + from zeroinstall.injector.iface_cache import iface_cache + feed = iface_cache.get_feed(self.uri) + if feed: + return feed.get_name() return '(' + os.path.basename(self.uri) + ')' def __repr__(self): @@ -686,7 +687,7 @@ def _get_long(elem, attr_name): if val is not None: try: val = long(val) - except ValueError, ex: + except ValueError: raise SafeException(_("Invalid value for integer attribute '%(attribute_name)s': %(value)s") % {'attribute_name': attr_name, 'value': val}) return val @@ -878,7 +879,6 @@ class ZeroInstallFeed(object): raise InvalidInterface(_("Missing version attribute")) impl.version = parse_version(version) - item_main = commands.get('run', None) impl.commands = commands impl.released = item_attrs.get('released', None) diff --git a/zeroinstall/injector/policy.py b/zeroinstall/injector/policy.py index fcf5252..e44751a 100644 --- a/zeroinstall/injector/policy.py +++ b/zeroinstall/injector/policy.py @@ -14,8 +14,9 @@ from logging import info, debug, warn import ConfigParser from zeroinstall import zerostore, SafeException -from zeroinstall.injector import arch +from zeroinstall.injector import arch, model from zeroinstall.injector.model import Interface, Implementation, network_levels, network_offline, DistributionImplementation, network_full +from zeroinstall.injector.handler import Handler from zeroinstall.injector.namespaces import config_site, config_prog from zeroinstall.support import tasks, basedir @@ -30,6 +31,7 @@ class Config(object): __slots__ = ['help_with_testing', 'freshness', 'network_use', '_fetcher', '_stores', '_iface_cache', 'handler'] def __init__(self, handler): + assert handler is not None self.help_with_testing = False self.freshness = 60 * 60 * 24 * 30 self.network_use = network_full @@ -46,7 +48,8 @@ class Config(object): def iface_cache(self): if not self._iface_cache: from zeroinstall.injector import iface_cache - self._iface_cache = iface_cache.IfaceCache() + self._iface_cache = iface_cache.iface_cache + #self._iface_cache = iface_cache.IfaceCache() return self._iface_cache @property @@ -90,6 +93,8 @@ def load_config(handler): config.network_use = parser.get('global', 'network_use') config.freshness = int(parser.get('global', 'freshness')) + assert config.network_use in network_levels, config.network_use + return config class Policy(object): @@ -112,14 +117,12 @@ class Policy(object): @ivar network_use: one of the model.network_* values @ivar freshness: seconds allowed since last update @type freshness: int - @ivar src: whether we are looking for source code - @type src: bool @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', 'command', 'config', - '_warned_offline', - 'target_arch', 'src', 'stale_feeds', 'solver', '_fetcher'] + __slots__ = ['root', 'watchers', 'requirements', 'config', '_warned_offline', + 'command', 'target_arch', + 'stale_feeds', 'solver'] help_with_testing = property(lambda self: self.config.help_with_testing, lambda self, value: setattr(self.config, 'help_with_testing', bool(value))) @@ -134,30 +137,41 @@ class Policy(object): ready = property(lambda self: self.solver.ready) - def __init__(self, root, handler = None, src = False, command = -1, config = None): + # (used by 0test) + handler = property(lambda self: self.config.handler, + lambda self, value: setattr(self.config, 'handler', value)) + + + def __init__(self, root = None, handler = None, src = None, command = -1, config = None, requirements = 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 + @param requirements: Details about the program we want to run + @type requirements: L{requirements.Requirements} @param config: The configuration settings to use, or None to load from disk. @type config: L{ConfigParser.ConfigParser} + Note: all other arguments are deprecated (since 0launch 0.52) """ self.watchers = [] - self.src = src # Root impl must be a "src" machine type + if requirements is None: + from zeroinstall.injector.requirements import Requirements + requirements = Requirements(root) + requirements.source = bool(src) # Root impl must be a "src" machine type + if command == -1: + if src: + command = 'compile' + else: + command = 'run' + requirements.command = command + self.target_arch = arch.get_host_architecture() + else: + assert root == src == None + assert command == -1 + self.target_arch = arch.get_architecture(requirements.os, requirements.cpu) + self.requirements = requirements + self.stale_feeds = set() - if command == -1: - if src: - command = 'compile' - else: - command = 'run' - self.command = command if config is None: - self.config = load_config(handler) + self.config = load_config(handler or Handler()) else: assert handler is None, "can't pass a handler and a config" self.config = config @@ -168,30 +182,19 @@ class Policy(object): # If we need to download something but can't because we are offline, # warn the user. But only the first time. self._warned_offline = False - self._fetcher = None debug(_("Supported systems: '%s'"), arch.os_ranks) debug(_("Supported processors: '%s'"), arch.machine_ranks) - assert self.network_use in network_levels, self.network_use - self.set_root(root) - - self.target_arch = arch.get_host_architecture() + if requirements.before or requirements.not_before: + self.solver.extra_restrictions[config.iface_cache.get_interface(requirements.interface_uri)] = [ + model.VersionRangeRestriction(model.parse_version(requirements.before), + model.parse_version(requirements.not_before))] @property def fetcher(self): return self.config.fetcher - @property - def handler(self): - return self.config.handler - - def set_root(self, root): - """Change the root interface URI.""" - assert isinstance(root, (str, unicode)) - self.root = root - for w in self.watchers: w() - def save_config(self): self.config.save_globals() @@ -203,7 +206,7 @@ class Policy(object): self.stale_feeds = set() host_arch = self.target_arch - if self.src: + if self.requirements.source: host_arch = arch.SourceArchitecture(host_arch) self.solver.solve(self.root, host_arch, command_name = self.command) @@ -230,7 +233,7 @@ class Policy(object): """Generator for C{iface.feeds} that are valid for our architecture. @rtype: generator @see: L{arch}""" - if self.src and iface.uri == self.root: + if self.requirements.source and iface.uri == self.root: # Note: when feeds are recursive, we'll need a better test for root here machine_ranks = {'src': 1} else: @@ -296,7 +299,7 @@ class Policy(object): try: return self.implementation[interface] - except KeyError, ex: + except KeyError: raise SafeException(_("No usable implementation found for '%s'.") % interface.uri) def get_cached(self, impl): @@ -320,12 +323,13 @@ class Policy(object): def get_uncached_implementations(self): """List all chosen implementations which aren't yet available locally. @rtype: [(L{model.Interface}, L{model.Implementation})]""" + iface_cache = self.config.iface_cache uncached = [] - for iface in self.solver.selections: - impl = self.solver.selections[iface] + for uri, selection in self.solver.selections.selections.iteritems(): + impl = selection.impl assert impl, self.solver.selections if not self.get_cached(impl): - uncached.append((iface, impl)) + uncached.append((iface_cache.get_interface(uri), impl)) return uncached def refresh_all(self, force = True): @@ -333,23 +337,25 @@ class Policy(object): @param force: Whether to restart existing downloads.""" return self.solve_with_downloads(force = True) - def get_feed_targets(self, feed_iface_uri): - """Return a list of Interfaces for which feed_iface can be a feed. - This is used by B{0launch --feed}. + def get_feed_targets(self, feed): + """Return a list of Interfaces for which feed can be a feed. + This is used by B{0install add-feed}. + @param feed: the feed + @type feed: L{model.ZeroInstallFeed} (or, deprecated, a URL) @rtype: [model.Interface] @raise SafeException: If there are no known feeds.""" - # TODO: what if it isn't cached yet? - feed_iface = self.config.iface_cache.get_interface(feed_iface_uri) - if not feed_iface.feed_for: - if not feed_iface.name: - raise SafeException(_("Can't get feed targets for '%s'; failed to load it.") % - feed_iface_uri) + + if not isinstance(feed, model.ZeroInstallFeed): + # (deprecated) + feed = self.config.iface_cache.get_feed(feed) + if feed is None: + raise SafeException("Feed is not cached and using deprecated API") + + if not feed.feed_for: raise SafeException(_("Missing element in '%s'; " - "it can't be used as a feed for any other interface.") % feed_iface_uri) - feed_targets = feed_iface.feed_for + "it can't be used as a feed for any other interface.") % feed.url) + feed_targets = feed.feed_for debug(_("Feed targets: %s"), feed_targets) - if not feed_iface.name: - warn(_("Warning: unknown interface '%s'") % feed_iface_uri) return [self.config.iface_cache.get_interface(uri) for uri in feed_targets] @tasks.async @@ -364,7 +370,7 @@ class Policy(object): downloads_in_progress = {} # URL -> Download host_arch = self.target_arch - if self.src: + if self.requirements.source: host_arch = arch.SourceArchitecture(host_arch) # There are three cases: @@ -453,7 +459,7 @@ class Policy(object): @return: true if we MUST download something (feeds or implementations) @rtype: bool""" host_arch = self.target_arch - if self.src: + if self.requirements.source: host_arch = arch.SourceArchitecture(host_arch) self.solver.solve(self.root, host_arch, command_name = self.command) for w in self.watchers: w() @@ -497,10 +503,17 @@ class Policy(object): warnings.warn("Policy.get_interface is deprecated!", DeprecationWarning, stacklevel = 2) return self.config.iface_cache.get_interface(uri) + @property + def command(self): + return self.requirements.command + + @property + def root(self): + return self.requirements.interface_uri + _config = None def get_deprecated_singleton_config(): global _config if _config is None: - from zeroinstall.injector import handler - _config = load_config(handler.Handler()) + _config = load_config(Handler()) return _config diff --git a/zeroinstall/injector/reader.py b/zeroinstall/injector/reader.py index e009231..2a1ff05 100644 --- a/zeroinstall/injector/reader.py +++ b/zeroinstall/injector/reader.py @@ -10,7 +10,7 @@ import os from logging import debug, info, warn from zeroinstall.support import basedir -from zeroinstall.injector import qdom, distro +from zeroinstall.injector import qdom from zeroinstall.injector.namespaces import config_site, config_prog, XMLNS_IFACE from zeroinstall.injector.model import Interface, InvalidInterface, ZeroInstallFeed, escape, Feed, stability_levels from zeroinstall.injector import model @@ -161,7 +161,7 @@ def check_readable(feed_url, source): {'uri': feed_url, 'source': source, 'exception': ex}) raise InvalidInterface(_("Error loading feed '%(uri)s':\n\n%(exception)s") % {'uri': feed_url, 'exception': ex}) -def update(interface, source, local = False): +def update(interface, source, local = False, iface_cache = None): """Read in information about an interface. Deprecated. @param interface: the interface object to update @@ -183,9 +183,9 @@ def update(interface, source, local = False): "%(interface_uri)s was requested") % {'feed_url': feed.url, 'interface_uri': interface.uri}) - # Hack. - from zeroinstall.injector import policy - iface_cache = policy.get_deprecated_singleton_config().iface_cache + if iface_cache is None: + from zeroinstall.injector import policy + iface_cache = policy.get_deprecated_singleton_config().iface_cache iface_cache._feeds[unicode(interface.uri)] = feed return feed diff --git a/zeroinstall/injector/requirements.py b/zeroinstall/injector/requirements.py new file mode 100644 index 0000000..c1a0bd7 --- /dev/null +++ b/zeroinstall/injector/requirements.py @@ -0,0 +1,72 @@ +""" +Holds information about what the user asked for (which program, version constraints, etc). +""" + +# Copyright (C) 2011, Thomas Leonard +# See the README file for details, or visit http://0install.net. + +from zeroinstall import _ + +class Requirements(object): + """ + Holds information about what the user asked for (which program, version constraints, etc). + """ + __slots__ = [ + 'interface_uri', + 'command', + 'source', + 'before', 'not_before', + 'os', 'cpu', + 'message', + ] + + def __init__(self, interface_uri): + self.interface_uri = interface_uri + self.command = 'run' + self.source = False + self.before = self.not_before = None + self.os = self.cpu = None + self.message = None + + def parse_options(self, options): + self.not_before = options.not_before + self.before = options.before + + self.source = bool(options.source) + self.message = options.message + + self.cpu = options.cpu + self.os = options.os + + # (None becomes 'run', while '' becomes None) + if options.command is None: + if self.source: + self.command = 'compile' + else: + self.command = 'run' + else: + self.command = options.command or None + + def get_as_options(self): + gui_args = [] + if self.not_before: + gui_args.insert(0, self.not_before) + gui_args.insert(0, '--not-before') + if self.before: + gui_args.insert(0, self.before) + gui_args.insert(0, '--before') + if self.source: + gui_args.insert(0, '--source') + if self.message: + gui_args.insert(0, self.message) + gui_args.insert(0, '--message') + if self.cpu: + gui_args.insert(0, self.cpu) + gui_args.insert(0, '--cpu') + if self.os: + gui_args.insert(0, self.os) + gui_args.insert(0, '--os') + gui_args.append('--command') + gui_args.append(self.command or '') + + return gui_args diff --git a/zeroinstall/injector/run.py b/zeroinstall/injector/run.py index d49a49e..099a3c0 100644 --- a/zeroinstall/injector/run.py +++ b/zeroinstall/injector/run.py @@ -7,7 +7,7 @@ Executes a set of implementations as a program. from zeroinstall import _ import os, sys -from logging import debug, info +from logging import info from string import Template from zeroinstall.injector.model import SafeException, EnvironmentBinding, Command @@ -45,7 +45,6 @@ def test_selections(selections, prog_args, dry_run, main, wrapper = None): @return: the output produced by the process @since: 0.27 """ - args = [] import tempfile output = tempfile.TemporaryFile(prefix = '0launch-test') try: @@ -101,7 +100,7 @@ def execute_selections(selections, prog_args, dry_run = False, main = None, wrap @since: 0.27 @precondition: All implementations are in the cache. """ - assert stores is not None + #assert stores is not None if stores is None: from zeroinstall import zerostore stores = zerostore.Stores() diff --git a/zeroinstall/injector/sat.py b/zeroinstall/injector/sat.py index 0ac6670..1f9ff25 100644 --- a/zeroinstall/injector/sat.py +++ b/zeroinstall/injector/sat.py @@ -18,9 +18,6 @@ This is not part of the public API. # - We add an AtMostOneClause (the paper suggests this in the Excercises, and # it's very useful for our purposes). -import tempfile, subprocess, os, sys -from logging import warn - def debug(msg, *args): return print "SAT:", msg % args @@ -75,7 +72,6 @@ def makeAtMostOneClause(solver): # If we later backtrace, call our undo function to unset current solver.get_varinfo_for_lit(lit).undo.append(self) - count = 0 for l in self.lits: value = solver.lit_value(l) #debug("Value of %s is %s" % (solver.name_lit(l), value)) @@ -300,7 +296,6 @@ class SATProblem(object): while self.propQ: lit = self.propQ[0] del self.propQ[0] - var_info = self.get_varinfo_for_lit(lit) wi = watch_index(lit) watches = self.watches[wi] self.watches[wi] = [] diff --git a/zeroinstall/injector/selections.py b/zeroinstall/injector/selections.py index e748cc8..1358f6f 100644 --- a/zeroinstall/injector/selections.py +++ b/zeroinstall/injector/selections.py @@ -7,7 +7,8 @@ Load and save a set of chosen implementations. # See the README file for details, or visit http://0install.net. from zeroinstall import _ -from zeroinstall.injector.policy import Policy +from zeroinstall.injector import model +from zeroinstall.injector.policy import Policy, get_deprecated_singleton_config 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, Prefixes @@ -246,26 +247,35 @@ class Selections(object): def __repr__(self): return "Selections for " + self.interface - def download_missing(self, iface_cache, fetcher): + def download_missing(self, config, _old = None): """Check all selected implementations are available. Download any that are not present. Note: package implementations (distribution packages) are ignored. - @param iface_cache: cache to find feeds with download information - @param fetcher: used to download missing implementations + @param config: used to get iface_cache, stores and fetcher @return: a L{tasks.Blocker} or None""" from zeroinstall.zerostore import NotStored + if _old: + config = get_deprecated_singleton_config() + + iface_cache = config.iface_cache + stores = config.stores + # Check that every required selection is cached needed_downloads = [] for sel in self.selections.values(): if (not sel.local_path) and (not sel.id.startswith('package:')): try: - iface_cache.stores.lookup_any(sel.digests) - except NotStored, ex: + stores.lookup_any(sel.digests) + except NotStored: needed_downloads.append(sel) if not needed_downloads: return + if config.network_use == model.network_offline: + from zeroinstall import NeedDownload + raise NeedDownload(', '.join([str(x) for x in needed_downloads])) + @tasks.async def download(): # We're missing some. For each one, get the feed it came from @@ -278,7 +288,7 @@ class Selections(object): feed_url = sel.attrs.get('from-feed', None) or sel.attrs['interface'] feed = iface_cache.get_feed(feed_url) if feed is None or sel.id not in feed.implementations: - fetch_feed = fetcher.download_and_import_feed(feed_url, iface_cache) + fetch_feed = config.fetcher.download_and_import_feed(feed_url, iface_cache) yield fetch_feed tasks.check(fetch_feed) @@ -287,7 +297,7 @@ class Selections(object): impl = feed.implementations[sel.id] needed_impls.append(impl) - fetch_impls = fetcher.download_impls(needed_impls, iface_cache.stores) + fetch_impls = config.fetcher.download_impls(needed_impls, stores) yield fetch_impls tasks.check(fetch_impls) return download() @@ -303,22 +313,18 @@ class Selections(object): def iteritems(self): # Deprecated - from zeroinstall.injector import policy - iface_cache = policy.get_deprecated_singleton_config().iface_cache + iface_cache = get_deprecated_singleton_config().iface_cache for (uri, sel) in self.selections.iteritems(): yield (iface_cache.get_interface(uri), sel and sel.impl) def values(self): # Deprecated - from zeroinstall.injector import policy - iface_cache = policy.get_deprecated_singleton_config().iface_cache for (uri, sel) in self.selections.iteritems(): yield sel and sel.impl def __iter__(self): # Deprecated - from zeroinstall.injector import policy - iface_cache = policy.get_deprecated_singleton_config().iface_cache + iface_cache = get_deprecated_singleton_config().iface_cache for (uri, sel) in self.selections.iteritems(): yield iface_cache.get_interface(uri) diff --git a/zeroinstall/injector/solver.py b/zeroinstall/injector/solver.py index ad58b8e..b2e8e4d 100644 --- a/zeroinstall/injector/solver.py +++ b/zeroinstall/injector/solver.py @@ -6,7 +6,7 @@ Chooses a set of components to make a running program. # See the README file for details, or visit http://0install.net. from zeroinstall import _ -import os, tempfile, subprocess, sys, locale +import os, locale from logging import debug, warn, info from zeroinstall.zerostore import BadDigest, NotStored @@ -121,6 +121,10 @@ class Solver(object): class SATSolver(Solver): __slots__ = ['_failure_reason', 'config', 'extra_restrictions', 'langs'] + @property + def iface_cache(self): + return self.config.iface_cache # (deprecated; used by 0compile) + """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""" @@ -229,7 +233,6 @@ class SATSolver(Solver): # selects that. iface_cache = self.config.iface_cache - feeds_added = set() problem = sat.SATProblem() impl_to_var = {} # Impl -> sat var @@ -332,7 +335,6 @@ class SATSolver(Solver): """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) iface = iface_cache.get_interface(uri) @@ -358,7 +360,7 @@ class SATSolver(Solver): impls.extend(distro_feed.implementations.values()) except Exception, ex: warn(_("Failed to load feed %(feed)s for %(interface)s: %(exception)s"), {'feed': f, 'interface': iface, 'exception': ex}) - raise + #raise impls.sort(lambda a, b: self.compare(iface, a, b, arch)) diff --git a/zeroinstall/support/tasks.py b/zeroinstall/support/tasks.py index c22ba43..3766547 100644 --- a/zeroinstall/support/tasks.py +++ b/zeroinstall/support/tasks.py @@ -250,7 +250,6 @@ class Task: def _resume(self): # Remove from our blockers' queues - exception = None for blocker in self._zero_blockers: blocker.remove_task(self) # Resume the task diff --git a/zeroinstall/zerostore/__init__.py b/zeroinstall/zerostore/__init__.py index d02d789..ed878f0 100644 --- a/zeroinstall/zerostore/__init__.py +++ b/zeroinstall/zerostore/__init__.py @@ -26,7 +26,6 @@ def _copytree2(src, dst): import shutil names = os.listdir(src) assert os.path.isdir(dst) - errors = [] for name in names: srcname = os.path.join(src, name) dstname = os.path.join(dst, name) @@ -87,7 +86,7 @@ class Store: def add_archive_to_cache(self, required_digest, data, url, extract = None, type = None, start_offset = 0, try_helper = False): import unpack - info(_("Caching new implementation (digest %s)"), required_digest) + info(_("Caching new implementation (digest %s) in %s"), required_digest, self.dir) if self.lookup(required_digest): info(_("Not adding %s as it already exists!"), required_digest) @@ -103,7 +102,7 @@ class Store: try: self.check_manifest_and_rename(required_digest, tmp, extract, try_helper = try_helper) - except Exception, ex: + except Exception: warn(_("Leaving extracted directory as %s"), tmp) raise -- 2.11.4.GIT