1 # Copyright (C) 2006, Thomas Leonard
2 # See http://0install.net/0compile.html
4 import os
, sys
, shutil
, traceback
6 from os
.path
import join
7 from logging
import info
10 from zeroinstall
.injector
import model
, selections
, qdom
11 from zeroinstall
.injector
.arch
import canonicalize_os
, canonicalize_machine
13 from zeroinstall
.injector
.iface_cache
import iface_cache
14 from zeroinstall
import SafeException
15 from zeroinstall
.zerostore
import Store
, NotStored
19 ENV_FILE
= '0compile.properties'
20 XMLNS_0COMPILE
= 'http://zero-install.sourceforge.net/2006/namespaces/0compile'
22 zeroinstall_dir
= os
.environ
.get('0COMPILE_ZEROINSTALL', None)
24 # XXX: we're assuming that, if installed through 0install, 0launch requires
25 # the same version of Python as 0compile. This is currently needed for Arch
26 # Linux, but long-term we need to use the <runner>.
27 launch_prog
= [sys
.executable
, os
.path
.join(zeroinstall_dir
, '0launch')]
29 launch_prog
= ['0launch']
31 if os
.path
.isdir('dependencies'):
32 dep_dir
= os
.path
.realpath('dependencies')
33 iface_cache
.stores
.stores
.append(Store(dep_dir
))
34 launch_prog
+= ['--with-store', dep_dir
]
41 def is_package_impl(impl
):
42 return impl
.id.startswith("package:")
44 def lookup(impl_or_sel
):
46 if id.startswith('/'):
49 raise SafeException("Directory '%s' no longer exists. Try '0compile setup'" % id)
51 return iface_cache
.stores
.lookup_any(impl_or_sel
.digests
)
53 raise NotStored(str(ex
) + "\nHint: try '0compile setup'")
55 def ensure_dir(d
, clean
= False):
63 raise SafeException("'%s' exists, but is not a directory!" % d
)
66 def find_in_path(prog
):
67 for d
in os
.environ
['PATH'].split(':'):
68 path
= os
.path
.join(d
, prog
)
69 if os
.path
.isfile(path
):
73 def spawn_and_check(prog
, args
):
74 status
= os
.spawnv(os
.P_WAIT
, prog
, [prog
] + args
)
76 raise SafeException("Program '%s' failed with exit code %d" % (prog
, status
))
78 raise SafeException("Program '%s' failed with signal %d" % (prog
, -status
))
80 def wait_for_child(child
):
81 """Wait for child to exit and reap it. Throw an exception if it doesn't return success."""
82 pid
, status
= os
.waitpid(child
, 0)
84 if os
.WIFEXITED(status
):
85 exit_code
= os
.WEXITSTATUS(status
)
89 raise SafeException('Command failed with exit status %d' % exit_code
)
91 raise SafeException('Command failed with signal %d' % os
.WTERMSIG(status
))
93 def spawn_maybe_sandboxed(readable
, writable
, tmpdir
, prog
, args
):
98 exec_maybe_sandboxed(readable
, writable
, tmpdir
, prog
, args
)
100 traceback
.print_exc()
102 print >>sys
.stderr
, "Exec failed"
104 wait_for_child(child
)
106 def exec_maybe_sandboxed(readable
, writable
, tmpdir
, prog
, args
):
107 """execl prog, with (only) the 'writable' directories writable if sandboxing is available.
108 The readable directories will be readable, as well as various standard locations.
109 If no sandbox is available, run without a sandbox."""
111 USE_PLASH
= 'USE_PLASH_0COMPILE'
113 assert prog
.startswith('/')
114 _pola_run
= find_in_path('pola-run')
116 if _pola_run
is None:
117 print "Not using sandbox (plash not installed)"
120 use_plash
= os
.environ
.get(USE_PLASH
, '').lower() or 'not set'
121 if use_plash
in ('not set', 'false'):
122 print "Not using plash: $%s is %s" % (USE_PLASH
, use_plash
)
124 elif use_plash
== 'true':
127 raise Exception('$%s must be "true" or "false", not "%s"' % (USE_PLASH
, use_plash
))
130 os
.execlp(prog
, prog
, *args
)
132 print "Using plash to sandbox the build..."
134 # We have pola-shell :-)
135 pola_args
= ['--prog', prog
, '-B']
137 pola_args
+= ['-a', a
]
139 pola_args
+= ['-f', r
]
141 pola_args
+= ['-fw', w
]
142 pola_args
+= ['-tw', '/tmp', tmpdir
]
143 os
.environ
['TMPDIR'] = '/tmp'
144 os
.execl(_pola_run
, _pola_run
, *pola_args
)
148 target_os
= canonicalize_os(uname
[0])
149 target_machine
= canonicalize_machine(uname
[4])
150 if target_os
== 'Darwin' and target_machine
== 'i386':
151 # this system detection shell script comes from config.guess (20090918):
152 CC
= os
.getenv("CC_FOR_BUILD") or os
.getenv("CC") or os
.getenv("HOST_CC") or "cc"
153 process
= subprocess
.Popen("(echo '#ifdef __LP64__'; echo IS_64BIT_ARCH; echo '#endif') | " +
154 "(CCOPTS= %s -E - 2>/dev/null) | " % CC
+
155 "grep IS_64BIT_ARCH >/dev/null", stdout
=subprocess
.PIPE
, shell
=True)
156 output
, error
= process
.communicate()
157 retcode
= process
.poll()
159 target_machine
='x86_64'
160 if target_machine
in ('i585', 'i686'):
161 target_machine
= 'i486' # (sensible default)
162 return target_os
+ '-' + target_machine
165 def __init__(self
, need_config
= True):
166 if need_config
and not os
.path
.isfile(ENV_FILE
):
167 raise SafeException("Run 0compile from a directory containing a '%s' file" % ENV_FILE
)
169 self
.config
= ConfigParser
.RawConfigParser()
170 self
.config
.add_section('compile')
171 self
.config
.set('compile', 'download-base-url', '')
172 self
.config
.set('compile', 'version-modifier', '')
173 self
.config
.set('compile', 'interface', '')
174 self
.config
.set('compile', 'selections', '')
175 self
.config
.set('compile', 'metadir', '0install')
176 self
.config
.set('compile', 'distdir', '')
178 self
.config
.read(ENV_FILE
)
180 self
._selections
= None
185 def iface_name(self
):
186 iface_name
= os
.path
.basename(self
.interface
)
187 if iface_name
.endswith('.xml'):
188 iface_name
= iface_name
[:-4]
189 iface_name
= iface_name
.replace(' ', '-')
190 if iface_name
.endswith('-src'):
191 iface_name
= iface_name
[:-4]
194 interface
= property(lambda self
: model
.canonical_iface_uri(self
.config
.get('compile', 'interface')))
198 distdir_name
= self
.config
.get('compile', 'distdir')
200 arch
= self
.target_arch
.replace('*', 'any')
201 distdir_name
= self
.iface_name
.lower()
202 distdir_name
+= '-' + arch
.lower()
203 assert '/' not in distdir_name
204 return os
.path
.realpath(distdir_name
)
206 def get_binary_template(self
):
207 """Find the <compile:implementation> element for the selected compile command, if any"""
208 sels
= self
.get_selections()
210 for elem
in sels
.commands
[0].qdom
.childNodes
:
211 if elem
.uri
== XMLNS_0COMPILE
and elem
.name
== 'implementation':
214 # XXX: hack for 0launch < 0.54 which messes up the namespace
215 if elem
.name
== 'implementation':
221 metadir
= self
.config
.get('compile', 'metadir')
222 assert not metadir
.startswith('/')
223 return join(self
.distdir
, metadir
)
226 def local_iface_file(self
):
227 return join(self
.metadir
, self
.iface_name
+ '.xml')
230 def target_arch(self
):
231 temp
= self
.get_binary_template()
232 arch
= temp
and temp
.getAttribute('arch')
233 return arch
or get_arch_name()
236 def version_modifier(self
):
237 vm
= self
.config
.get('compile', 'version-modifier')
244 def archive_stem(self
):
245 # Use the version that we actually built, not the version we would build now
246 feed
= self
.load_built_feed()
247 assert len(feed
.implementations
) == 1
248 version
= feed
.implementations
.values()[0].get_version()
250 # Don't use the feed's name, as it may contain the version number
251 name
= feed
.get_name().lower().replace(' ', '-')
252 arch
= self
.target_arch
.lower().replace('*-*', 'bin').replace('*', 'any')
254 return '%s-%s-%s' % (name
, arch
, version
)
256 def load_built_feed(self
):
257 path
= self
.local_iface_file
260 feed
= model
.ZeroInstallFeed(qdom
.parse(stream
), local_path
= path
)
265 def load_built_selections(self
):
266 path
= join(self
.metadir
, 'build-environment.xml')
267 if os
.path
.exists(path
):
270 return selections
.Selections(qdom
.parse(stream
))
276 def download_base_url(self
):
277 return self
.config
.get('compile', 'download-base-url')
279 def chosen_impl(self
, uri
):
280 sels
= self
.get_selections()
281 assert uri
in sels
.selections
282 return sels
.selections
[uri
]
285 def local_download_iface(self
):
286 impl
, = self
.load_built_feed().implementations
.values()
287 return '%s-%s.xml' % (self
.iface_name
, impl
.get_version())
290 stream
= file(ENV_FILE
, 'w')
292 self
.config
.write(stream
)
296 def get_selections(self
, prompt
= False):
299 return self
._selections
301 selections_file
= self
.config
.get('compile', 'selections')
304 raise SafeException("Selections are fixed by %s" % selections_file
)
305 stream
= file(selections_file
)
307 self
._selections
= selections
.Selections(qdom
.parse(stream
))
310 from zeroinstall
.injector
import handler
, policy
312 h
= handler
.ConsoleHandler()
314 h
= handler
.Handler()
315 config
= policy
.load_config(h
)
316 blocker
= self
._selections
.download_missing(config
)
318 print "Waiting for selected implementations to be downloaded..."
319 h
.wait_for_blocker(blocker
)
322 if prompt
and '--console' not in launch_prog
:
323 options
.append('--gui')
324 child
= subprocess
.Popen(launch_prog
+ ['--source', '--get-selections'] + options
+ [self
.interface
], stdout
= subprocess
.PIPE
)
326 self
._selections
= selections
.Selections(qdom
.parse(child
.stdout
))
329 raise SafeException("0launch --get-selections failed (exit code %d)" % child
.returncode
)
331 self
.root_impl
= self
._selections
.selections
[self
.interface
]
333 self
.orig_srcdir
= os
.path
.realpath(lookup(self
.root_impl
))
334 self
.user_srcdir
= None
336 if os
.path
.isdir('src'):
337 self
.user_srcdir
= os
.path
.realpath('src')
338 if self
.user_srcdir
== self
.orig_srcdir
or \
339 self
.user_srcdir
.startswith(self
.orig_srcdir
+ '/') or \
340 self
.orig_srcdir
.startswith(self
.user_srcdir
+ '/'):
341 info("Ignoring 'src' directory because it coincides with %s",
343 self
.user_srcdir
= None
345 return self
._selections
347 def get_build_changes(self
):
348 sels
= self
.get_selections()
349 old_sels
= self
.load_built_selections()
352 # See if things have changed since the last build
353 all_ifaces
= set(sels
.selections
) |
set(old_sels
.selections
)
355 old_impl
= old_sels
.selections
.get(x
, no_impl
)
356 new_impl
= sels
.selections
.get(x
, no_impl
)
357 if old_impl
.version
!= new_impl
.version
:
358 changes
.append("Version change for %s: %s -> %s" % (x
, old_impl
.version
, new_impl
.version
))
359 elif old_impl
.id != new_impl
.id:
360 changes
.append("Version change for %s: %s -> %s" % (x
, old_impl
.id, new_impl
.id))
364 root
= node
.ownerDocument
.documentElement
366 while node
and node
is not root
:
367 node
= node
.parentNode
372 if s
== 'true': return True
373 if s
== 'false': return False
374 raise SafeException('Expected "true" or "false" but got "%s"' % s
)
377 # Copied from 0launch 0.54 (remove once 0.54 is released)
378 def __init__(self
, default_ns
):
380 self
.default_ns
= default_ns
383 prefix
= self
.prefixes
.get(ns
, None)
386 prefix
= 'ns%d' % len(self
.prefixes
)
387 self
.prefixes
[ns
] = prefix
390 def setAttributeNS(self
, elem
, uri
, localName
, value
):
392 elem
.setAttributeNS(None, localName
, value
)
394 elem
.setAttributeNS(uri
, self
.get(uri
) + ':' + localName
, value
)
396 def createElementNS(self
, doc
, uri
, localName
):
397 if uri
== self
.default_ns
:
398 return doc
.createElementNS(uri
, localName
)
400 return doc
.createElementNS(uri
, self
.get(uri
) + ':' + localName
)