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
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
.support
import tasks
63 from zeroinstall
.zerostore
import BadDigest
64 from zeroinstall
.injector
import model
, arch
, solver
65 from zeroinstall
.injector
.config
import load_config
66 from zeroinstall
.injector
.requirements
import Requirements
67 from zeroinstall
.injector
.driver
import Driver
68 from zeroinstall
import SafeException
72 uris
= [model
.canonical_iface_uri(x
) for x
in args
[1:]]
75 def get_arch(arch_str
):
76 if '-' not in arch_str
:
77 raise Exception("Architecture syntax is OS-CPU (e.g. 'Linux-86_64')")
78 os
, cpu
= arch_str
.split('-', 1)
79 if os
in ("*", "any"): os
= None
80 if cpu
in ("*", "any"): cpu
= None
81 return arch
.get_architecture(os
, cpu
)
82 archs
= [get_arch(a
) for a
in options
.arch
]
84 archs
= [arch
.get_host_architecture()]
86 class ExportSolver(solver
.SATSolver
):
87 def get_rating(self
, interface
, impl
, arch
):
88 # Always prefer non-native packages, as only these can be included in a bundle
89 return [not impl
.id.startswith('package:')] + solver
.SATSolver
.get_rating(self
, interface
, impl
, arch
)
91 solver
.DefaultSolver
= ExportSolver
93 config
= load_config()
95 def choose_implementations(uris
, only_feeds
):
96 interfaces_used
= set() # URI
97 feeds_used
= set() # URI
98 implementations_used
= {} # ID -> Implementation
99 uri_impls
= {} # Maps top-level URIs to implementations
102 print "Choosing versions for %s..." % uri
104 for target_arch
in archs
:
105 requirements
= Requirements(uri
)
106 driver
= Driver(config
, requirements
)
107 driver
.target_arch
= target_arch
109 # Don't let us choose local devel versions
110 driver
.solver
.extra_restrictions
= utils
.NoLocalRestrictions(uris
)
112 solved
= driver
.solve_with_downloads()
113 tasks
.wait_for_blocker(solved
)
115 if not driver
.solver
.ready
:
117 warn("Failed to select any version for %s for architecture %s" % (uri
, target_arch
))
121 feeds_used
= feeds_used | driver
.solver
.feeds_used
122 iface_cache
= config
.iface_cache
125 for iface
, impl
in driver
.solver
.selections
.items():
126 if impl
.id.startswith('package:'):
127 debug('Skip package implementation %r', impl
)
129 print " %-10s : %s (%s)" % (iface
.get_name(), impl
.get_version(), impl
.arch
or 'any arch')
130 assert impl
.digests
, "Implementation %s has no digests!" % impl
131 implementations_used
[impl
.digests
[0]] = impl
133 downloads
= driver
.download_uncached_implementations()
135 print "Downloading implementations..."
136 tasks
.wait_for_blocker(downloads
)
138 uri_impls
[uri
] = driver
.solver
.selections
[iface_cache
.get_interface(uri
)].id
140 for iface
, impl
in driver
.solver
.selections
.items():
141 interfaces_used
.add(iface
.uri
)
144 raise SafeException("Failed to select a set of versions for %s" % uri
)
146 for iface_uri
in interfaces_used
:
147 if iface_uri
.startswith('/'): continue
148 iface
= iface_cache
.get_interface(iface_uri
)
149 icon
= iface_cache
.get_icon_path(iface
)
151 download_icon
= config
.fetcher
.download_icon(iface
)
153 print "Downloading icon..."
154 tasks
.wait_for_blocker(download_icon
)
156 return (feeds_used
, implementations_used
, uri_impls
)
158 bootstrap_uris
= [options
.injector
]
161 (bootstrap_feeds_used
, bootstrap_implementations_used
, bootstrap_uri_impls
) = choose_implementations(bootstrap_uris
, False)
162 (feeds_used
, implementations_used
, uri_impls
) = choose_implementations(uris
, options
.net_install
)
166 print "Building package..."
168 tmp
= tempfile
.mkdtemp(prefix
= '0export-')
169 bootstrap_tmp
= tempfile
.mkdtemp(prefix
= '0export-')
172 utils
.export_feeds(bootstrap_tmp
, bootstrap_feeds_used | feeds_used
, keys_used
)
174 # Add implementations...
175 utils
.export_impls(bootstrap_tmp
, bootstrap_implementations_used
)
177 os
.symlink(os
.path
.join('implementations', bootstrap_uri_impls
[options
.injector
]), os
.path
.join(bootstrap_tmp
, 'zeroinstall'))
180 keys_dir
= os
.path
.join(bootstrap_tmp
, 'keys')
182 for key
in keys_used
:
183 utils
.export_key(key
, keys_dir
)
186 mydir
= os
.path
.dirname(os
.path
.abspath(sys
.argv
[0]))
188 install_code
= file(os
.path
.join(mydir
, 'install.py')).read()
189 install_code
= install_code
.replace('@ZEROINSTALL_URI@', options
.injector
)
190 install_file
= file(os
.path
.join(bootstrap_tmp
, 'install.py'), 'w')
191 install_file
.write(install_code
)
194 # Record the toplevel interfaces (not those brought in only as dependencies)
195 # These are the programs the installer will offer to run
196 toplevels
= file(os
.path
.join(bootstrap_tmp
, 'toplevel_uris'), 'w')
198 toplevels
.write(uri
+ '\n')
201 # Create an archive with bootstrap data
202 bootstrap_path
= os
.path
.join(tmp
, 'bootstrap.tar.bz2')
203 ts
= tarfile
.open(bootstrap_path
, 'w|bz2')
204 ts
.add(bootstrap_tmp
, '.')
207 # Collect all other implementations in separate archives in the archive
209 for impl
in set(implementations_used
) - set(bootstrap_implementations_used
):
210 impltmp
= tempfile
.mkdtemp(prefix
= '0export-')
212 utils
.export_impls(impltmp
, {impl
: implementations_used
[impl
]})
213 implpath
= os
.path
.join(tmp
, impl
+ '.tar.bz2')
214 ts
= tarfile
.open(implpath
, 'w|bz2')
215 ts
.add(str(os
.path
.join(impltmp
, 'implementations', impl
)), '.')
218 support
.ro_rmtree(impltmp
)
219 impl_files
.append(implpath
)
221 extract_header
= extract_header
.replace('@INSTALLER_MODE@', str(options
.net_install
))
222 extract_header
= extract_header
.replace('00000', "%05d" % len(extract_header
))
223 setup_stream
= file(setup_file
, 'wb')
224 setup_stream
.write(extract_header
)
225 setup_tar
= tarfile
.open(setup_file
, 'w|', setup_stream
)
226 setup_tar
.add(bootstrap_path
, os
.path
.basename(bootstrap_path
))
227 for impl
in impl_files
:
228 setup_tar
.add(impl
, str('implementations/' + os
.path
.basename(impl
)))
231 os
.chmod(setup_file
, (os
.stat(setup_file
).st_mode
& 0777) |
0111)
233 support
.ro_rmtree(bootstrap_tmp
)
234 support
.ro_rmtree(tmp
)
235 except BadDigest
, ex
:
236 print >>sys
.stderr
, str(ex
)
238 print >>sys
.stderr
, ex
.detail
240 except SafeException
, ex
:
241 print >>sys
.stderr
, str(ex
)