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 if os
.path
.isdir('dependencies'):
24 iface_cache
.stores
.stores
.append(Store(os
.path
.realpath('dependencies')))
32 if id.startswith('/'):
35 raise SafeException("Directory '%s' no longer exists. Try '0compile setup'" % id)
37 return iface_cache
.stores
.lookup(id)
39 raise NotStored(str(ex
) + "\nHint: try '0compile setup'")
41 def ensure_dir(d
, clean
= False):
49 raise SafeException("'%s' exists, but is not a directory!" % d
)
52 def find_in_path(prog
):
53 for d
in os
.environ
['PATH'].split(':'):
54 path
= os
.path
.join(d
, prog
)
55 if os
.path
.isfile(path
):
59 def spawn_and_check(prog
, args
):
60 status
= os
.spawnv(os
.P_WAIT
, prog
, [prog
] + args
)
62 raise SafeException("Program '%s' failed with exit code %d" % (prog
, status
))
64 raise SafeException("Program '%s' failed with signal %d" % (prog
, -status
))
66 def wait_for_child(child
):
67 """Wait for child to exit and reap it. Throw an exception if it doesn't return success."""
68 pid
, status
= os
.waitpid(child
, 0)
70 if os
.WIFEXITED(status
):
71 exit_code
= os
.WEXITSTATUS(status
)
75 raise SafeException('Command failed with exit status %d' % exit_code
)
77 raise SafeException('Command failed with signal %d' % WTERMSIG(status
))
79 def spawn_maybe_sandboxed(readable
, writable
, tmpdir
, prog
, args
):
84 exec_maybe_sandboxed(readable
, writable
, tmpdir
, prog
, args
)
88 print >>sys
.stderr
, "Exec failed"
92 def exec_maybe_sandboxed(readable
, writable
, tmpdir
, prog
, args
):
93 """execl prog, with (only) the 'writable' directories writable if sandboxing is available.
94 The readable directories will be readable, as well as various standard locations.
95 If no sandbox is available, run without a sandbox."""
97 USE_PLASH
= 'USE_PLASH_0COMPILE'
99 assert prog
.startswith('/')
100 _pola_run
= find_in_path('pola-run')
102 if _pola_run
is None:
103 print "Not using sandbox (plash not installed)"
106 use_plash
= os
.environ
.get(USE_PLASH
, '').lower() or 'not set'
107 if use_plash
in ('not set', 'false'):
108 print "Not using plash: $%s is %s" % (USE_PLASH
, use_plash
)
110 elif use_plash
== 'true':
113 raise Exception('$%s must be "true" or "false", not "%s"' % (USE_PLASH
, use_plash
))
116 os
.execlp(prog
, prog
, *args
)
118 print "Using plash to sandbox the build..."
120 # We have pola-shell :-)
121 pola_args
= ['--prog', prog
, '-B']
123 pola_args
+= ['-a', a
]
125 pola_args
+= ['-f', r
]
127 pola_args
+= ['-fw', w
]
128 pola_args
+= ['-tw', '/tmp', tmpdir
]
129 os
.environ
['TMPDIR'] = '/tmp'
130 os
.execl(_pola_run
, _pola_run
, *pola_args
)
134 target_os
, target_machine
= uname
[0], uname
[-1]
135 if target_machine
in ('i585', 'i686'):
136 target_machine
= 'i486' # (sensible default)
137 return target_os
+ '-' + target_machine
140 def __init__(self
, need_config
= True):
141 if need_config
and not os
.path
.isfile(ENV_FILE
):
142 raise SafeException("Run 0compile from a directory containing a '%s' file" % ENV_FILE
)
144 self
.config
= ConfigParser
.RawConfigParser()
145 self
.config
.add_section('compile')
146 self
.config
.set('compile', 'download-base-url', '')
147 self
.config
.set('compile', 'version-modifier', '')
148 self
.config
.set('compile', 'interface', '')
149 self
.config
.set('compile', 'selections', '')
150 self
.config
.set('compile', 'metadir', '0install')
152 self
.config
.read(ENV_FILE
)
154 self
._selections
= None
159 def iface_name(self
):
160 iface_name
= os
.path
.basename(self
.interface
)
161 if iface_name
.endswith('.xml'):
162 iface_name
= iface_name
[:-4]
163 iface_name
= iface_name
.replace(' ', '-')
164 if iface_name
.endswith('-src'):
165 iface_name
= iface_name
[:-4]
168 interface
= property(lambda self
: self
.config
.get('compile', 'interface'))
172 distdir_name
= '%s-%s' % (self
.iface_name
.lower(), get_arch_name().lower())
173 assert '/' not in distdir_name
174 return os
.path
.realpath(distdir_name
)
178 metadir
= self
.config
.get('compile', 'metadir')
179 assert not metadir
.startswith('/')
180 return join(self
.distdir
, metadir
)
183 def local_iface_file(self
):
184 return join(self
.metadir
, self
.iface_name
+ '.xml')
187 def target_arch(self
):
188 return get_arch_name()
191 def version_modifier(self
):
192 vm
= self
.config
.get('compile', 'version-modifier')
199 def archive_stem(self
):
200 # Use the version that we actually built, not the version we would build now
201 feed
= self
.load_built_feed()
202 assert len(feed
.implementations
) == 1
203 version
= feed
.implementations
.values()[0].get_version()
204 return '%s-%s-%s' % (self
.iface_name
.lower(), self
.target_arch
.lower(), version
)
206 def load_built_feed(self
):
207 path
= self
.local_iface_file
210 feed
= model
.ZeroInstallFeed(qdom
.parse(stream
), local_path
= path
)
215 def load_built_selections(self
):
216 path
= join(self
.metadir
, 'build-environment.xml')
217 if os
.path
.exists(path
):
220 return selections
.Selections(qdom
.parse(stream
))
226 def download_base_url(self
):
227 return self
.config
.get('compile', 'download-base-url')
229 def chosen_impl(self
, uri
):
230 sels
= self
.get_selections()
231 assert uri
in sels
.selections
232 return sels
.selections
[uri
]
235 def local_download_iface(self
):
236 impl
, = self
.load_built_feed().implementations
.values()
237 return '%s-%s.xml' % (self
.iface_name
, impl
.get_version())
240 stream
= file(ENV_FILE
, 'w')
242 self
.config
.write(stream
)
246 def get_selections(self
, prompt
= False):
249 return self
._selections
251 selections_file
= self
.config
.get('compile', 'selections')
254 raise SafeException("Selections are fixed by %s" % selections_file
)
255 stream
= file(selections_file
)
257 self
._selections
= selections
.Selections(qdom
.parse(stream
))
260 from zeroinstall
.injector
import fetch
261 from zeroinstall
.injector
.handler
import Handler
263 fetcher
= fetch
.Fetcher(handler
)
264 blocker
= self
._selections
.download_missing(iface_cache
, fetcher
)
266 print "Waiting for selected implementations to be downloaded..."
267 handler
.wait_for_blocker(blocker
)
271 options
.append('--gui')
272 child
= subprocess
.Popen(['0launch', '--source', '--get-selections'] + options
+ [self
.interface
], stdout
= subprocess
.PIPE
)
274 self
._selections
= selections
.Selections(qdom
.parse(child
.stdout
))
277 raise SafeException("0launch --get-selections failed (exit code %d)" % child
.returncode
)
279 self
.root_impl
= self
._selections
.selections
[self
.interface
]
281 self
.orig_srcdir
= os
.path
.realpath(lookup(self
.root_impl
.id))
282 self
.user_srcdir
= None
284 if os
.path
.isdir('src'):
285 self
.user_srcdir
= os
.path
.realpath('src')
286 if self
.user_srcdir
== self
.orig_srcdir
or \
287 self
.user_srcdir
.startswith(self
.orig_srcdir
+ '/') or \
288 self
.orig_srcdir
.startswith(self
.user_srcdir
+ '/'):
289 info("Ignoring 'src' directory because it coincides with %s",
291 self
.user_srcdir
= None
293 return self
._selections
295 def get_build_changes(self
):
296 sels
= self
.get_selections()
297 old_sels
= self
.load_built_selections()
300 # See if things have changed since the last build
301 all_ifaces
= set(sels
.selections
) |
set(old_sels
.selections
)
303 old_impl
= old_sels
.selections
.get(x
, no_impl
)
304 new_impl
= sels
.selections
.get(x
, no_impl
)
305 if old_impl
.version
!= new_impl
.version
:
306 changes
.append("Version change for %s: %s -> %s" % (x
, old_impl
.version
, new_impl
.version
))
307 elif old_impl
.id != new_impl
.id:
308 changes
.append("Version change for %s: %s -> %s" % (x
, old_impl
.id, new_impl
.id))
312 root
= node
.ownerDocument
.documentElement
314 while node
and node
is not root
:
315 node
= node
.parentNode
319 format_version
= model
.format_version
320 parse_version
= model
.parse_version
323 if s
== 'true': return True
324 if s
== 'false': return False
325 raise SafeException('Expected "true" or "false" but got "%s"' % s
)