NoLocalVersions inherits from model.Restriction
[0export.git] / install.py
blob87919986c31586722776d504f8c331e7ba809664
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 if not trust.trust_db.is_trusted(s.fingerprint, domain):
97 print "Adding key %s to trusted list for %s" % (s.fingerprint, domain)
98 trust.trust_db.trust_key(s.fingerprint, domain)
99 oldest_sig = min([s.get_timestamp() for s in sigs])
100 try:
101 config.iface_cache.update_feed_from_network(uri, stream.read(), oldest_sig)
102 except iface_cache.ReplayAttack:
103 # OK, the user has a newer copy already
104 pass
105 if feed_stream != stream:
106 feed_stream.close()
107 stream.close()
109 if os.path.exists(icon_path):
110 icons_cache = basedir.save_cache_path(namespaces.config_site, 'interface_icons')
111 icon_file = os.path.join(icons_cache, model.escape(uri))
112 if not os.path.exists(icon_file):
113 shutil.copyfile(icon_path, icon_file)
115 # Step 3. Solve to find out which implementations we actually need
116 archive_stream.seek(archive_offset)
118 extract_impls = {} # Impls we need but which are compressed (ID -> Impl)
119 tmp = tempfile.mkdtemp(prefix = '0export-')
120 try:
121 # Create a "fake store" with the implementation in the archive
122 archive = tarfile.open(name=archive_stream.name, mode='r|', fileobj=archive_stream)
123 fake_store = FakeStore()
124 for tarmember in archive:
125 if tarmember.name.startswith('implementations'):
126 impl = os.path.basename(tarmember.name).split('.')[0]
127 fake_store.impls.add(impl)
129 bootstrap_store = zerostore.Store(os.path.join(mydir, 'implementations'))
130 stores = config.stores
132 toplevel_uris = [uri.strip() for uri in file(os.path.join(mydir, 'toplevel_uris'))]
133 ZEROINSTALL_URI = "@ZEROINSTALL_URI@"
134 for uri in [ZEROINSTALL_URI] + toplevel_uris:
135 # This is so the solver treats versions in the setup archive as 'cached',
136 # meaning that it will prefer using them to doing a download
137 stores.stores.append(bootstrap_store)
138 stores.stores.append(fake_store)
140 # Shouldn't need to download anything, but we might not have all feeds
141 r = requirements.Requirements(uri)
142 d = driver.Driver(config = config, requirements = r)
143 config.network_use = model.network_minimal
144 download_feeds = d.solve_with_downloads()
145 h.wait_for_blocker(download_feeds)
146 assert d.solver.ready, d.solver.get_failure_reason()
148 # Add anything chosen from the setup store to the main store
149 stores.stores.remove(fake_store)
150 stores.stores.remove(bootstrap_store)
151 for iface, impl in d.get_uncached_implementations():
152 print >>sys.stderr, "Need to import", impl
153 if impl.id in fake_store.impls:
154 # Delay extraction
155 extract_impls[impl.id] = impl
156 else:
157 impl_src = os.path.join(mydir, 'implementations', impl.id)
159 if os.path.isdir(impl_src):
160 stores.add_dir_to_cache(impl.id, impl_src)
161 else:
162 print >>sys.stderr, "Required impl %s (for %s) not present" % (impl, iface)
164 # Remember where we copied 0launch to, because we'll need it after
165 # the temporary directory is deleted.
166 if uri == ZEROINSTALL_URI:
167 global copied_0launch_in_cache
168 impl = d.solver.selections.selections[uri]
169 if not impl.id.startswith('package:'):
170 copied_0launch_in_cache = impl.get_path(stores = config.stores)
171 # (else we selected the distribution version of Zero Install)
172 finally:
173 shutil.rmtree(tmp)
175 # Count total number of bytes to extract
176 extract_total = 0
177 for impl in extract_impls.values():
178 impl_info = archive.getmember('implementations/' + impl.id + '.tar.bz2')
179 extract_total += impl_info.size
181 self.sent = 0
183 # Actually extract+import implementations in archive
184 archive_stream.seek(archive_offset)
185 archive = tarfile.open(name=archive_stream.name, mode='r|',
186 fileobj=archive_stream)
188 for tarmember in archive:
189 if not tarmember.name.startswith('implementations'):
190 continue
191 impl_id = tarmember.name.split('/')[1].split('.')[0]
192 if impl_id not in extract_impls:
193 print "Skip", impl_id
194 continue
195 print "Extracting", impl_id
196 tmp = tempfile.mkdtemp(prefix = '0export-')
197 try:
198 impl_stream = archive.extractfile(tarmember)
199 self.child = subprocess.Popen('bunzip2|tar xf -', shell = True, stdin = subprocess.PIPE, cwd = tmp)
200 mainloop = gobject.MainLoop(gobject.main_context_default())
202 def pipe_ready(src, cond):
203 data = impl_stream.read(4096)
204 if not data:
205 mainloop.quit()
206 self.child.stdin.close()
207 return False
208 self.sent += len(data)
209 if progress_bar:
210 progress_bar.set_fraction(float(self.sent) / extract_total)
211 self.child.stdin.write(data)
212 return True
213 gobject.io_add_watch(self.child.stdin, gobject.IO_OUT | gobject.IO_HUP, pipe_ready, priority = gobject.PRIORITY_LOW)
215 mainloop.run()
217 self.child.wait()
218 if self.child.returncode:
219 raise Exception("Failed to unpack archive (code %d)" % self.child.returncode)
221 stores.add_dir_to_cache(impl_id, tmp)
223 finally:
224 shutil.rmtree(tmp)
226 return toplevel_uris
228 def add_to_menu(uris):
229 for uri in uris:
230 iface = config.iface_cache.get_interface(uri)
231 icon_path = config.iface_cache.get_icon_path(iface)
233 feed_category = ''
234 for meta in iface.get_metadata(namespaces.XMLNS_IFACE, 'category'):
235 c = meta.content
236 if '\n' in c:
237 raise Exception("Invalid category '%s'" % c)
238 feed_category = c
239 break
241 xdgutils.add_to_menu(iface, icon_path, feed_category)
243 if find_in_path('0launch'):
244 return
246 if find_in_path('sudo') and find_in_path('gnome-terminal') and find_in_path('apt-get'):
247 check_call(['gnome-terminal', '--disable-factory', '-x', 'sh', '-c',
248 'echo "We need to install the zeroinstall-injector package to make the menu items work."; '
249 'sudo apt-get install zeroinstall-injector || sleep 4'])
251 if find_in_path('0launch'):
252 return
254 import gtk
255 box = gtk.MessageDialog(None, 0, buttons = gtk.BUTTONS_OK)
256 box.set_markup("The new menu item won't work until the '<b>zeroinstall-injector</b>' package is installed.\n"
257 "Please install it using your distribution's package manager.")
258 box.run()
259 box.destroy()
260 gtk.gdk.flush()
262 def run(uri, args, prog_args):
263 print "Running program..."
264 if copied_0launch_in_cache:
265 launch = os.path.join(copied_0launch_in_cache, '0launch')
266 else:
267 launch = find_in_path('0launch')
268 os.execv(launch, [launch] + args + [uri] + prog_args)