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
: model
.canonical_iface_uri(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()
213 # Don't use the feed's name, as it may contain the version number
214 name
= feed
.get_name().lower().replace(' ', '-')
216 return '%s-%s-%s' % (name
, self
.target_arch
.lower(), version
)
218 def load_built_feed(self
):
219 path
= self
.local_iface_file
222 feed
= model
.ZeroInstallFeed(qdom
.parse(stream
), local_path
= path
)
227 def load_built_selections(self
):
228 path
= join(self
.metadir
, 'build-environment.xml')
229 if os
.path
.exists(path
):
232 return selections
.Selections(qdom
.parse(stream
))
238 def download_base_url(self
):
239 return self
.config
.get('compile', 'download-base-url')
241 def chosen_impl(self
, uri
):
242 sels
= self
.get_selections()
243 assert uri
in sels
.selections
244 return sels
.selections
[uri
]
247 def local_download_iface(self
):
248 impl
, = self
.load_built_feed().implementations
.values()
249 return '%s-%s.xml' % (self
.iface_name
, impl
.get_version())
252 stream
= file(ENV_FILE
, 'w')
254 self
.config
.write(stream
)
258 def get_selections(self
, prompt
= False):
261 return self
._selections
263 selections_file
= self
.config
.get('compile', 'selections')
266 raise SafeException("Selections are fixed by %s" % selections_file
)
267 stream
= file(selections_file
)
269 self
._selections
= selections
.Selections(qdom
.parse(stream
))
272 from zeroinstall
.injector
import fetch
273 from zeroinstall
.injector
.handler
import Handler
275 fetcher
= fetch
.Fetcher(handler
)
276 blocker
= self
._selections
.download_missing(iface_cache
, fetcher
)
278 print "Waiting for selected implementations to be downloaded..."
279 handler
.wait_for_blocker(blocker
)
283 options
.append('--gui')
284 child
= subprocess
.Popen(launch_prog
+ ['--source', '--get-selections'] + options
+ [self
.interface
], stdout
= subprocess
.PIPE
)
286 self
._selections
= selections
.Selections(qdom
.parse(child
.stdout
))
289 raise SafeException("0launch --get-selections failed (exit code %d)" % child
.returncode
)
291 self
.root_impl
= self
._selections
.selections
[self
.interface
]
293 self
.orig_srcdir
= os
.path
.realpath(lookup(self
.root_impl
.id))
294 self
.user_srcdir
= None
296 if os
.path
.isdir('src'):
297 self
.user_srcdir
= os
.path
.realpath('src')
298 if self
.user_srcdir
== self
.orig_srcdir
or \
299 self
.user_srcdir
.startswith(self
.orig_srcdir
+ '/') or \
300 self
.orig_srcdir
.startswith(self
.user_srcdir
+ '/'):
301 info("Ignoring 'src' directory because it coincides with %s",
303 self
.user_srcdir
= None
305 return self
._selections
307 def get_build_changes(self
):
308 sels
= self
.get_selections()
309 old_sels
= self
.load_built_selections()
312 # See if things have changed since the last build
313 all_ifaces
= set(sels
.selections
) |
set(old_sels
.selections
)
315 old_impl
= old_sels
.selections
.get(x
, no_impl
)
316 new_impl
= sels
.selections
.get(x
, no_impl
)
317 if old_impl
.version
!= new_impl
.version
:
318 changes
.append("Version change for %s: %s -> %s" % (x
, old_impl
.version
, new_impl
.version
))
319 elif old_impl
.id != new_impl
.id:
320 changes
.append("Version change for %s: %s -> %s" % (x
, old_impl
.id, new_impl
.id))
324 root
= node
.ownerDocument
.documentElement
326 while node
and node
is not root
:
327 node
= node
.parentNode
331 format_version
= model
.format_version
332 parse_version
= model
.parse_version
335 if s
== 'true': return True
336 if s
== 'false': return False
337 raise SafeException('Expected "true" or "false" but got "%s"' % s
)