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
87 archs
= [get_arch(a
) for a
in options
.arch
]
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 if target_arch
is not None:
113 requirements
.os
= target_arch
[0]
114 requirements
.cpu
= target_arch
[1]
116 # Don't let us choose local devel versions
117 driver
.solver
.extra_restrictions
= utils
.NoLocalRestrictions(uris
)
119 solved
= driver
.solve_with_downloads()
120 tasks
.wait_for_blocker(solved
)
122 if not driver
.solver
.ready
:
124 warn("Failed to select any version for %s for architecture %s" % (uri
, target_arch
))
128 feeds_used
= feeds_used | driver
.solver
.feeds_used
129 iface_cache
= config
.iface_cache
132 for iface
, impl
in driver
.solver
.selections
.items():
133 if impl
.id.startswith('package:'):
134 debug('Skip package implementation %r', impl
)
136 print " %-10s : %s (%s)" % (iface
.get_name(), impl
.get_version(), impl
.arch
or 'any arch')
137 assert impl
.digests
, "Implementation %s has no digests!" % impl
138 implementations_used
[impl
.digests
[0]] = impl
140 downloads
= driver
.download_uncached_implementations()
142 print "Downloading implementations..."
143 tasks
.wait_for_blocker(downloads
)
145 uri_impls
[uri
] = driver
.solver
.selections
[iface_cache
.get_interface(uri
)].id
147 for iface
, impl
in driver
.solver
.selections
.items():
148 interfaces_used
.add(iface
.uri
)
151 raise SafeException("Failed to select a set of versions for %s" % uri
)
153 for iface_uri
in interfaces_used
:
154 if iface_uri
.startswith('/'): continue
155 iface
= iface_cache
.get_interface(iface_uri
)
156 icon
= iface_cache
.get_icon_path(iface
)
158 download_icon
= config
.fetcher
.download_icon(iface
)
160 print "Downloading icon..."
161 tasks
.wait_for_blocker(download_icon
)
163 return (feeds_used
, implementations_used
, uri_impls
)
165 bootstrap_uris
= [options
.injector
]
168 (bootstrap_feeds_used
, bootstrap_implementations_used
, bootstrap_uri_impls
) = choose_implementations(bootstrap_uris
, False)
169 (feeds_used
, implementations_used
, uri_impls
) = choose_implementations(uris
, options
.net_install
)
173 print "Building package..."
175 tmp
= tempfile
.mkdtemp(prefix
= '0export-')
176 bootstrap_tmp
= tempfile
.mkdtemp(prefix
= '0export-')
179 utils
.export_feeds(bootstrap_tmp
, bootstrap_feeds_used | feeds_used
, keys_used
)
181 # Add implementations...
182 utils
.export_impls(bootstrap_tmp
, bootstrap_implementations_used
)
184 os
.symlink(os
.path
.join('implementations', bootstrap_uri_impls
[options
.injector
]), os
.path
.join(bootstrap_tmp
, 'zeroinstall'))
187 keys_dir
= os
.path
.join(bootstrap_tmp
, 'keys')
189 for key
in keys_used
:
190 utils
.export_key(key
, keys_dir
)
193 mydir
= os
.path
.dirname(os
.path
.abspath(sys
.argv
[0]))
195 install_code
= file(os
.path
.join(mydir
, 'install.py')).read()
196 install_file
= file(os
.path
.join(bootstrap_tmp
, 'install.py'), 'w')
197 install_file
.write(install_code
)
200 # Record the toplevel interfaces (not those brought in only as dependencies)
201 # These are the programs the installer will offer to run
202 toplevels
= file(os
.path
.join(bootstrap_tmp
, 'toplevel_uris'), 'w')
204 toplevels
.write(uri
+ '\n')
207 # Create an archive with bootstrap data
208 bootstrap_path
= os
.path
.join(tmp
, 'bootstrap.tar.bz2')
209 ts
= tarfile
.open(bootstrap_path
, 'w|bz2')
210 ts
.add(bootstrap_tmp
, '.')
213 # Collect all other implementations in separate archives in the archive
215 for impl
in set(implementations_used
) - set(bootstrap_implementations_used
):
216 impltmp
= tempfile
.mkdtemp(prefix
= '0export-')
218 utils
.export_impls(impltmp
, {impl
: implementations_used
[impl
]})
219 implpath
= os
.path
.join(tmp
, impl
+ '.tar.bz2')
220 ts
= tarfile
.open(implpath
, 'w|bz2')
221 ts
.add(str(os
.path
.join(impltmp
, 'implementations', impl
)), '.')
224 support
.ro_rmtree(impltmp
)
225 impl_files
.append(implpath
)
227 extract_header
= extract_header
.replace('@INSTALLER_MODE@', str(options
.net_install
))
228 extract_header
= extract_header
.replace('00000', "%05d" % len(extract_header
))
229 setup_stream
= file(setup_file
, 'wb')
230 setup_stream
.write(extract_header
)
231 setup_tar
= tarfile
.open(setup_file
, 'w|', setup_stream
)
232 setup_tar
.add(bootstrap_path
, os
.path
.basename(bootstrap_path
))
233 for impl
in impl_files
:
234 setup_tar
.add(impl
, str('implementations/' + os
.path
.basename(impl
)))
237 os
.chmod(setup_file
, (os
.stat(setup_file
).st_mode
& 0777) |
0111)
239 support
.ro_rmtree(bootstrap_tmp
)
240 support
.ro_rmtree(tmp
)
241 except BadDigest
, ex
:
242 print >>sys
.stderr
, str(ex
)
244 print >>sys
.stderr
, ex
.detail
246 except SafeException
, ex
:
247 print >>sys
.stderr
, str(ex
)