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:")
42 def lookup(impl_or_sel
):
44 if id.startswith('/'):
47 raise SafeException("Directory '%s' no longer exists. Try '0compile setup'" % id)
49 return iface_cache
.stores
.lookup_any(impl_or_sel
.digests
)
51 raise NotStored(str(ex
) + "\nHint: try '0compile setup'")
53 def ensure_dir(d
, clean
= False):
61 raise SafeException("'%s' exists, but is not a directory!" % d
)
64 def find_in_path(prog
):
65 for d
in os
.environ
['PATH'].split(':'):
66 path
= os
.path
.join(d
, prog
)
67 if os
.path
.isfile(path
):
71 def spawn_and_check(prog
, args
):
72 status
= os
.spawnv(os
.P_WAIT
, prog
, [prog
] + args
)
74 raise SafeException("Program '%s' failed with exit code %d" % (prog
, status
))
76 raise SafeException("Program '%s' failed with signal %d" % (prog
, -status
))
78 def wait_for_child(child
):
79 """Wait for child to exit and reap it. Throw an exception if it doesn't return success."""
80 pid
, status
= os
.waitpid(child
, 0)
82 if os
.WIFEXITED(status
):
83 exit_code
= os
.WEXITSTATUS(status
)
87 raise SafeException('Command failed with exit status %d' % exit_code
)
89 raise SafeException('Command failed with signal %d' % WTERMSIG(status
))
91 def spawn_maybe_sandboxed(readable
, writable
, tmpdir
, prog
, args
):
96 exec_maybe_sandboxed(readable
, writable
, tmpdir
, prog
, args
)
100 print >>sys
.stderr
, "Exec failed"
102 wait_for_child(child
)
104 def exec_maybe_sandboxed(readable
, writable
, tmpdir
, prog
, args
):
105 """execl prog, with (only) the 'writable' directories writable if sandboxing is available.
106 The readable directories will be readable, as well as various standard locations.
107 If no sandbox is available, run without a sandbox."""
109 USE_PLASH
= 'USE_PLASH_0COMPILE'
111 assert prog
.startswith('/')
112 _pola_run
= find_in_path('pola-run')
114 if _pola_run
is None:
115 print "Not using sandbox (plash not installed)"
118 use_plash
= os
.environ
.get(USE_PLASH
, '').lower() or 'not set'
119 if use_plash
in ('not set', 'false'):
120 print "Not using plash: $%s is %s" % (USE_PLASH
, use_plash
)
122 elif use_plash
== 'true':
125 raise Exception('$%s must be "true" or "false", not "%s"' % (USE_PLASH
, use_plash
))
128 os
.execlp(prog
, prog
, *args
)
130 print "Using plash to sandbox the build..."
132 # We have pola-shell :-)
133 pola_args
= ['--prog', prog
, '-B']
135 pola_args
+= ['-a', a
]
137 pola_args
+= ['-f', r
]
139 pola_args
+= ['-fw', w
]
140 pola_args
+= ['-tw', '/tmp', tmpdir
]
141 os
.environ
['TMPDIR'] = '/tmp'
142 os
.execl(_pola_run
, _pola_run
, *pola_args
)
146 target_os
, target_machine
= uname
[0], uname
[-1]
147 if target_machine
in ('i585', 'i686'):
148 target_machine
= 'i486' # (sensible default)
149 return target_os
+ '-' + target_machine
152 def __init__(self
, need_config
= True):
153 if need_config
and not os
.path
.isfile(ENV_FILE
):
154 raise SafeException("Run 0compile from a directory containing a '%s' file" % ENV_FILE
)
156 self
.config
= ConfigParser
.RawConfigParser()
157 self
.config
.add_section('compile')
158 self
.config
.set('compile', 'download-base-url', '')
159 self
.config
.set('compile', 'version-modifier', '')
160 self
.config
.set('compile', 'interface', '')
161 self
.config
.set('compile', 'selections', '')
162 self
.config
.set('compile', 'metadir', '0install')
164 self
.config
.read(ENV_FILE
)
166 self
._selections
= None
171 def iface_name(self
):
172 iface_name
= os
.path
.basename(self
.interface
)
173 if iface_name
.endswith('.xml'):
174 iface_name
= iface_name
[:-4]
175 iface_name
= iface_name
.replace(' ', '-')
176 if iface_name
.endswith('-src'):
177 iface_name
= iface_name
[:-4]
180 interface
= property(lambda self
: model
.canonical_iface_uri(self
.config
.get('compile', 'interface')))
184 distdir_name
= '%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
.handler
import Handler
279 fetcher
= fetch
.Fetcher(handler
)
280 blocker
= self
._selections
.download_missing(iface_cache
, fetcher
)
282 print "Waiting for selected implementations to be downloaded..."
283 handler
.wait_for_blocker(blocker
)
287 options
.append('--gui')
288 child
= subprocess
.Popen(launch_prog
+ ['--source', '--get-selections'] + options
+ [self
.interface
], stdout
= subprocess
.PIPE
)
290 self
._selections
= selections
.Selections(qdom
.parse(child
.stdout
))
293 raise SafeException("0launch --get-selections failed (exit code %d)" % child
.returncode
)
295 self
.root_impl
= self
._selections
.selections
[self
.interface
]
297 self
.orig_srcdir
= os
.path
.realpath(lookup(self
.root_impl
))
298 self
.user_srcdir
= None
300 if os
.path
.isdir('src'):
301 self
.user_srcdir
= os
.path
.realpath('src')
302 if self
.user_srcdir
== self
.orig_srcdir
or \
303 self
.user_srcdir
.startswith(self
.orig_srcdir
+ '/') or \
304 self
.orig_srcdir
.startswith(self
.user_srcdir
+ '/'):
305 info("Ignoring 'src' directory because it coincides with %s",
307 self
.user_srcdir
= None
309 return self
._selections
311 def get_build_changes(self
):
312 sels
= self
.get_selections()
313 old_sels
= self
.load_built_selections()
316 # See if things have changed since the last build
317 all_ifaces
= set(sels
.selections
) |
set(old_sels
.selections
)
319 old_impl
= old_sels
.selections
.get(x
, no_impl
)
320 new_impl
= sels
.selections
.get(x
, no_impl
)
321 if old_impl
.version
!= new_impl
.version
:
322 changes
.append("Version change for %s: %s -> %s" % (x
, old_impl
.version
, new_impl
.version
))
323 elif old_impl
.id != new_impl
.id:
324 changes
.append("Version change for %s: %s -> %s" % (x
, old_impl
.id, new_impl
.id))
328 root
= node
.ownerDocument
.documentElement
330 while node
and node
is not root
:
331 node
= node
.parentNode
335 format_version
= model
.format_version
336 parse_version
= model
.parse_version
339 if s
== 'true': return True
340 if s
== 'false': return False
341 raise SafeException('Expected "true" or "false" but got "%s"' % s
)