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)
13 sys
.path
.insert(1, zeroinstall_dir
)
17 extract_header
= """#!/bin/sh
20 archive_format=application/x-tar
22 exec python - "$0" "$archive_offset" "$@" << EOF
23 """ + file(os
.path
.join(os
.path
.dirname(__file__
), 'unpacker.py')).read() + """
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()
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."
51 logger
= logging
.getLogger()
52 if options
.verbose
== 1:
53 logger
.setLevel(logging
.INFO
)
55 logger
.setLevel(logging
.DEBUG
)
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
68 uris
= [model
.canonical_iface_uri(x
) for x
in args
[1:]]
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
]
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:'))
87 return solver
.SATSolver
.compare(self
, interface
, b
, a
, arch
)
89 solver
.DefaultSolver
= ExportSolver
91 def choose_implementations(uris
, only_feeds
):
94 interfaces_used
= set() # URI
95 feeds_used
= set() # URI
96 implementations_used
= {} # ID -> Implementation
97 uri_impls
= {} # Maps top-level URIs to implementations
100 print "Choosing versions for %s..." % uri
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
)
114 warn("Failed to select any version for %s for architecture %s" % (uri
, target_arch
))
118 feeds_used
= feeds_used | p
.solver
.feeds_used
119 iface_cache
= p
.solver
.iface_cache
122 for iface
, impl
in p
.solver
.selections
.items():
123 if impl
.id.startswith('package:'):
124 debug('Skip package implementation %r', impl
)
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()
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
)
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
)
149 download_icon
= p
.fetcher
.download_icon(iface
)
151 print "Downloading icon..."
152 h
.wait_for_blocker(download_icon
)
154 return (feeds_used
, implementations_used
, uri_impls
)
156 bootstrap_uris
= [options
.injector
]
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
)
164 print "Building package..."
166 tmp
= tempfile
.mkdtemp(prefix
= '0export-')
167 bootstrap_tmp
= tempfile
.mkdtemp(prefix
= '0export-')
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'))
178 keys_dir
= os
.path
.join(bootstrap_tmp
, 'keys')
180 for key
in keys_used
:
181 utils
.export_key(key
, keys_dir
)
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
)
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')
196 toplevels
.write(uri
+ '\n')
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
, '.')
205 # Collect all other implementations in separate archives in the archive
207 for impl
in set(implementations_used
) - set(bootstrap_implementations_used
):
208 impltmp
= tempfile
.mkdtemp(prefix
= '0export-')
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
)), '.')
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
)))
229 os
.chmod(setup_file
, (os
.stat(setup_file
).st_mode
& 0777) |
0111)
231 support
.ro_rmtree(bootstrap_tmp
)
232 support
.ro_rmtree(tmp
)
233 except BadDigest
, ex
:
234 print >>sys
.stderr
, str(ex
)
236 print >>sys
.stderr
, ex
.detail
238 except SafeException
, ex
:
239 print >>sys
.stderr
, str(ex
)