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 launch_prog
= [os
.path
.join(zeroinstall_dir
, '0launch')]
26 launch_prog
= ['0launch']
28 if os
.path
.isdir('dependencies'):
29 dep_dir
= os
.path
.realpath('dependencies')
30 iface_cache
.stores
.stores
.append(Store(dep_dir
))
31 launch_prog
+= ['--with-store', dep_dir
]
38 def is_package_impl(impl
):
39 return impl
.id.startswith("package:")
41 def lookup(impl_or_sel
):
43 if id.startswith('/'):
46 raise SafeException("Directory '%s' no longer exists. Try '0compile setup'" % id)
48 return iface_cache
.stores
.lookup_any(impl_or_sel
.digests
)
50 raise NotStored(str(ex
) + "\nHint: try '0compile setup'")
52 def ensure_dir(d
, clean
= False):
60 raise SafeException("'%s' exists, but is not a directory!" % d
)
63 def find_in_path(prog
):
64 for d
in os
.environ
['PATH'].split(':'):
65 path
= os
.path
.join(d
, prog
)
66 if os
.path
.isfile(path
):
70 def spawn_and_check(prog
, args
):
71 status
= os
.spawnv(os
.P_WAIT
, prog
, [prog
] + args
)
73 raise SafeException("Program '%s' failed with exit code %d" % (prog
, status
))
75 raise SafeException("Program '%s' failed with signal %d" % (prog
, -status
))
77 def wait_for_child(child
):
78 """Wait for child to exit and reap it. Throw an exception if it doesn't return success."""
79 pid
, status
= os
.waitpid(child
, 0)
81 if os
.WIFEXITED(status
):
82 exit_code
= os
.WEXITSTATUS(status
)
86 raise SafeException('Command failed with exit status %d' % exit_code
)
88 raise SafeException('Command failed with signal %d' % os
.WTERMSIG(status
))
90 def spawn_maybe_sandboxed(readable
, writable
, tmpdir
, prog
, args
):
95 exec_maybe_sandboxed(readable
, writable
, tmpdir
, prog
, args
)
99 print >>sys
.stderr
, "Exec failed"
101 wait_for_child(child
)
103 def exec_maybe_sandboxed(readable
, writable
, tmpdir
, prog
, args
):
104 """execl prog, with (only) the 'writable' directories writable if sandboxing is available.
105 The readable directories will be readable, as well as various standard locations.
106 If no sandbox is available, run without a sandbox."""
108 USE_PLASH
= 'USE_PLASH_0COMPILE'
110 assert prog
.startswith('/')
111 _pola_run
= find_in_path('pola-run')
113 if _pola_run
is None:
114 print "Not using sandbox (plash not installed)"
117 use_plash
= os
.environ
.get(USE_PLASH
, '').lower() or 'not set'
118 if use_plash
in ('not set', 'false'):
119 print "Not using plash: $%s is %s" % (USE_PLASH
, use_plash
)
121 elif use_plash
== 'true':
124 raise Exception('$%s must be "true" or "false", not "%s"' % (USE_PLASH
, use_plash
))
127 os
.execlp(prog
, prog
, *args
)
129 print "Using plash to sandbox the build..."
131 # We have pola-shell :-)
132 pola_args
= ['--prog', prog
, '-B']
134 pola_args
+= ['-a', a
]
136 pola_args
+= ['-f', r
]
138 pola_args
+= ['-fw', w
]
139 pola_args
+= ['-tw', '/tmp', tmpdir
]
140 os
.environ
['TMPDIR'] = '/tmp'
141 os
.execl(_pola_run
, _pola_run
, *pola_args
)
145 target_os
= canonicalize_os(uname
[0])
146 target_machine
= canonicalize_machine(uname
[4])
147 if target_os
== 'Darwin' and target_machine
== 'i386':
148 # this system detection shell script comes from config.guess (20090918):
149 CC
= os
.getenv("CC_FOR_BUILD") or os
.getenv("CC") or os
.getenv("HOST_CC") or "cc"
150 process
= subprocess
.Popen("(echo '#ifdef __LP64__'; echo IS_64BIT_ARCH; echo '#endif') | " +
151 "(CCOPTS= %s -E - 2>/dev/null) | " % CC
+
152 "grep IS_64BIT_ARCH >/dev/null", stdout
=subprocess
.PIPE
, shell
=True)
153 output
, error
= process
.communicate()
154 retcode
= process
.poll()
156 target_machine
='x86_64'
157 if target_machine
in ('i585', 'i686'):
158 target_machine
= 'i486' # (sensible default)
159 return target_os
+ '-' + target_machine
162 def __init__(self
, need_config
= True):
163 if need_config
and not os
.path
.isfile(ENV_FILE
):
164 raise SafeException("Run 0compile from a directory containing a '%s' file" % ENV_FILE
)
166 self
.config
= ConfigParser
.RawConfigParser()
167 self
.config
.add_section('compile')
168 self
.config
.set('compile', 'download-base-url', '')
169 self
.config
.set('compile', 'version-modifier', '')
170 self
.config
.set('compile', 'interface', '')
171 self
.config
.set('compile', 'selections', '')
172 self
.config
.set('compile', 'metadir', '0install')
173 self
.config
.set('compile', 'distdir', '')
175 self
.config
.read(ENV_FILE
)
177 self
._selections
= None
182 def iface_name(self
):
183 iface_name
= os
.path
.basename(self
.interface
)
184 if iface_name
.endswith('.xml'):
185 iface_name
= iface_name
[:-4]
186 iface_name
= iface_name
.replace(' ', '-')
187 if iface_name
.endswith('-src'):
188 iface_name
= iface_name
[:-4]
191 interface
= property(lambda self
: model
.canonical_iface_uri(self
.config
.get('compile', 'interface')))
195 distdir_name
= self
.config
.get('compile', 'distdir')
197 arch
= self
.target_arch
.replace('*', 'any')
198 distdir_name
= self
.iface_name
.lower()
199 distdir_name
+= '-' + arch
.lower()
200 assert '/' not in distdir_name
201 return os
.path
.realpath(distdir_name
)
203 def get_binary_template(self
):
204 """Find the <compile:implementation> element for the selected compile command, if any"""
205 sels
= self
.get_selections()
207 for elem
in sels
.commands
[0].qdom
.childNodes
:
208 if elem
.uri
== XMLNS_0COMPILE
and elem
.name
== 'implementation':
211 # XXX: hack for 0launch < 0.54 which messes up the namespace
212 if elem
.name
== 'implementation':
218 metadir
= self
.config
.get('compile', 'metadir')
219 assert not metadir
.startswith('/')
220 return join(self
.distdir
, metadir
)
223 def local_iface_file(self
):
224 return join(self
.metadir
, self
.iface_name
+ '.xml')
227 def target_arch(self
):
228 temp
= self
.get_binary_template()
229 arch
= temp
and temp
.getAttribute('arch')
230 return arch
or get_arch_name()
233 def version_modifier(self
):
234 vm
= self
.config
.get('compile', 'version-modifier')
241 def archive_stem(self
):
242 # Use the version that we actually built, not the version we would build now
243 feed
= self
.load_built_feed()
244 assert len(feed
.implementations
) == 1
245 version
= feed
.implementations
.values()[0].get_version()
247 # Don't use the feed's name, as it may contain the version number
248 name
= feed
.get_name().lower().replace(' ', '-')
250 return '%s-%s-%s' % (name
, self
.target_arch
.lower(), version
)
252 def load_built_feed(self
):
253 path
= self
.local_iface_file
256 feed
= model
.ZeroInstallFeed(qdom
.parse(stream
), local_path
= path
)
261 def load_built_selections(self
):
262 path
= join(self
.metadir
, 'build-environment.xml')
263 if os
.path
.exists(path
):
266 return selections
.Selections(qdom
.parse(stream
))
272 def download_base_url(self
):
273 return self
.config
.get('compile', 'download-base-url')
275 def chosen_impl(self
, uri
):
276 sels
= self
.get_selections()
277 assert uri
in sels
.selections
278 return sels
.selections
[uri
]
281 def local_download_iface(self
):
282 impl
, = self
.load_built_feed().implementations
.values()
283 return '%s-%s.xml' % (self
.iface_name
, impl
.get_version())
286 stream
= file(ENV_FILE
, 'w')
288 self
.config
.write(stream
)
292 def get_selections(self
, prompt
= False):
295 return self
._selections
297 selections_file
= self
.config
.get('compile', 'selections')
300 raise SafeException("Selections are fixed by %s" % selections_file
)
301 stream
= file(selections_file
)
303 self
._selections
= selections
.Selections(qdom
.parse(stream
))
306 from zeroinstall
.injector
import handler
, policy
308 h
= handler
.ConsoleHandler()
310 h
= handler
.Handler()
311 config
= policy
.load_config(h
)
312 blocker
= self
._selections
.download_missing(config
)
314 print "Waiting for selected implementations to be downloaded..."
315 h
.wait_for_blocker(blocker
)
318 if prompt
and '--console' not in launch_prog
:
319 options
.append('--gui')
320 child
= subprocess
.Popen(launch_prog
+ ['--source', '--get-selections'] + options
+ [self
.interface
], stdout
= subprocess
.PIPE
)
322 self
._selections
= selections
.Selections(qdom
.parse(child
.stdout
))
325 raise SafeException("0launch --get-selections failed (exit code %d)" % child
.returncode
)
327 self
.root_impl
= self
._selections
.selections
[self
.interface
]
329 self
.orig_srcdir
= os
.path
.realpath(lookup(self
.root_impl
))
330 self
.user_srcdir
= None
332 if os
.path
.isdir('src'):
333 self
.user_srcdir
= os
.path
.realpath('src')
334 if self
.user_srcdir
== self
.orig_srcdir
or \
335 self
.user_srcdir
.startswith(self
.orig_srcdir
+ '/') or \
336 self
.orig_srcdir
.startswith(self
.user_srcdir
+ '/'):
337 info("Ignoring 'src' directory because it coincides with %s",
339 self
.user_srcdir
= None
341 return self
._selections
343 def get_build_changes(self
):
344 sels
= self
.get_selections()
345 old_sels
= self
.load_built_selections()
348 # See if things have changed since the last build
349 all_ifaces
= set(sels
.selections
) |
set(old_sels
.selections
)
351 old_impl
= old_sels
.selections
.get(x
, no_impl
)
352 new_impl
= sels
.selections
.get(x
, no_impl
)
353 if old_impl
.version
!= new_impl
.version
:
354 changes
.append("Version change for %s: %s -> %s" % (x
, old_impl
.version
, new_impl
.version
))
355 elif old_impl
.id != new_impl
.id:
356 changes
.append("Version change for %s: %s -> %s" % (x
, old_impl
.id, new_impl
.id))
360 root
= node
.ownerDocument
.documentElement
362 while node
and node
is not root
:
363 node
= node
.parentNode
368 if s
== 'true': return True
369 if s
== 'false': return False
370 raise SafeException('Expected "true" or "false" but got "%s"' % s
)
373 # Copied from 0launch 0.54 (remove once 0.54 is released)
374 def __init__(self
, default_ns
):
376 self
.default_ns
= default_ns
379 prefix
= self
.prefixes
.get(ns
, None)
382 prefix
= 'ns%d' % len(self
.prefixes
)
383 self
.prefixes
[ns
] = prefix
386 def setAttributeNS(self
, elem
, uri
, localName
, value
):
388 elem
.setAttributeNS(None, localName
, value
)
390 elem
.setAttributeNS(uri
, self
.get(uri
) + ':' + localName
, value
)
392 def createElementNS(self
, doc
, uri
, localName
):
393 if uri
== self
.default_ns
:
394 return doc
.createElementNS(uri
, localName
)
396 return doc
.createElementNS(uri
, self
.get(uri
) + ':' + localName
)