Release 0.3
[0export.git] / 0export
blob290677ff35a5a2614fb4091506c662e527b0297b
1 #!/usr/bin/env python
2 # Copyright (C) 2010, Thomas Leonard
3 # See the README file for details, or visit http://0install.net.
5 from optparse import OptionParser
6 import os, sys, tempfile, shutil, tarfile
7 from logging import warn, info, debug
9 ZEROINSTALL_URI = "http://0install.net/2007/interfaces/ZeroInstall.xml"
11 zeroinstall_dir = os.environ.get('0EXPORT_ZEROINSTALL', None)
12 if zeroinstall_dir:
13 sys.path.insert(1, zeroinstall_dir)
15 version = '0.3'
17 extract_header = """#!/bin/sh
18 # 0export %s
19 archive_offset=00000
20 archive_format=application/x-tar
21 package_version=2
22 exec python - "$0" "$archive_offset" "$@" << EOF
23 """ + file(os.path.join(os.path.dirname(__file__), 'unpacker.py')).read() + """
24 EOF
25 exit 1
26 """
28 parser = OptionParser(usage="usage: %prog setup.sh URI...\n"
29 "Create setup.sh self-extracting installer for program(s) URI...")
31 parser.add_option("-j", "--injector", help="use custom zeroinstall-injector url", default=ZEROINSTALL_URI)
32 parser.add_option("-n", "--net-install", help="lightweight installer mode; bundle only injector, passed interface will be downloaded at run time", action='store_true')
33 parser.add_option("-a", "--arch", help="add a target architecture (os-cpu)", metavar='CPU', action='append')
34 parser.add_option("-v", "--verbose", help="more verbose output", action='count')
35 parser.add_option("-V", "--version", help="display version information", action='store_true')
37 (options, args) = parser.parse_args()
39 if options.version:
40 print "0export (zero-install) " + version
41 print "Copyright (C) 2010 Thomas Leonard"
42 print "This program comes with ABSOLUTELY NO WARRANTY,"
43 print "to the extent permitted by law."
44 print "You may redistribute copies of this program"
45 print "under the terms of the GNU General Public License."
46 print "For more information about these matters, see the file named COPYING."
47 sys.exit(0)
49 if options.verbose:
50 import logging
51 logger = logging.getLogger()
52 if options.verbose == 1:
53 logger.setLevel(logging.INFO)
54 else:
55 logger.setLevel(logging.DEBUG)
57 if len(args) < 2:
58 parser.print_help()
59 sys.exit(1)
61 from zeroinstall import support
62 from zeroinstall.zerostore import BadDigest
63 from zeroinstall.injector import policy, handler, model, arch, solver
64 from zeroinstall import SafeException
65 import utils
67 setup_file = args[0]
68 uris = [model.canonical_iface_uri(x) for x in args[1:]]
70 if options.arch:
71 def get_arch(arch_str):
72 if '-' not in arch_str:
73 raise Exception("Architecture syntax is OS-CPU (e.g. 'Linux-86_64')")
74 os, cpu = arch_str.split('-', 1)
75 if os in ("*", "any"): os = None
76 if cpu in ("*", "any"): cpu = None
77 return arch.get_architecture(os, cpu)
78 archs = [get_arch(a) for a in options.arch]
79 else:
80 archs = [arch.get_host_architecture()]
82 class ExportSolver(solver.SATSolver):
83 def compare(self, interface, b, a, arch):
84 # Prefer non-native packages all time to bundle them at first
85 r = cmp(b.id.startswith('package:'), a.id.startswith('package:'))
86 if r: return r
87 return solver.SATSolver.compare(self, interface, b, a, arch)
89 solver.DefaultSolver = ExportSolver
91 def choose_implementations(uris, only_feeds):
92 h = handler.Handler()
94 interfaces_used = set() # URI
95 feeds_used = set() # URI
96 implementations_used = {} # ID -> Implementation
97 uri_impls = {} # Maps top-level URIs to implementations
99 for uri in uris:
100 print "Choosing versions for %s..." % uri
101 versions = 0
102 for target_arch in archs:
103 p = policy.Policy(uri, h)
104 p.target_arch = target_arch
106 # Don't let us choose local devel versions
107 p.solver.extra_restrictions = utils.NoLocalRestrictions(uris)
109 solved = p.solve_with_downloads()
110 h.wait_for_blocker(solved)
112 if not p.ready:
113 if len(archs) > 1:
114 warn("Failed to select any version for %s for architecture %s" % (uri, target_arch))
115 continue
116 versions += 1
118 feeds_used = feeds_used | p.solver.feeds_used
119 iface_cache = p.solver.iface_cache
121 if not only_feeds:
122 for iface, impl in p.solver.selections.items():
123 if impl.id.startswith('package:'):
124 debug('Skip package implementation %r', impl)
125 continue
126 print " %-10s : %s (%s)" % (iface.get_name(), impl.get_version(), impl.arch or 'any arch')
127 # TODO: assumes the ID is a digest (globally unique)
128 # With 0launch >= 0.45, that might not be true.
129 implementations_used[impl.id] = impl
131 downloads = p.download_uncached_implementations()
132 if downloads:
133 print "Downloading implementations..."
134 h.wait_for_blocker(downloads)
136 uri_impls[uri] = p.solver.selections[iface_cache.get_interface(uri)].id
138 for iface, impl in p.solver.selections.items():
139 interfaces_used.add(iface.uri)
141 if not versions:
142 raise SafeException("Failed to select a set of versions for %s" % uri)
144 for iface_uri in interfaces_used:
145 if iface_uri.startswith('/'): continue
146 iface = iface_cache.get_interface(iface_uri)
147 icon = iface_cache.get_icon_path(iface)
148 if icon is None:
149 download_icon = p.fetcher.download_icon(iface)
150 if download_icon:
151 print "Downloading icon..."
152 h.wait_for_blocker(download_icon)
154 return (feeds_used, implementations_used, uri_impls)
156 bootstrap_uris = [options.injector]
158 try:
159 (bootstrap_feeds_used, bootstrap_implementations_used, bootstrap_uri_impls) = choose_implementations(bootstrap_uris, False)
160 (feeds_used, implementations_used, uri_impls) = choose_implementations(uris, options.net_install)
162 keys_used = set()
164 print "Building package..."
166 tmp = tempfile.mkdtemp(prefix = '0export-')
167 bootstrap_tmp = tempfile.mkdtemp(prefix = '0export-')
168 try:
169 # Add feeds...
170 utils.export_feeds(bootstrap_tmp, bootstrap_feeds_used | feeds_used, keys_used)
172 # Add implementations...
173 utils.export_impls(bootstrap_tmp, bootstrap_implementations_used)
175 os.symlink(os.path.join('implementations', bootstrap_uri_impls[options.injector]), os.path.join(bootstrap_tmp, 'zeroinstall'))
177 # Add keys...
178 keys_dir = os.path.join(bootstrap_tmp, 'keys')
179 os.mkdir(keys_dir)
180 for key in keys_used:
181 utils.export_key(key, keys_dir)
183 # Add installer...
184 mydir = os.path.dirname(os.path.abspath(sys.argv[0]))
186 install_code = file(os.path.join(mydir, 'install.py')).read()
187 install_code = install_code.replace('@ZEROINSTALL_URI@', options.injector)
188 install_file = file(os.path.join(bootstrap_tmp, 'install.py'), 'w')
189 install_file.write(install_code)
190 install_file.close()
192 # Record the toplevel interfaces (not those brought in only as dependencies)
193 # These are the programs the installer will offer to run
194 toplevels = file(os.path.join(bootstrap_tmp, 'toplevel_uris'), 'w')
195 for uri in uris:
196 toplevels.write(uri + '\n')
197 toplevels.close()
199 # Create an archive with bootstrap data
200 bootstrap_path = os.path.join(tmp, 'bootstrap.tar.bz2')
201 ts = tarfile.open(bootstrap_path, 'w|bz2')
202 ts.add(bootstrap_tmp, '.')
203 ts.close()
205 # Collect all other implementations in separate archives in the archive
206 impl_files = []
207 for impl in set(implementations_used) - set(bootstrap_implementations_used):
208 impltmp = tempfile.mkdtemp(prefix = '0export-')
209 try:
210 utils.export_impls(impltmp, {impl : implementations_used[impl]})
211 implpath = os.path.join(tmp, impl + '.tar.bz2')
212 ts = tarfile.open(implpath, 'w|bz2')
213 ts.add(str(os.path.join(impltmp, 'implementations', impl)), '.')
214 ts.close()
215 finally:
216 support.ro_rmtree(impltmp)
217 impl_files.append(implpath)
219 extract_header = extract_header.replace('@INSTALLER_MODE@', str(options.net_install))
220 extract_header = extract_header.replace('00000', "%05d" % len(extract_header))
221 setup_stream = file(setup_file, 'wb')
222 setup_stream.write(extract_header)
223 setup_tar = tarfile.open(setup_file, 'w|', setup_stream)
224 setup_tar.add(bootstrap_path, os.path.basename(bootstrap_path))
225 for impl in impl_files:
226 setup_tar.add(impl, str('implementations/' + os.path.basename(impl)))
227 setup_tar.close()
228 setup_stream.close()
229 os.chmod(setup_file, (os.stat(setup_file).st_mode & 0777) | 0111)
230 finally:
231 support.ro_rmtree(bootstrap_tmp)
232 support.ro_rmtree(tmp)
233 except BadDigest, ex:
234 print >>sys.stderr, str(ex)
235 if ex.detail:
236 print >>sys.stderr, ex.detail
237 sys.exit(1)
238 except SafeException, ex:
239 print >>sys.stderr, str(ex)
240 sys.exit(1)