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
, 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)
13 sys
.path
.insert(1, zeroinstall_dir
)
17 extract_header
= """#!/bin/sh
18 # 0export """ + version
+ """
20 archive_format=application/x-tar
22 if [ -x /usr/bin/python2 ]; then
23 PYTHON=/usr/bin/python2
27 exec "$PYTHON" - "$0" "$archive_offset" "$@" << EOF
28 """ + file(os
.path
.join(os
.path
.dirname(__file__
), 'unpacker.py')).read() + """
33 parser
= OptionParser(usage
="usage: %prog setup.sh URI...\n"
34 "Create setup.sh self-extracting installer for program(s) URI...")
36 parser
.add_option("-j", "--injector", help="use custom zeroinstall-injector url", default
=ZEROINSTALL_URI
)
37 parser
.add_option("-n", "--net-install", help="lightweight installer mode; bundle only injector, passed interface will be downloaded at run time", action
='store_true')
38 parser
.add_option("-a", "--arch", help="add a target architecture (os-cpu)", metavar
='CPU', action
='append')
39 parser
.add_option("-v", "--verbose", help="more verbose output", action
='count')
40 parser
.add_option("-V", "--version", help="display version information", action
='store_true')
42 (options
, args
) = parser
.parse_args()
45 print "0export (zero-install) " + version
46 print "Copyright (C) 2010 Thomas Leonard"
47 print "This program comes with ABSOLUTELY NO WARRANTY,"
48 print "to the extent permitted by law."
49 print "You may redistribute copies of this program"
50 print "under the terms of the GNU General Public License."
51 print "For more information about these matters, see the file named COPYING."
56 logger
= logging
.getLogger()
57 if options
.verbose
== 1:
58 logger
.setLevel(logging
.INFO
)
60 logger
.setLevel(logging
.DEBUG
)
66 from zeroinstall
import support
67 from zeroinstall
.support
import tasks
68 from zeroinstall
.zerostore
import BadDigest
69 from zeroinstall
.injector
import model
, arch
, solver
70 from zeroinstall
.injector
.config
import load_config
71 from zeroinstall
.injector
.requirements
import Requirements
72 from zeroinstall
.injector
.driver
import Driver
73 from zeroinstall
import SafeException
77 uris
= [model
.canonical_iface_uri(x
) for x
in args
[1:]]
80 def get_arch(arch_str
):
81 if '-' not in arch_str
:
82 raise Exception("Architecture syntax is OS-CPU (e.g. 'Linux-86_64')")
83 os
, cpu
= arch_str
.split('-', 1)
84 if os
in ("*", "any"): os
= None
85 if cpu
in ("*", "any"): cpu
= None
86 return arch
.get_architecture(os
, cpu
)
87 archs
= [get_arch(a
) for a
in options
.arch
]
89 archs
= [arch
.get_host_architecture()]
91 class ExportSolver(solver
.SATSolver
):
92 def get_rating(self
, interface
, impl
, arch
):
93 # Always prefer non-native packages, as only these can be included in a bundle
94 return [not impl
.id.startswith('package:')] + solver
.SATSolver
.get_rating(self
, interface
, impl
, arch
)
96 solver
.DefaultSolver
= ExportSolver
98 config
= load_config()
100 def choose_implementations(uris
, only_feeds
):
101 interfaces_used
= set() # URI
102 feeds_used
= set() # URI
103 implementations_used
= {} # ID -> Implementation
104 uri_impls
= {} # Maps top-level URIs to implementations
107 print "Choosing versions for %s..." % uri
109 for target_arch
in archs
:
110 requirements
= Requirements(uri
)
111 driver
= Driver(config
, requirements
)
112 driver
.target_arch
= target_arch
114 # Don't let us choose local devel versions
115 driver
.solver
.extra_restrictions
= utils
.NoLocalRestrictions(uris
)
117 solved
= driver
.solve_with_downloads()
118 tasks
.wait_for_blocker(solved
)
120 if not driver
.solver
.ready
:
122 warn("Failed to select any version for %s for architecture %s" % (uri
, target_arch
))
126 feeds_used
= feeds_used | driver
.solver
.feeds_used
127 iface_cache
= config
.iface_cache
130 for iface
, impl
in driver
.solver
.selections
.items():
131 if impl
.id.startswith('package:'):
132 debug('Skip package implementation %r', impl
)
134 print " %-10s : %s (%s)" % (iface
.get_name(), impl
.get_version(), impl
.arch
or 'any arch')
135 assert impl
.digests
, "Implementation %s has no digests!" % impl
136 implementations_used
[impl
.digests
[0]] = impl
138 downloads
= driver
.download_uncached_implementations()
140 print "Downloading implementations..."
141 tasks
.wait_for_blocker(downloads
)
143 uri_impls
[uri
] = driver
.solver
.selections
[iface_cache
.get_interface(uri
)].id
145 for iface
, impl
in driver
.solver
.selections
.items():
146 interfaces_used
.add(iface
.uri
)
149 raise SafeException("Failed to select a set of versions for %s" % uri
)
151 for iface_uri
in interfaces_used
:
152 if iface_uri
.startswith('/'): continue
153 iface
= iface_cache
.get_interface(iface_uri
)
154 icon
= iface_cache
.get_icon_path(iface
)
156 download_icon
= config
.fetcher
.download_icon(iface
)
158 print "Downloading icon..."
159 tasks
.wait_for_blocker(download_icon
)
161 return (feeds_used
, implementations_used
, uri_impls
)
163 bootstrap_uris
= [options
.injector
]
166 (bootstrap_feeds_used
, bootstrap_implementations_used
, bootstrap_uri_impls
) = choose_implementations(bootstrap_uris
, False)
167 (feeds_used
, implementations_used
, uri_impls
) = choose_implementations(uris
, options
.net_install
)
171 print "Building package..."
173 tmp
= tempfile
.mkdtemp(prefix
= '0export-')
174 bootstrap_tmp
= tempfile
.mkdtemp(prefix
= '0export-')
177 utils
.export_feeds(bootstrap_tmp
, bootstrap_feeds_used | feeds_used
, keys_used
)
179 # Add implementations...
180 utils
.export_impls(bootstrap_tmp
, bootstrap_implementations_used
)
182 os
.symlink(os
.path
.join('implementations', bootstrap_uri_impls
[options
.injector
]), os
.path
.join(bootstrap_tmp
, 'zeroinstall'))
185 keys_dir
= os
.path
.join(bootstrap_tmp
, 'keys')
187 for key
in keys_used
:
188 utils
.export_key(key
, keys_dir
)
191 mydir
= os
.path
.dirname(os
.path
.abspath(sys
.argv
[0]))
193 install_code
= file(os
.path
.join(mydir
, 'install.py')).read()
194 install_code
= install_code
.replace('@ZEROINSTALL_URI@', options
.injector
)
195 install_file
= file(os
.path
.join(bootstrap_tmp
, 'install.py'), 'w')
196 install_file
.write(install_code
)
199 # Record the toplevel interfaces (not those brought in only as dependencies)
200 # These are the programs the installer will offer to run
201 toplevels
= file(os
.path
.join(bootstrap_tmp
, 'toplevel_uris'), 'w')
203 toplevels
.write(uri
+ '\n')
206 # Create an archive with bootstrap data
207 bootstrap_path
= os
.path
.join(tmp
, 'bootstrap.tar.bz2')
208 ts
= tarfile
.open(bootstrap_path
, 'w|bz2')
209 ts
.add(bootstrap_tmp
, '.')
212 # Collect all other implementations in separate archives in the archive
214 for impl
in set(implementations_used
) - set(bootstrap_implementations_used
):
215 impltmp
= tempfile
.mkdtemp(prefix
= '0export-')
217 utils
.export_impls(impltmp
, {impl
: implementations_used
[impl
]})
218 implpath
= os
.path
.join(tmp
, impl
+ '.tar.bz2')
219 ts
= tarfile
.open(implpath
, 'w|bz2')
220 ts
.add(str(os
.path
.join(impltmp
, 'implementations', impl
)), '.')
223 support
.ro_rmtree(impltmp
)
224 impl_files
.append(implpath
)
226 extract_header
= extract_header
.replace('@INSTALLER_MODE@', str(options
.net_install
))
227 extract_header
= extract_header
.replace('00000', "%05d" % len(extract_header
))
228 setup_stream
= file(setup_file
, 'wb')
229 setup_stream
.write(extract_header
)
230 setup_tar
= tarfile
.open(setup_file
, 'w|', setup_stream
)
231 setup_tar
.add(bootstrap_path
, os
.path
.basename(bootstrap_path
))
232 for impl
in impl_files
:
233 setup_tar
.add(impl
, str('implementations/' + os
.path
.basename(impl
)))
236 os
.chmod(setup_file
, (os
.stat(setup_file
).st_mode
& 0777) |
0111)
238 support
.ro_rmtree(bootstrap_tmp
)
239 support
.ro_rmtree(tmp
)
240 except BadDigest
, ex
:
241 print >>sys
.stderr
, str(ex
)
243 print >>sys
.stderr
, ex
.detail
245 except SafeException
, ex
:
246 print >>sys
.stderr
, str(ex
)