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
12 from zeroinstall
.injector
.iface_cache
import iface_cache
13 from zeroinstall
import SafeException
14 from zeroinstall
.zerostore
import Store
, NotStored
18 ENV_FILE
= '0compile.properties'
19 XMLNS_0COMPILE
= 'http://zero-install.sourceforge.net/2006/namespaces/0compile'
21 zeroinstall_dir
= os
.environ
.get('0COMPILE_ZEROINSTALL', None)
23 launch_prog
= [os
.path
.join(zeroinstall_dir
, '0launch')]
25 launch_prog
= ['0launch']
27 if os
.path
.isdir('dependencies'):
28 dep_dir
= os
.path
.realpath('dependencies')
29 iface_cache
.stores
.stores
.append(Store(dep_dir
))
30 launch_prog
+= ['--with-store', dep_dir
]
37 def is_package_impl(impl
):
38 return impl
.id.startswith("package:")
40 def lookup(impl_or_sel
):
42 if id.startswith('/'):
45 raise SafeException("Directory '%s' no longer exists. Try '0compile setup'" % id)
47 return iface_cache
.stores
.lookup_any(impl_or_sel
.digests
)
49 raise NotStored(str(ex
) + "\nHint: try '0compile setup'")
51 def ensure_dir(d
, clean
= False):
59 raise SafeException("'%s' exists, but is not a directory!" % d
)
62 def find_in_path(prog
):
63 for d
in os
.environ
['PATH'].split(':'):
64 path
= os
.path
.join(d
, prog
)
65 if os
.path
.isfile(path
):
69 def spawn_and_check(prog
, args
):
70 status
= os
.spawnv(os
.P_WAIT
, prog
, [prog
] + args
)
72 raise SafeException("Program '%s' failed with exit code %d" % (prog
, status
))
74 raise SafeException("Program '%s' failed with signal %d" % (prog
, -status
))
76 def wait_for_child(child
):
77 """Wait for child to exit and reap it. Throw an exception if it doesn't return success."""
78 pid
, status
= os
.waitpid(child
, 0)
80 if os
.WIFEXITED(status
):
81 exit_code
= os
.WEXITSTATUS(status
)
85 raise SafeException('Command failed with exit status %d' % exit_code
)
87 raise SafeException('Command failed with signal %d' % os
.WTERMSIG(status
))
89 def spawn_maybe_sandboxed(readable
, writable
, tmpdir
, prog
, args
):
94 exec_maybe_sandboxed(readable
, writable
, tmpdir
, prog
, args
)
98 print >>sys
.stderr
, "Exec failed"
100 wait_for_child(child
)
102 def exec_maybe_sandboxed(readable
, writable
, tmpdir
, prog
, args
):
103 """execl prog, with (only) the 'writable' directories writable if sandboxing is available.
104 The readable directories will be readable, as well as various standard locations.
105 If no sandbox is available, run without a sandbox."""
107 USE_PLASH
= 'USE_PLASH_0COMPILE'
109 assert prog
.startswith('/')
110 _pola_run
= find_in_path('pola-run')
112 if _pola_run
is None:
113 print "Not using sandbox (plash not installed)"
116 use_plash
= os
.environ
.get(USE_PLASH
, '').lower() or 'not set'
117 if use_plash
in ('not set', 'false'):
118 print "Not using plash: $%s is %s" % (USE_PLASH
, use_plash
)
120 elif use_plash
== 'true':
123 raise Exception('$%s must be "true" or "false", not "%s"' % (USE_PLASH
, use_plash
))
126 os
.execlp(prog
, prog
, *args
)
128 print "Using plash to sandbox the build..."
130 # We have pola-shell :-)
131 pola_args
= ['--prog', prog
, '-B']
133 pola_args
+= ['-a', a
]
135 pola_args
+= ['-f', r
]
137 pola_args
+= ['-fw', w
]
138 pola_args
+= ['-tw', '/tmp', tmpdir
]
139 os
.environ
['TMPDIR'] = '/tmp'
140 os
.execl(_pola_run
, _pola_run
, *pola_args
)
144 target_os
, target_machine
= uname
[0], uname
[-1]
145 if target_machine
in ('i585', 'i686'):
146 target_machine
= 'i486' # (sensible default)
147 return target_os
+ '-' + target_machine
150 def __init__(self
, need_config
= True):
151 if need_config
and not os
.path
.isfile(ENV_FILE
):
152 raise SafeException("Run 0compile from a directory containing a '%s' file" % ENV_FILE
)
154 self
.config
= ConfigParser
.RawConfigParser()
155 self
.config
.add_section('compile')
156 self
.config
.set('compile', 'download-base-url', '')
157 self
.config
.set('compile', 'version-modifier', '')
158 self
.config
.set('compile', 'interface', '')
159 self
.config
.set('compile', 'selections', '')
160 self
.config
.set('compile', 'metadir', '0install')
161 self
.config
.set('compile', 'distdir', '')
163 self
.config
.read(ENV_FILE
)
165 self
._selections
= None
170 def iface_name(self
):
171 iface_name
= os
.path
.basename(self
.interface
)
172 if iface_name
.endswith('.xml'):
173 iface_name
= iface_name
[:-4]
174 iface_name
= iface_name
.replace(' ', '-')
175 if iface_name
.endswith('-src'):
176 iface_name
= iface_name
[:-4]
179 interface
= property(lambda self
: model
.canonical_iface_uri(self
.config
.get('compile', 'interface')))
183 distdir_name
= self
.config
.get('compile', 'distdir') or \
184 '%s-%s' % (self
.iface_name
.lower(), get_arch_name().lower())
185 assert '/' not in distdir_name
186 return os
.path
.realpath(distdir_name
)
190 metadir
= self
.config
.get('compile', 'metadir')
191 assert not metadir
.startswith('/')
192 return join(self
.distdir
, metadir
)
195 def local_iface_file(self
):
196 return join(self
.metadir
, self
.iface_name
+ '.xml')
199 def target_arch(self
):
200 return get_arch_name()
203 def version_modifier(self
):
204 vm
= self
.config
.get('compile', 'version-modifier')
211 def archive_stem(self
):
212 # Use the version that we actually built, not the version we would build now
213 feed
= self
.load_built_feed()
214 assert len(feed
.implementations
) == 1
215 version
= feed
.implementations
.values()[0].get_version()
217 # Don't use the feed's name, as it may contain the version number
218 name
= feed
.get_name().lower().replace(' ', '-')
220 return '%s-%s-%s' % (name
, self
.target_arch
.lower(), version
)
222 def load_built_feed(self
):
223 path
= self
.local_iface_file
226 feed
= model
.ZeroInstallFeed(qdom
.parse(stream
), local_path
= path
)
231 def load_built_selections(self
):
232 path
= join(self
.metadir
, 'build-environment.xml')
233 if os
.path
.exists(path
):
236 return selections
.Selections(qdom
.parse(stream
))
242 def download_base_url(self
):
243 return self
.config
.get('compile', 'download-base-url')
245 def chosen_impl(self
, uri
):
246 sels
= self
.get_selections()
247 assert uri
in sels
.selections
248 return sels
.selections
[uri
]
251 def local_download_iface(self
):
252 impl
, = self
.load_built_feed().implementations
.values()
253 return '%s-%s.xml' % (self
.iface_name
, impl
.get_version())
256 stream
= file(ENV_FILE
, 'w')
258 self
.config
.write(stream
)
262 def get_selections(self
, prompt
= False):
265 return self
._selections
267 selections_file
= self
.config
.get('compile', 'selections')
270 raise SafeException("Selections are fixed by %s" % selections_file
)
271 stream
= file(selections_file
)
273 self
._selections
= selections
.Selections(qdom
.parse(stream
))
276 from zeroinstall
.injector
import fetch
277 from zeroinstall
.injector
import handler
279 h
= handler
.ConsoleHandler()
281 h
= handler
.Handler()
283 fetcher
= fetch
.Fetcher(h
)
284 blocker
= self
._selections
.download_missing(iface_cache
, fetcher
)
286 print "Waiting for selected implementations to be downloaded..."
287 h
.wait_for_blocker(blocker
)
290 if prompt
and '--console' not in launch_prog
:
291 options
.append('--gui')
292 child
= subprocess
.Popen(launch_prog
+ ['--source', '--get-selections'] + options
+ [self
.interface
], stdout
= subprocess
.PIPE
)
294 self
._selections
= selections
.Selections(qdom
.parse(child
.stdout
))
297 raise SafeException("0launch --get-selections failed (exit code %d)" % child
.returncode
)
299 self
.root_impl
= self
._selections
.selections
[self
.interface
]
301 self
.orig_srcdir
= os
.path
.realpath(lookup(self
.root_impl
))
302 self
.user_srcdir
= None
304 if os
.path
.isdir('src'):
305 self
.user_srcdir
= os
.path
.realpath('src')
306 if self
.user_srcdir
== self
.orig_srcdir
or \
307 self
.user_srcdir
.startswith(self
.orig_srcdir
+ '/') or \
308 self
.orig_srcdir
.startswith(self
.user_srcdir
+ '/'):
309 info("Ignoring 'src' directory because it coincides with %s",
311 self
.user_srcdir
= None
313 return self
._selections
315 def get_build_changes(self
):
316 sels
= self
.get_selections()
317 old_sels
= self
.load_built_selections()
320 # See if things have changed since the last build
321 all_ifaces
= set(sels
.selections
) |
set(old_sels
.selections
)
323 old_impl
= old_sels
.selections
.get(x
, no_impl
)
324 new_impl
= sels
.selections
.get(x
, no_impl
)
325 if old_impl
.version
!= new_impl
.version
:
326 changes
.append("Version change for %s: %s -> %s" % (x
, old_impl
.version
, new_impl
.version
))
327 elif old_impl
.id != new_impl
.id:
328 changes
.append("Version change for %s: %s -> %s" % (x
, old_impl
.id, new_impl
.id))
332 root
= node
.ownerDocument
.documentElement
334 while node
and node
is not root
:
335 node
= node
.parentNode
340 if s
== 'true': return True
341 if s
== 'false': return False
342 raise SafeException('Expected "true" or "false" but got "%s"' % s
)