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
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')
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
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!)"
39 def check_call(*args
, **kwargs
):
40 exitstatus
= subprocess
.call(*args
, **kwargs
)
42 raise SafeException("Command failed with exit code %d:\n%s" % (exitstatus
, ' '.join(args
[0])))
50 def lookup(self
, digest
):
51 if digest
in self
.impls
:
52 return "/fake/" + digest
57 return find_in_path('gpg') or find_in_path('gpg2')
64 if self
.child
is not None:
65 os
.kill(self
.child
.pid
, signal
.SIGTERM
)
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')
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
)
94 stream
, sigs
= gpg
.check_stream(feed_stream
)
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
])
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
106 if feed_stream
!= stream
:
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-')
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
:
156 extract_impls
[impl
.id] = impl
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
)
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)
176 # Count total number of bytes to extract
178 for impl
in extract_impls
.values():
179 impl_info
= archive
.getmember('implementations/' + impl
.id + '.tar.bz2')
180 extract_total
+= impl_info
.size
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'):
192 impl_id
= tarmember
.name
.split('/')[1].split('.')[0]
193 if impl_id
not in extract_impls
:
194 print "Skip", impl_id
196 print "Extracting", impl_id
197 tmp
= tempfile
.mkdtemp(prefix
= '0export-')
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)
207 self
.child
.stdin
.close()
209 self
.sent
+= len(data
)
211 progress_bar
.set_fraction(float(self
.sent
) / extract_total
)
212 self
.child
.stdin
.write(data
)
214 gobject
.io_add_watch(self
.child
.stdin
, gobject
.IO_OUT | gobject
.IO_HUP
, pipe_ready
, priority
= gobject
.PRIORITY_LOW
)
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
)
229 def add_to_menu(uris
):
231 iface
= config
.iface_cache
.get_interface(uri
)
232 icon_path
= config
.iface_cache
.get_icon_path(iface
)
235 for meta
in iface
.get_metadata(namespaces
.XMLNS_IFACE
, 'category'):
238 raise Exception("Invalid category '%s'" % c
)
242 xdgutils
.add_to_menu(iface
, icon_path
, feed_category
)
244 if find_in_path('0launch'):
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'):
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.")
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')
268 launch
= find_in_path('0launch')
269 os
.execv(launch
, [launch
] + args
+ [uri
] + prog_args
)