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('package:'):
48 if id.startswith('/'):
51 raise SafeException("Directory '%s' no longer exists. Try '0compile setup'" % id)
53 return iface_cache
.stores
.lookup_any(impl_or_sel
.digests
)
55 raise NotStored(str(ex
) + "\nHint: try '0compile setup'")
57 def ensure_dir(d
, clean
= False):
65 raise SafeException("'%s' exists, but is not a directory!" % d
)
68 def find_in_path(prog
):
69 for d
in os
.environ
['PATH'].split(':'):
70 path
= os
.path
.join(d
, prog
)
71 if os
.path
.isfile(path
):
75 def spawn_and_check(prog
, args
):
76 status
= os
.spawnv(os
.P_WAIT
, prog
, [prog
] + args
)
78 raise SafeException("Program '%s' failed with exit code %d" % (prog
, status
))
80 raise SafeException("Program '%s' failed with signal %d" % (prog
, -status
))
82 def wait_for_child(child
):
83 """Wait for child to exit and reap it. Throw an exception if it doesn't return success."""
84 pid
, status
= os
.waitpid(child
, 0)
86 if os
.WIFEXITED(status
):
87 exit_code
= os
.WEXITSTATUS(status
)
91 raise SafeException('Command failed with exit status %d' % exit_code
)
93 raise SafeException('Command failed with signal %d' % os
.WTERMSIG(status
))
95 def spawn_maybe_sandboxed(readable
, writable
, tmpdir
, prog
, args
):
100 exec_maybe_sandboxed(readable
, writable
, tmpdir
, prog
, args
)
102 traceback
.print_exc()
104 print >>sys
.stderr
, "Exec failed"
106 wait_for_child(child
)
108 def exec_maybe_sandboxed(readable
, writable
, tmpdir
, prog
, args
):
109 """execl prog, with (only) the 'writable' directories writable if sandboxing is available.
110 The readable directories will be readable, as well as various standard locations.
111 If no sandbox is available, run without a sandbox."""
113 USE_PLASH
= 'USE_PLASH_0COMPILE'
115 assert prog
.startswith('/')
116 _pola_run
= find_in_path('pola-run')
118 if _pola_run
is None:
119 print "Not using sandbox (plash not installed)"
122 use_plash
= os
.environ
.get(USE_PLASH
, '').lower() or 'not set'
123 if use_plash
in ('not set', 'false'):
124 print "Not using plash: $%s is %s" % (USE_PLASH
, use_plash
)
126 elif use_plash
== 'true':
129 raise Exception('$%s must be "true" or "false", not "%s"' % (USE_PLASH
, use_plash
))
132 os
.execlp(prog
, prog
, *args
)
134 print "Using plash to sandbox the build..."
136 # We have pola-shell :-)
137 pola_args
= ['--prog', prog
, '-B']
139 pola_args
+= ['-a', a
]
141 pola_args
+= ['-f', r
]
143 pola_args
+= ['-fw', w
]
144 pola_args
+= ['-tw', '/tmp', tmpdir
]
145 os
.environ
['TMPDIR'] = '/tmp'
146 os
.execl(_pola_run
, _pola_run
, *pola_args
)
150 target_os
= canonicalize_os(uname
[0])
151 target_machine
= canonicalize_machine(uname
[4])
152 if target_os
== 'Darwin' and target_machine
== 'i386':
153 # this system detection shell script comes from config.guess (20090918):
154 CC
= os
.getenv("CC_FOR_BUILD") or os
.getenv("CC") or os
.getenv("HOST_CC") or "cc"
155 process
= subprocess
.Popen("(echo '#ifdef __LP64__'; echo IS_64BIT_ARCH; echo '#endif') | " +
156 "(CCOPTS= %s -E - 2>/dev/null) | " % CC
+
157 "grep IS_64BIT_ARCH >/dev/null", stdout
=subprocess
.PIPE
, shell
=True)
158 output
, error
= process
.communicate()
159 retcode
= process
.poll()
161 target_machine
='x86_64'
162 if target_machine
in ('i585', 'i686'):
163 target_machine
= 'i486' # (sensible default)
164 return target_os
+ '-' + target_machine
167 def __init__(self
, need_config
= True):
168 if need_config
and not os
.path
.isfile(ENV_FILE
):
169 raise SafeException("Run 0compile from a directory containing a '%s' file" % ENV_FILE
)
171 self
.config
= ConfigParser
.RawConfigParser()
172 self
.config
.add_section('compile')
173 self
.config
.set('compile', 'download-base-url', '')
174 self
.config
.set('compile', 'version-modifier', '')
175 self
.config
.set('compile', 'interface', '')
176 self
.config
.set('compile', 'selections', '')
177 self
.config
.set('compile', 'metadir', '0install')
178 self
.config
.set('compile', 'distdir', '')
180 self
.config
.read(ENV_FILE
)
182 self
._selections
= None
187 def iface_name(self
):
188 iface_name
= os
.path
.basename(self
.interface
)
189 if iface_name
.endswith('.xml'):
190 iface_name
= iface_name
[:-4]
191 iface_name
= iface_name
.replace(' ', '-')
192 if iface_name
.endswith('-src'):
193 iface_name
= iface_name
[:-4]
196 interface
= property(lambda self
: model
.canonical_iface_uri(self
.config
.get('compile', 'interface')))
200 distdir_name
= self
.config
.get('compile', 'distdir')
202 arch
= self
.target_arch
.replace('*', 'any')
203 distdir_name
= self
.iface_name
.lower()
204 distdir_name
+= '-' + arch
.lower()
205 assert '/' not in distdir_name
206 return os
.path
.realpath(distdir_name
)
208 def get_binary_template(self
):
209 """Find the <compile:implementation> element for the selected compile command, if any"""
210 sels
= self
.get_selections()
212 for elem
in sels
.commands
[0].qdom
.childNodes
:
213 if elem
.uri
== XMLNS_0COMPILE
and elem
.name
== 'implementation':
216 # XXX: hack for 0launch < 0.54 which messes up the namespace
217 if elem
.name
== 'implementation':
223 metadir
= self
.config
.get('compile', 'metadir')
224 assert not metadir
.startswith('/')
225 return join(self
.distdir
, metadir
)
228 def local_iface_file(self
):
229 return join(self
.metadir
, self
.iface_name
+ '.xml')
232 def target_arch(self
):
233 temp
= self
.get_binary_template()
234 arch
= temp
and temp
.getAttribute('arch')
235 return arch
or get_arch_name()
238 def version_modifier(self
):
239 vm
= self
.config
.get('compile', 'version-modifier')
246 def archive_stem(self
):
247 # Use the version that we actually built, not the version we would build now
248 feed
= self
.load_built_feed()
249 assert len(feed
.implementations
) == 1
250 version
= feed
.implementations
.values()[0].get_version()
252 # Don't use the feed's name, as it may contain the version number
253 name
= feed
.get_name().lower().replace(' ', '-')
254 arch
= self
.target_arch
.lower().replace('*-*', 'bin').replace('*', 'any')
256 return '%s-%s-%s' % (name
, arch
, version
)
258 def load_built_feed(self
):
259 path
= self
.local_iface_file
262 feed
= model
.ZeroInstallFeed(qdom
.parse(stream
), local_path
= path
)
267 def load_built_selections(self
):
268 path
= join(self
.metadir
, 'build-environment.xml')
269 if os
.path
.exists(path
):
272 return selections
.Selections(qdom
.parse(stream
))
278 def download_base_url(self
):
279 return self
.config
.get('compile', 'download-base-url')
281 def chosen_impl(self
, uri
):
282 sels
= self
.get_selections()
283 assert uri
in sels
.selections
284 return sels
.selections
[uri
]
287 def local_download_iface(self
):
288 impl
, = self
.load_built_feed().implementations
.values()
289 return '%s-%s.xml' % (self
.iface_name
, impl
.get_version())
292 stream
= file(ENV_FILE
, 'w')
294 self
.config
.write(stream
)
298 def get_selections(self
, prompt
= False):
301 return self
._selections
303 selections_file
= self
.config
.get('compile', 'selections')
306 raise SafeException("Selections are fixed by %s" % selections_file
)
307 stream
= file(selections_file
)
309 self
._selections
= selections
.Selections(qdom
.parse(stream
))
312 from zeroinstall
.injector
import handler
, policy
314 h
= handler
.ConsoleHandler()
316 h
= handler
.Handler()
317 config
= policy
.load_config(h
)
318 blocker
= self
._selections
.download_missing(config
)
320 print "Waiting for selected implementations to be downloaded..."
321 h
.wait_for_blocker(blocker
)
324 if prompt
and '--console' not in launch_prog
:
325 options
.append('--gui')
326 child
= subprocess
.Popen(launch_prog
+ ['--source', '--get-selections'] + options
+ [self
.interface
], stdout
= subprocess
.PIPE
)
328 self
._selections
= selections
.Selections(qdom
.parse(child
.stdout
))
331 raise SafeException("0launch --get-selections failed (exit code %d)" % child
.returncode
)
333 self
.root_impl
= self
._selections
.selections
[self
.interface
]
335 self
.orig_srcdir
= os
.path
.realpath(lookup(self
.root_impl
))
336 self
.user_srcdir
= None
338 if os
.path
.isdir('src'):
339 self
.user_srcdir
= os
.path
.realpath('src')
340 if self
.user_srcdir
== self
.orig_srcdir
or \
341 self
.user_srcdir
.startswith(self
.orig_srcdir
+ '/') or \
342 self
.orig_srcdir
.startswith(self
.user_srcdir
+ '/'):
343 info("Ignoring 'src' directory because it coincides with %s",
345 self
.user_srcdir
= None
347 return self
._selections
349 def get_build_changes(self
):
350 sels
= self
.get_selections()
351 old_sels
= self
.load_built_selections()
354 # See if things have changed since the last build
355 all_ifaces
= set(sels
.selections
) |
set(old_sels
.selections
)
357 old_impl
= old_sels
.selections
.get(x
, no_impl
)
358 new_impl
= sels
.selections
.get(x
, no_impl
)
359 if old_impl
.version
!= new_impl
.version
:
360 changes
.append("Version change for %s: %s -> %s" % (x
, old_impl
.version
, new_impl
.version
))
361 elif old_impl
.id != new_impl
.id:
362 changes
.append("Version change for %s: %s -> %s" % (x
, old_impl
.id, new_impl
.id))
366 root
= node
.ownerDocument
.documentElement
368 while node
and node
is not root
:
369 node
= node
.parentNode
374 if s
== 'true': return True
375 if s
== 'false': return False
376 raise SafeException('Expected "true" or "false" but got "%s"' % s
)
379 # Copied from 0launch 0.54 (remove once 0.54 is released)
380 def __init__(self
, default_ns
):
382 self
.default_ns
= default_ns
385 prefix
= self
.prefixes
.get(ns
, None)
388 prefix
= 'ns%d' % len(self
.prefixes
)
389 self
.prefixes
[ns
] = prefix
392 def setAttributeNS(self
, elem
, uri
, localName
, value
):
394 elem
.setAttributeNS(None, localName
, value
)
396 elem
.setAttributeNS(uri
, self
.get(uri
) + ':' + localName
, value
)
398 def createElementNS(self
, doc
, uri
, localName
):
399 if uri
== self
.default_ns
:
400 return doc
.createElementNS(uri
, localName
)
402 return doc
.createElementNS(uri
, self
.get(uri
) + ':' + localName
)