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
]
40 if id.startswith('/'):
43 raise SafeException("Directory '%s' no longer exists. Try '0compile setup'" % id)
45 return iface_cache
.stores
.lookup(id)
47 raise NotStored(str(ex
) + "\nHint: try '0compile setup'")
49 def ensure_dir(d
, clean
= False):
57 raise SafeException("'%s' exists, but is not a directory!" % d
)
60 def find_in_path(prog
):
61 for d
in os
.environ
['PATH'].split(':'):
62 path
= os
.path
.join(d
, prog
)
63 if os
.path
.isfile(path
):
67 def spawn_and_check(prog
, args
):
68 status
= os
.spawnv(os
.P_WAIT
, prog
, [prog
] + args
)
70 raise SafeException("Program '%s' failed with exit code %d" % (prog
, status
))
72 raise SafeException("Program '%s' failed with signal %d" % (prog
, -status
))
74 def wait_for_child(child
):
75 """Wait for child to exit and reap it. Throw an exception if it doesn't return success."""
76 pid
, status
= os
.waitpid(child
, 0)
78 if os
.WIFEXITED(status
):
79 exit_code
= os
.WEXITSTATUS(status
)
83 raise SafeException('Command failed with exit status %d' % exit_code
)
85 raise SafeException('Command failed with signal %d' % WTERMSIG(status
))
87 def spawn_maybe_sandboxed(readable
, writable
, tmpdir
, prog
, args
):
92 exec_maybe_sandboxed(readable
, writable
, tmpdir
, prog
, args
)
96 print >>sys
.stderr
, "Exec failed"
100 def exec_maybe_sandboxed(readable
, writable
, tmpdir
, prog
, args
):
101 """execl prog, with (only) the 'writable' directories writable if sandboxing is available.
102 The readable directories will be readable, as well as various standard locations.
103 If no sandbox is available, run without a sandbox."""
105 USE_PLASH
= 'USE_PLASH_0COMPILE'
107 assert prog
.startswith('/')
108 _pola_run
= find_in_path('pola-run')
110 if _pola_run
is None:
111 print "Not using sandbox (plash not installed)"
114 use_plash
= os
.environ
.get(USE_PLASH
, '').lower() or 'not set'
115 if use_plash
in ('not set', 'false'):
116 print "Not using plash: $%s is %s" % (USE_PLASH
, use_plash
)
118 elif use_plash
== 'true':
121 raise Exception('$%s must be "true" or "false", not "%s"' % (USE_PLASH
, use_plash
))
124 os
.execlp(prog
, prog
, *args
)
126 print "Using plash to sandbox the build..."
128 # We have pola-shell :-)
129 pola_args
= ['--prog', prog
, '-B']
131 pola_args
+= ['-a', a
]
133 pola_args
+= ['-f', r
]
135 pola_args
+= ['-fw', w
]
136 pola_args
+= ['-tw', '/tmp', tmpdir
]
137 os
.environ
['TMPDIR'] = '/tmp'
138 os
.execl(_pola_run
, _pola_run
, *pola_args
)
142 target_os
, target_machine
= uname
[0], uname
[-1]
143 if target_machine
in ('i585', 'i686'):
144 target_machine
= 'i486' # (sensible default)
145 return target_os
+ '-' + target_machine
148 def __init__(self
, need_config
= True):
149 if need_config
and not os
.path
.isfile(ENV_FILE
):
150 raise SafeException("Run 0compile from a directory containing a '%s' file" % ENV_FILE
)
152 self
.config
= ConfigParser
.RawConfigParser()
153 self
.config
.add_section('compile')
154 self
.config
.set('compile', 'download-base-url', '')
155 self
.config
.set('compile', 'version-modifier', '')
156 self
.config
.set('compile', 'interface', '')
157 self
.config
.set('compile', 'selections', '')
158 self
.config
.set('compile', 'metadir', '0install')
160 self
.config
.read(ENV_FILE
)
162 self
._selections
= None
167 def iface_name(self
):
168 iface_name
= os
.path
.basename(self
.interface
)
169 if iface_name
.endswith('.xml'):
170 iface_name
= iface_name
[:-4]
171 iface_name
= iface_name
.replace(' ', '-')
172 if iface_name
.endswith('-src'):
173 iface_name
= iface_name
[:-4]
176 interface
= property(lambda self
: self
.config
.get('compile', 'interface'))
180 distdir_name
= '%s-%s' % (self
.iface_name
.lower(), get_arch_name().lower())
181 assert '/' not in distdir_name
182 return os
.path
.realpath(distdir_name
)
186 metadir
= self
.config
.get('compile', 'metadir')
187 assert not metadir
.startswith('/')
188 return join(self
.distdir
, metadir
)
191 def local_iface_file(self
):
192 return join(self
.metadir
, self
.iface_name
+ '.xml')
195 def target_arch(self
):
196 return get_arch_name()
199 def version_modifier(self
):
200 vm
= self
.config
.get('compile', 'version-modifier')
207 def archive_stem(self
):
208 # Use the version that we actually built, not the version we would build now
209 feed
= self
.load_built_feed()
210 assert len(feed
.implementations
) == 1
211 version
= feed
.implementations
.values()[0].get_version()
212 return '%s-%s-%s' % (self
.iface_name
.lower(), self
.target_arch
.lower(), version
)
214 def load_built_feed(self
):
215 path
= self
.local_iface_file
218 feed
= model
.ZeroInstallFeed(qdom
.parse(stream
), local_path
= path
)
223 def load_built_selections(self
):
224 path
= join(self
.metadir
, 'build-environment.xml')
225 if os
.path
.exists(path
):
228 return selections
.Selections(qdom
.parse(stream
))
234 def download_base_url(self
):
235 return self
.config
.get('compile', 'download-base-url')
237 def chosen_impl(self
, uri
):
238 sels
= self
.get_selections()
239 assert uri
in sels
.selections
240 return sels
.selections
[uri
]
243 def local_download_iface(self
):
244 impl
, = self
.load_built_feed().implementations
.values()
245 return '%s-%s.xml' % (self
.iface_name
, impl
.get_version())
248 stream
= file(ENV_FILE
, 'w')
250 self
.config
.write(stream
)
254 def get_selections(self
, prompt
= False):
257 return self
._selections
259 selections_file
= self
.config
.get('compile', 'selections')
262 raise SafeException("Selections are fixed by %s" % selections_file
)
263 stream
= file(selections_file
)
265 self
._selections
= selections
.Selections(qdom
.parse(stream
))
268 from zeroinstall
.injector
import fetch
269 from zeroinstall
.injector
.handler
import Handler
271 fetcher
= fetch
.Fetcher(handler
)
272 blocker
= self
._selections
.download_missing(iface_cache
, fetcher
)
274 print "Waiting for selected implementations to be downloaded..."
275 handler
.wait_for_blocker(blocker
)
279 options
.append('--gui')
280 child
= subprocess
.Popen(launch_prog
+ ['--source', '--get-selections'] + options
+ [self
.interface
], stdout
= subprocess
.PIPE
)
282 self
._selections
= selections
.Selections(qdom
.parse(child
.stdout
))
285 raise SafeException("0launch --get-selections failed (exit code %d)" % child
.returncode
)
287 self
.root_impl
= self
._selections
.selections
[self
.interface
]
289 self
.orig_srcdir
= os
.path
.realpath(lookup(self
.root_impl
.id))
290 self
.user_srcdir
= None
292 if os
.path
.isdir('src'):
293 self
.user_srcdir
= os
.path
.realpath('src')
294 if self
.user_srcdir
== self
.orig_srcdir
or \
295 self
.user_srcdir
.startswith(self
.orig_srcdir
+ '/') or \
296 self
.orig_srcdir
.startswith(self
.user_srcdir
+ '/'):
297 info("Ignoring 'src' directory because it coincides with %s",
299 self
.user_srcdir
= None
301 return self
._selections
303 def get_build_changes(self
):
304 sels
= self
.get_selections()
305 old_sels
= self
.load_built_selections()
308 # See if things have changed since the last build
309 all_ifaces
= set(sels
.selections
) |
set(old_sels
.selections
)
311 old_impl
= old_sels
.selections
.get(x
, no_impl
)
312 new_impl
= sels
.selections
.get(x
, no_impl
)
313 if old_impl
.version
!= new_impl
.version
:
314 changes
.append("Version change for %s: %s -> %s" % (x
, old_impl
.version
, new_impl
.version
))
315 elif old_impl
.id != new_impl
.id:
316 changes
.append("Version change for %s: %s -> %s" % (x
, old_impl
.id, new_impl
.id))
320 root
= node
.ownerDocument
.documentElement
322 while node
and node
is not root
:
323 node
= node
.parentNode
327 format_version
= model
.format_version
328 parse_version
= model
.parse_version
331 if s
== 'true': return True
332 if s
== 'false': return False
333 raise SafeException('Expected "true" or "false" but got "%s"' % s
)