1 # Copyright (C) 2006, Thomas Leonard
2 # See http://0install.net/0compile.html
4 import os
, sys
, tempfile
, 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
.model
import Interface
, Implementation
, EnvironmentBinding
, escape
12 from zeroinstall
.injector
import namespaces
, reader
13 from zeroinstall
.support
import basedir
15 from zeroinstall
.injector
.iface_cache
import iface_cache
16 from zeroinstall
import SafeException
17 from zeroinstall
.injector
import run
18 from zeroinstall
.zerostore
import Stores
, Store
, NotStored
20 ENV_FILE
= '0compile.properties'
21 XMLNS_0COMPILE
= 'http://zero-install.sourceforge.net/2006/namespaces/0compile'
23 zeroinstall_dir
= os
.environ
.get('0COMPILE_ZEROINSTALL', None)
25 launch_prog
= [os
.path
.join(zeroinstall_dir
, '0launch')]
27 launch_prog
= ['0launch']
29 if os
.path
.isdir('dependencies'):
30 dep_dir
= os
.path
.realpath('dependencies')
31 iface_cache
.stores
.stores
.append(Store(dep_dir
))
32 launch_prog
+= ['--with-store', dep_dir
]
39 def is_package_impl(impl
):
40 return impl
.id.startswith("package:")
43 if id.startswith('/'):
46 raise SafeException("Directory '%s' no longer exists. Try '0compile setup'" % id)
48 return iface_cache
.stores
.lookup(id)
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' % 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
, target_machine
= uname
[0], uname
[-1]
146 if target_machine
in ('i585', 'i686'):
147 target_machine
= 'i486' # (sensible default)
148 return target_os
+ '-' + target_machine
151 def __init__(self
, need_config
= True):
152 if need_config
and not os
.path
.isfile(ENV_FILE
):
153 raise SafeException("Run 0compile from a directory containing a '%s' file" % ENV_FILE
)
155 self
.config
= ConfigParser
.RawConfigParser()
156 self
.config
.add_section('compile')
157 self
.config
.set('compile', 'download-base-url', '')
158 self
.config
.set('compile', 'version-modifier', '')
159 self
.config
.set('compile', 'interface', '')
160 self
.config
.set('compile', 'selections', '')
161 self
.config
.set('compile', 'metadir', '0install')
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
= '%s-%s' % (self
.iface_name
.lower(), get_arch_name().lower())
184 assert '/' not in distdir_name
185 return os
.path
.realpath(distdir_name
)
189 metadir
= self
.config
.get('compile', 'metadir')
190 assert not metadir
.startswith('/')
191 return join(self
.distdir
, metadir
)
194 def local_iface_file(self
):
195 return join(self
.metadir
, self
.iface_name
+ '.xml')
198 def target_arch(self
):
199 return get_arch_name()
202 def version_modifier(self
):
203 vm
= self
.config
.get('compile', 'version-modifier')
210 def archive_stem(self
):
211 # Use the version that we actually built, not the version we would build now
212 feed
= self
.load_built_feed()
213 assert len(feed
.implementations
) == 1
214 version
= feed
.implementations
.values()[0].get_version()
216 # Don't use the feed's name, as it may contain the version number
217 name
= feed
.get_name().lower().replace(' ', '-')
219 return '%s-%s-%s' % (name
, self
.target_arch
.lower(), version
)
221 def load_built_feed(self
):
222 path
= self
.local_iface_file
225 feed
= model
.ZeroInstallFeed(qdom
.parse(stream
), local_path
= path
)
230 def load_built_selections(self
):
231 path
= join(self
.metadir
, 'build-environment.xml')
232 if os
.path
.exists(path
):
235 return selections
.Selections(qdom
.parse(stream
))
241 def download_base_url(self
):
242 return self
.config
.get('compile', 'download-base-url')
244 def chosen_impl(self
, uri
):
245 sels
= self
.get_selections()
246 assert uri
in sels
.selections
247 return sels
.selections
[uri
]
250 def local_download_iface(self
):
251 impl
, = self
.load_built_feed().implementations
.values()
252 return '%s-%s.xml' % (self
.iface_name
, impl
.get_version())
255 stream
= file(ENV_FILE
, 'w')
257 self
.config
.write(stream
)
261 def get_selections(self
, prompt
= False):
264 return self
._selections
266 selections_file
= self
.config
.get('compile', 'selections')
269 raise SafeException("Selections are fixed by %s" % selections_file
)
270 stream
= file(selections_file
)
272 self
._selections
= selections
.Selections(qdom
.parse(stream
))
275 from zeroinstall
.injector
import fetch
276 from zeroinstall
.injector
.handler
import Handler
278 fetcher
= fetch
.Fetcher(handler
)
279 blocker
= self
._selections
.download_missing(iface_cache
, fetcher
)
281 print "Waiting for selected implementations to be downloaded..."
282 handler
.wait_for_blocker(blocker
)
286 options
.append('--gui')
287 child
= subprocess
.Popen(launch_prog
+ ['--source', '--get-selections'] + options
+ [self
.interface
], stdout
= subprocess
.PIPE
)
289 self
._selections
= selections
.Selections(qdom
.parse(child
.stdout
))
292 raise SafeException("0launch --get-selections failed (exit code %d)" % child
.returncode
)
294 self
.root_impl
= self
._selections
.selections
[self
.interface
]
296 self
.orig_srcdir
= os
.path
.realpath(lookup(self
.root_impl
.id))
297 self
.user_srcdir
= None
299 if os
.path
.isdir('src'):
300 self
.user_srcdir
= os
.path
.realpath('src')
301 if self
.user_srcdir
== self
.orig_srcdir
or \
302 self
.user_srcdir
.startswith(self
.orig_srcdir
+ '/') or \
303 self
.orig_srcdir
.startswith(self
.user_srcdir
+ '/'):
304 info("Ignoring 'src' directory because it coincides with %s",
306 self
.user_srcdir
= None
308 return self
._selections
310 def get_build_changes(self
):
311 sels
= self
.get_selections()
312 old_sels
= self
.load_built_selections()
315 # See if things have changed since the last build
316 all_ifaces
= set(sels
.selections
) |
set(old_sels
.selections
)
318 old_impl
= old_sels
.selections
.get(x
, no_impl
)
319 new_impl
= sels
.selections
.get(x
, no_impl
)
320 if old_impl
.version
!= new_impl
.version
:
321 changes
.append("Version change for %s: %s -> %s" % (x
, old_impl
.version
, new_impl
.version
))
322 elif old_impl
.id != new_impl
.id:
323 changes
.append("Version change for %s: %s -> %s" % (x
, old_impl
.id, new_impl
.id))
327 root
= node
.ownerDocument
.documentElement
329 while node
and node
is not root
:
330 node
= node
.parentNode
334 format_version
= model
.format_version
335 parse_version
= model
.parse_version
338 if s
== 'true': return True
339 if s
== 'false': return False
340 raise SafeException('Expected "true" or "false" but got "%s"' % s
)