Better error when a key is missing from the bundle
[0export.git] / install.py
blobfea66e9cd85e63b5ffae3ca0f614cbd76f6ddb8e
1 # This script is part of 0export
2 # Copyright (C) 2010, Thomas Leonard
3 # See http://0install.net for details.
5 # This file goes inside the generated setup.sh archive
6 # It runs or installs the program
8 import os, sys, subprocess, tempfile, tarfile, gobject, signal
9 import shutil
11 mydir = os.path.dirname(os.path.abspath(sys.argv[0]))
12 zidir = os.path.join(mydir, 'zeroinstall')
13 sys.path.insert(0, zidir)
14 feeds_dir = os.path.join(mydir, 'feeds')
15 pypath = os.environ.get('PYTHONPATH')
16 if pypath:
17 pypath = ':' + pypath
18 else:
19 pypath = ''
20 os.environ['PYTHONPATH'] = zidir + pypath
22 from zeroinstall.injector import gpg, trust, qdom, iface_cache, driver, handler, model, namespaces, config, requirements
23 from zeroinstall.support import basedir, find_in_path
24 from zeroinstall import SafeException, zerostore
25 from zeroinstall.gtkui import xdgutils
27 h = handler.Handler()
28 config = config.load_config(handler = h)
30 # During the install we copy this to the local cache
31 copied_0launch_in_cache = None
33 if not os.path.isdir(feeds_dir):
34 print >>sys.stderr, "Directory %s not found." % feeds_dir
35 print >>sys.stderr, "This script should be run from an unpacked setup.sh archive."
36 print >>sys.stderr, "(are you trying to install 0export? you're in the wrong place!)"
37 sys.exit(1)
39 def check_call(*args, **kwargs):
40 exitstatus = subprocess.call(*args, **kwargs)
41 if exitstatus != 0:
42 raise SafeException("Command failed with exit code %d:\n%s" % (exitstatus, ' '.join(args[0])))
44 class FakeStore:
45 dir = '/fake'
47 def __init__(self):
48 self.impls = set()
50 def lookup(self, digest):
51 if digest in self.impls:
52 return "/fake/" + digest
53 else:
54 return None
56 def get_gpg():
57 return find_in_path('gpg') or find_in_path('gpg2')
59 class Installer:
60 child = None
61 sent = 0
63 def abort(self):
64 if self.child is not None:
65 os.kill(self.child.pid, signal.SIGTERM)
66 self.child.wait()
67 self.child = None
69 def do_install(self, archive_stream, progress_bar, archive_offset):
70 # Step 1. Import GPG keys
72 # Maybe GPG has never been run before. Let it initialse, or we'll get an error code
73 # from the first import... (ignore return value here)
74 subprocess.call([get_gpg(), '--check-trustdb', '-q'])
76 key_dir = os.path.join(mydir, 'keys')
77 for key in os.listdir(key_dir):
78 check_call([get_gpg(), '--import', '-q', os.path.join(key_dir, key)])
80 # Step 2. Import feeds and trust their signing keys
81 for root, dirs, files in os.walk(os.path.join(mydir, 'feeds')):
82 if 'latest.xml' in files:
83 feed_path = os.path.join(root, 'latest.xml')
84 icon_path = os.path.join(root, 'icon.png')
86 # Get URI
87 feed_stream = file(feed_path)
88 doc = qdom.parse(feed_stream)
89 uri = doc.getAttribute('uri')
90 assert uri, "Missing 'uri' attribute on root element in '%s'" % feed_path
91 domain = trust.domain_from_url(uri)
93 feed_stream.seek(0)
94 stream, sigs = gpg.check_stream(feed_stream)
95 for s in sigs:
96 assert isinstance(s, gpg.ValidSig), str(s)
97 if not trust.trust_db.is_trusted(s.fingerprint, domain):
98 print "Adding key %s to trusted list for %s" % (s.fingerprint, domain)
99 trust.trust_db.trust_key(s.fingerprint, domain)
100 oldest_sig = min([s.get_timestamp() for s in sigs])
101 try:
102 config.iface_cache.update_feed_from_network(uri, stream.read(), oldest_sig)
103 except iface_cache.ReplayAttack:
104 # OK, the user has a newer copy already
105 pass
106 if feed_stream != stream:
107 feed_stream.close()
108 stream.close()
110 if os.path.exists(icon_path):
111 icons_cache = basedir.save_cache_path(namespaces.config_site, 'interface_icons')
112 icon_file = os.path.join(icons_cache, model.escape(uri))
113 if not os.path.exists(icon_file):
114 shutil.copyfile(icon_path, icon_file)
116 # Step 3. Solve to find out which implementations we actually need
117 archive_stream.seek(archive_offset)
119 extract_impls = {} # Impls we need but which are compressed (ID -> Impl)
120 tmp = tempfile.mkdtemp(prefix = '0export-')
121 try:
122 # Create a "fake store" with the implementation in the archive
123 archive = tarfile.open(name=archive_stream.name, mode='r|', fileobj=archive_stream)
124 fake_store = FakeStore()
125 for tarmember in archive:
126 if tarmember.name.startswith('implementations'):
127 impl = os.path.basename(tarmember.name).split('.')[0]
128 fake_store.impls.add(impl)
130 bootstrap_store = zerostore.Store(os.path.join(mydir, 'implementations'))
131 stores = config.stores
133 toplevel_uris = [uri.strip() for uri in file(os.path.join(mydir, 'toplevel_uris'))]
134 ZEROINSTALL_URI = "@ZEROINSTALL_URI@"
135 for uri in [ZEROINSTALL_URI] + toplevel_uris:
136 # This is so the solver treats versions in the setup archive as 'cached',
137 # meaning that it will prefer using them to doing a download
138 stores.stores.append(bootstrap_store)
139 stores.stores.append(fake_store)
141 # Shouldn't need to download anything, but we might not have all feeds
142 r = requirements.Requirements(uri)
143 d = driver.Driver(config = config, requirements = r)
144 config.network_use = model.network_minimal
145 download_feeds = d.solve_with_downloads()
146 h.wait_for_blocker(download_feeds)
147 assert d.solver.ready, d.solver.get_failure_reason()
149 # Add anything chosen from the setup store to the main store
150 stores.stores.remove(fake_store)
151 stores.stores.remove(bootstrap_store)
152 for iface, impl in d.get_uncached_implementations():
153 print >>sys.stderr, "Need to import", impl
154 if impl.id in fake_store.impls:
155 # Delay extraction
156 extract_impls[impl.id] = impl
157 else:
158 impl_src = os.path.join(mydir, 'implementations', impl.id)
160 if os.path.isdir(impl_src):
161 stores.add_dir_to_cache(impl.id, impl_src)
162 else:
163 print >>sys.stderr, "Required impl %s (for %s) not present" % (impl, iface)
165 # Remember where we copied 0launch to, because we'll need it after
166 # the temporary directory is deleted.
167 if uri == ZEROINSTALL_URI:
168 global copied_0launch_in_cache
169 impl = d.solver.selections.selections[uri]
170 if not impl.id.startswith('package:'):
171 copied_0launch_in_cache = impl.get_path(stores = config.stores)
172 # (else we selected the distribution version of Zero Install)
173 finally:
174 shutil.rmtree(tmp)
176 # Count total number of bytes to extract
177 extract_total = 0
178 for impl in extract_impls.values():
179 impl_info = archive.getmember('implementations/' + impl.id + '.tar.bz2')
180 extract_total += impl_info.size
182 self.sent = 0
184 # Actually extract+import implementations in archive
185 archive_stream.seek(archive_offset)
186 archive = tarfile.open(name=archive_stream.name, mode='r|',
187 fileobj=archive_stream)
189 for tarmember in archive:
190 if not tarmember.name.startswith('implementations'):
191 continue
192 impl_id = tarmember.name.split('/')[1].split('.')[0]
193 if impl_id not in extract_impls:
194 print "Skip", impl_id
195 continue
196 print "Extracting", impl_id
197 tmp = tempfile.mkdtemp(prefix = '0export-')
198 try:
199 impl_stream = archive.extractfile(tarmember)
200 self.child = subprocess.Popen('bunzip2|tar xf -', shell = True, stdin = subprocess.PIPE, cwd = tmp)
201 mainloop = gobject.MainLoop(gobject.main_context_default())
203 def pipe_ready(src, cond):
204 data = impl_stream.read(4096)
205 if not data:
206 mainloop.quit()
207 self.child.stdin.close()
208 return False
209 self.sent += len(data)
210 if progress_bar:
211 progress_bar.set_fraction(float(self.sent) / extract_total)
212 self.child.stdin.write(data)
213 return True
214 gobject.io_add_watch(self.child.stdin, gobject.IO_OUT | gobject.IO_HUP, pipe_ready, priority = gobject.PRIORITY_LOW)
216 mainloop.run()
218 self.child.wait()
219 if self.child.returncode:
220 raise Exception("Failed to unpack archive (code %d)" % self.child.returncode)
222 stores.add_dir_to_cache(impl_id, tmp)
224 finally:
225 shutil.rmtree(tmp)
227 return toplevel_uris
229 def add_to_menu(uris):
230 for uri in uris:
231 iface = config.iface_cache.get_interface(uri)
232 icon_path = config.iface_cache.get_icon_path(iface)
234 feed_category = ''
235 for meta in iface.get_metadata(namespaces.XMLNS_IFACE, 'category'):
236 c = meta.content
237 if '\n' in c:
238 raise Exception("Invalid category '%s'" % c)
239 feed_category = c
240 break
242 xdgutils.add_to_menu(iface, icon_path, feed_category)
244 if find_in_path('0launch'):
245 return
247 if find_in_path('sudo') and find_in_path('gnome-terminal') and find_in_path('apt-get'):
248 check_call(['gnome-terminal', '--disable-factory', '-x', 'sh', '-c',
249 'echo "We need to install the zeroinstall-injector package to make the menu items work."; '
250 'sudo apt-get install zeroinstall-injector || sleep 4'])
252 if find_in_path('0launch'):
253 return
255 import gtk
256 box = gtk.MessageDialog(None, 0, buttons = gtk.BUTTONS_OK)
257 box.set_markup("The new menu item won't work until the '<b>zeroinstall-injector</b>' package is installed.\n"
258 "Please install it using your distribution's package manager.")
259 box.run()
260 box.destroy()
261 gtk.gdk.flush()
263 def run(uri, args, prog_args):
264 print "Running program..."
265 if copied_0launch_in_cache:
266 launch = os.path.join(copied_0launch_in_cache, '0launch')
267 else:
268 launch = find_in_path('0launch')
269 os.execv(launch, [launch] + args + [uri] + prog_args)