Depend on 0launch >= 0.41 to get new trust confirmation dialog box
[0compile.git] / build.py
blob15733affc7adb7659ad38b16ca8c8df724528d1e
1 # Copyright (C) 2006, Thomas Leonard
2 # See http://0install.net/0compile.html
4 import sys, os, __main__, time, shutil, glob, codecs, subprocess
5 from os.path import join
6 from logging import info, warn
7 from xml.dom import minidom, XMLNS_NAMESPACE
8 from optparse import OptionParser
10 from support import *
12 if hasattr(os.path, 'relpath'):
13 relpath = os.path.relpath
14 else:
15 # Copied from Python 2.6 (GPL compatible license)
16 # Copyright (c) 2001, 2002, 2003, 2004, 2005, 2006 Python Software Foundation; All Rights Reserved
18 # Return the longest prefix of all list elements.
19 def commonprefix(m):
20 "Given a list of pathnames, returns the longest common leading component"
21 if not m: return ''
22 s1 = min(m)
23 s2 = max(m)
24 for i, c in enumerate(s1):
25 if c != s2[i]:
26 return s1[:i]
27 return s1
29 def relpath(path, start):
30 """Return a relative version of a path"""
32 if not path:
33 raise ValueError("no path specified")
35 start_list = os.path.abspath(start).split('/')
36 path_list = os.path.abspath(path).split('/')
38 # Work out how much of the filepath is shared by start and path.
39 i = len(commonprefix([start_list, path_list]))
41 rel_list = ['..'] * (len(start_list)-i) + path_list[i:]
42 if not rel_list:
43 return '.'
44 return join(*rel_list)
46 # If we have to modify any pkg-config files, we put the new versions in $TMPDIR/PKG_CONFIG_OVERRIDES
47 PKG_CONFIG_OVERRIDES = 'pkg-config-overrides'
49 def env(name, value):
50 os.environ[name] = value
51 print "%s=%s" % (name, value)
53 def do_env_binding(binding, path):
54 env(binding.name, binding.get_value(path, os.environ.get(binding.name, None)))
56 def correct_for_64bit(base, rel_path):
57 """If rel_path starts lib or usr/lib and doesn't exist, try with lib64 instead."""
58 if os.path.exists(os.path.join(base, rel_path)):
59 return rel_path
61 if rel_path.startswith('lib/') or rel_path.startswith('usr/lib/'):
62 new_rel_path = rel_path.replace('lib/', 'lib64/', 1)
63 if os.path.exists(os.path.join(base, new_rel_path)):
64 return new_rel_path
66 return rel_path
68 def write_pc(name, lines):
69 overrides_dir = os.path.join(os.environ['TMPDIR'], PKG_CONFIG_OVERRIDES)
70 if not os.path.isdir(overrides_dir):
71 os.mkdir(overrides_dir)
72 stream = open(os.path.join(overrides_dir, name), 'w')
73 stream.write(''.join(lines))
74 stream.close()
76 def do_pkg_config_binding(binding, impl):
77 feed_name = impl.feed.split('/')[-1]
78 path = lookup(impl.id)
79 new_insert = correct_for_64bit(path, binding.insert)
80 if new_insert != binding.insert:
81 print "PKG_CONFIG_PATH dir <%s>/%s not found; using %s instead" % (feed_name, binding.insert, new_insert)
82 binding = model.EnvironmentBinding(binding.name,
83 new_insert,
84 binding.default,
85 binding.mode)
87 orig_path = os.path.join(path, binding.insert)
88 if os.path.isdir(orig_path):
89 for pc in os.listdir(orig_path):
90 stream = open(os.path.join(orig_path, pc))
91 lines = stream.readlines()
92 stream.close()
93 for i, line in enumerate(lines):
94 if '=' not in line: continue
95 name, value = [x.strip() for x in line.split('=', 1)]
96 if name == 'prefix' and value.startswith('/'):
97 print "Absolute prefix=%s in %s; overriding..." % (value, feed_name)
98 lines[i] = 'prefix=%s/%s\n' % (path, value[1:])
99 write_pc(pc, lines)
100 break
101 do_env_binding(binding, path)
103 def fixup_generated_pkgconfig_file(pc_file):
104 stream = open(pc_file)
105 lines = stream.readlines()
106 stream.close()
107 for i, line in enumerate(lines):
108 if '=' not in line: continue
109 name, value = [x.strip() for x in line.split('=', 1)]
110 if name == 'prefix' and value.startswith('/'):
111 print "Absolute prefix=%s in %s; fixing..." % (value, pc_file)
112 rel_path = relpath(value, os.path.dirname(pc_file)) # Requires Python 2.6
113 lines[i] = 'prefix=${pcfiledir}/%s\n' % rel_path
114 write_pc(pc_file, lines)
115 break
117 # After doing a build, check that we didn't generate pkgconfig files with absolute paths
118 # Rewrite if so
119 def fixup_generated_pkgconfig_files():
120 for root, dirs, files in os.walk(os.environ['DISTDIR']):
121 if os.path.basename(root) == 'pkgconfig':
122 for f in files:
123 if f.endswith('.pc'):
124 info("Checking generated pkgconfig file '%s'", f)
125 fixup_generated_pkgconfig_file(os.path.join(root, f))
127 def remove_la_file(path):
128 # Read the contents...
129 stream = open(path)
130 data = stream.read()
131 stream.close()
133 # Check it really is a libtool archive...
134 if 'Please DO NOT delete this file' not in data:
135 warn("Ignoring %s; doesn't look like a libtool archive", path)
136 return
138 os.unlink(path)
139 print "Removed %s (.la files contain absolute paths)" % path
141 # libtool archives contain hard-coded paths. Lucky, modern systems don't need them, so remove
142 # them.
143 def remove_la_files():
144 for root, dirs, files in os.walk(os.environ['DISTDIR']):
145 if os.path.basename(root) == 'lib':
146 for f in files:
147 if f.endswith('.la'):
148 remove_la_file(os.path.join(root, f))
149 if f.endswith('.a'):
150 warn("Found static archive '%s'; maybe build with --disable-static?", f)
152 def do_build_internal(options, args):
153 """build-internal"""
154 # If a sandbox is being used, we're in it now.
155 import getpass, socket, time
157 buildenv = BuildEnv()
158 sels = buildenv.get_selections()
160 builddir = os.path.realpath('build')
161 ensure_dir(buildenv.metadir)
163 build_env_xml = join(buildenv.metadir, 'build-environment.xml')
165 buildenv_doc = buildenv.get_selections().toDOM()
167 # Create build-environment.xml file
168 root = buildenv_doc.documentElement
169 info = buildenv_doc.createElementNS(XMLNS_0COMPILE, 'build-info')
170 root.appendChild(info)
171 info.setAttributeNS(None, 'time', time.strftime('%Y-%m-%d %H:%M').strip())
172 info.setAttributeNS(None, 'host', socket.getfqdn())
173 info.setAttributeNS(None, 'user', getpass.getuser())
174 uname = os.uname()
175 info.setAttributeNS(None, 'arch', '%s-%s' % (uname[0], uname[4]))
176 stream = file(build_env_xml, 'w')
177 buildenv_doc.writexml(stream, addindent=" ", newl="\n")
178 stream.close()
180 # Create local binary interface file
181 src_iface = iface_cache.get_interface(buildenv.interface)
182 src_impl = buildenv.chosen_impl(buildenv.interface)
183 write_sample_interface(buildenv, src_iface, src_impl)
185 # Check 0compile is new enough
186 min_version = parse_version(src_impl.attrs.get(XMLNS_0COMPILE + ' min-version', None))
187 if min_version and min_version > parse_version(__main__.version):
188 raise SafeException("%s-%s requires 0compile >= %s, but we are only version %s" %
189 (src_iface.get_name(), src_impl.version, format_version(min_version), __main__.version))
191 # Create the patch
192 patch_file = join(buildenv.metadir, 'from-%s.patch' % src_impl.version)
193 if buildenv.user_srcdir:
194 # (ignore errors; will already be shown on stderr)
195 os.system("diff -urN '%s' src > %s" %
196 (buildenv.orig_srcdir.replace('\\', '\\\\').replace("'", "\\'"),
197 patch_file))
198 if os.path.getsize(patch_file) == 0:
199 os.unlink(patch_file)
200 elif os.path.exists(patch_file):
201 os.unlink(patch_file)
203 env('BUILDDIR', builddir)
204 env('DISTDIR', buildenv.distdir)
205 env('SRCDIR', buildenv.user_srcdir or buildenv.orig_srcdir)
206 os.chdir(builddir)
207 print "cd", builddir
209 for needed_iface in sels.selections:
210 impl = buildenv.chosen_impl(needed_iface)
211 assert impl
212 for dep in impl.dependencies:
213 dep_iface = sels.selections[dep.interface]
214 for b in dep.bindings:
215 if isinstance(b, EnvironmentBinding):
216 dep_impl = buildenv.chosen_impl(dep.interface)
217 if b.name == 'PKG_CONFIG_PATH':
218 do_pkg_config_binding(b, dep_impl)
219 else:
220 do_env_binding(b, lookup(dep_impl.id))
222 mappings = {}
223 for impl in sels.selections.values():
224 new_mappings = impl.attrs.get(XMLNS_0COMPILE + ' lib-mappings', '')
225 if new_mappings:
226 new_mappings = new_mappings.split(' ')
227 for mapping in new_mappings:
228 assert ':' in mapping, "lib-mappings missing ':' in '%s' from '%s'" % (mapping, impl.feed)
229 name, major_version = mapping.split(':', 1)
230 assert '/' not in mapping, "lib-mappings '%s' contains a / in the version number (from '%s')!" % (mapping, impl.feed)
231 mappings[name] = 'lib%s.so.%s' % (name, major_version)
232 impl_path = lookup(impl.id)
233 for libdirname in ['lib', 'usr/lib', 'lib64', 'usr/lib64']:
234 libdir = os.path.join(impl_path, libdirname)
235 if os.path.isdir(libdir):
236 find_broken_version_symlinks(libdir, mappings)
238 if mappings:
239 set_up_mappings(mappings)
241 overrides_dir = os.path.join(os.environ['TMPDIR'], PKG_CONFIG_OVERRIDES)
242 if os.path.isdir(overrides_dir):
243 add_overrides = model.EnvironmentBinding('PKG_CONFIG_PATH', PKG_CONFIG_OVERRIDES)
244 do_env_binding(add_overrides, os.environ['TMPDIR'])
246 # Some programs want to put temporary build files in the source directory.
247 # Make a copy of the source if needed.
248 dup_src_type = src_impl.attrs.get(XMLNS_0COMPILE + ' dup-src', None)
249 if dup_src_type == 'true':
250 dup_src(shutil.copy2)
251 env('SRCDIR', builddir)
252 elif dup_src_type:
253 raise Exception("Unknown dup-src value '%s'" % dup_src_type)
255 if options.shell:
256 spawn_and_check(find_in_path('sh'), [])
257 else:
258 command = src_impl.attrs[XMLNS_0COMPILE + ' command']
260 # Remove any existing log files
261 for log in ['build.log', 'build-success.log', 'build-failure.log']:
262 if os.path.exists(log):
263 os.unlink(log)
265 # Run the command, copying output to a new log
266 log = file('build.log', 'w')
267 try:
268 print >>log, "Build log for %s-%s" % (src_iface.get_name(),
269 src_impl.version)
270 print >>log, "\nBuilt using 0compile-%s" % __main__.version
271 print >>log, "\nBuild system: " + ', '.join(uname)
272 print >>log, "\n%s:\n" % ENV_FILE
273 shutil.copyfileobj(file("../" + ENV_FILE), log)
275 log.write('\n')
277 if os.path.exists(patch_file):
278 print >>log, "\nPatched with:\n"
279 shutil.copyfileobj(file(patch_file), log)
280 log.write('\n')
282 print "Executing: " + command
283 print >>log, "Executing: " + command
285 # Tee the output to the console and to the log
286 child = subprocess.Popen(command, shell = True, stdout = subprocess.PIPE, stderr = subprocess.STDOUT)
287 while True:
288 data = os.read(child.stdout.fileno(), 100)
289 if not data: break
290 sys.stdout.write(data)
291 log.write(data)
292 status = child.wait()
293 failure = None
294 if status == 0:
295 print >>log, "Build successful"
296 fixup_generated_pkgconfig_files()
297 remove_la_files()
298 elif status > 0:
299 failure = "Build failed with exit code %d" % status
300 else:
301 failure = "Build failure: exited due to signal %d" % (-status)
302 if failure:
303 print >>log, failure
304 os.rename('build.log', 'build-failure.log')
305 raise SafeException("Command '%s': %s" % (command, failure))
306 else:
307 os.rename('build.log', 'build-success.log')
308 finally:
309 log.close()
311 def do_build(args):
312 """build [ --no-sandbox ] [ --shell | --force | --clean ]"""
313 buildenv = BuildEnv()
314 sels = buildenv.get_selections()
316 parser = OptionParser(usage="usage: %prog build [options]")
318 parser.add_option('', "--no-sandbox", help="disable use of sandboxing", action='store_true')
319 parser.add_option("-s", "--shell", help="run a shell instead of building", action='store_true')
320 parser.add_option("-c", "--clean", help="remove the build directories", action='store_true')
321 parser.add_option("-f", "--force", help="build even if dependencies have changed", action='store_true')
323 parser.disable_interspersed_args()
325 (options, args2) = parser.parse_args(args)
327 builddir = os.path.realpath('build')
329 changes = buildenv.get_build_changes()
330 if changes:
331 if not (options.force or options.clean):
332 raise SafeException("Build dependencies have changed:\n" +
333 '\n'.join(changes) + "\n\n" +
334 "To build anyway, use: 0compile build --force\n" +
335 "To do a clean build: 0compile build --clean")
336 if not options.no_sandbox:
337 print "Build dependencies have changed:\n" + '\n'.join(changes)
339 ensure_dir(builddir, options.clean)
340 ensure_dir(buildenv.distdir, options.clean)
342 if options.no_sandbox:
343 return do_build_internal(options, args2)
345 tmpdir = tempfile.mkdtemp(prefix = '0compile-')
346 try:
347 my_dir = os.path.dirname(__file__)
348 readable = ['.', my_dir]
349 writable = ['build', buildenv.distdir, tmpdir]
350 env('TMPDIR', tmpdir)
352 for selection in sels.selections.values():
353 readable.append(lookup(selection.id))
355 options = []
356 if __main__.options.verbose:
357 options.append('--verbose')
359 readable.append('/etc') # /etc/ld.*
361 spawn_maybe_sandboxed(readable, writable, tmpdir, sys.executable, ['-u', sys.argv[0]] + options + ['build', '--no-sandbox'] + args)
362 finally:
363 info("Deleting temporary directory '%s'" % tmpdir)
364 shutil.rmtree(tmpdir)
366 def write_sample_interface(buildenv, iface, src_impl):
367 path = buildenv.local_iface_file
368 target_arch = buildenv.target_arch
370 impl = minidom.getDOMImplementation()
372 XMLNS_IFACE = namespaces.XMLNS_IFACE
374 doc = impl.createDocument(XMLNS_IFACE, "interface", None)
376 root = doc.documentElement
377 root.setAttributeNS(XMLNS_NAMESPACE, 'xmlns', XMLNS_IFACE)
379 def addSimple(parent, name, text = None):
380 elem = doc.createElementNS(XMLNS_IFACE, name)
382 parent.appendChild(doc.createTextNode('\n' + ' ' * (1 + depth(parent))))
383 parent.appendChild(elem)
384 if text:
385 elem.appendChild(doc.createTextNode(text))
386 return elem
388 def close(element):
389 element.appendChild(doc.createTextNode('\n' + ' ' * depth(element)))
391 addSimple(root, 'name', iface.name)
392 addSimple(root, 'summary', iface.summary)
393 addSimple(root, 'description', iface.description)
394 feed_for = addSimple(root, 'feed-for')
396 uri = iface.uri
397 if uri.startswith('/'):
398 print "Note: source %s is a local feed" % iface.uri
399 for feed_uri in iface.feed_for or []:
400 uri = feed_uri
401 print "Will use <feed-for interface='%s'> instead..." % uri
402 break
403 else:
404 master_feed = minidom.parse(uri).documentElement
405 if master_feed.hasAttribute('uri'):
406 uri = master_feed.getAttribute('uri')
407 print "Will use <feed-for interface='%s'> instead..." % uri
409 feed_for.setAttributeNS(None, 'interface', uri)
411 group = addSimple(root, 'group')
412 main = src_impl.attrs.get(XMLNS_0COMPILE + ' binary-main', None)
413 if main:
414 group.setAttributeNS(None, 'main', main)
416 lib_mappings = src_impl.attrs.get(XMLNS_0COMPILE + ' binary-lib-mappings', None)
417 if lib_mappings:
418 root.setAttributeNS(XMLNS_NAMESPACE, 'xmlns:compile', XMLNS_0COMPILE)
419 group.setAttributeNS(XMLNS_0COMPILE, 'compile:lib-mappings', lib_mappings)
421 for d in src_impl.dependencies:
422 # 0launch < 0.32 messed up the namespace...
423 if parse_bool(d.metadata.get('include-binary', 'false')) or \
424 parse_bool(d.metadata.get(XMLNS_0COMPILE + ' include-binary', 'false')):
425 requires = addSimple(group, 'requires')
426 requires.setAttributeNS(None, 'interface', d.interface)
427 for b in d.bindings:
428 if isinstance(b, model.EnvironmentBinding):
429 env_elem = addSimple(requires, 'environment')
430 env_elem.setAttributeNS(None, 'name', b.name)
431 env_elem.setAttributeNS(None, 'insert', b.insert)
432 if b.default:
433 env_elem.setAttributeNS(None, 'default', b.default)
434 else:
435 raise Exception('Unknown binding type ' + b)
436 close(requires)
438 group.setAttributeNS(None, 'arch', target_arch)
439 impl_elem = addSimple(group, 'implementation')
440 impl_elem.setAttributeNS(None, 'version', src_impl.version)
442 version_modifier = buildenv.version_modifier
443 if version_modifier:
444 impl_elem.setAttributeNS(None, 'version-modifier', version_modifier)
446 impl_elem.setAttributeNS(None, 'id', '..')
447 impl_elem.setAttributeNS(None, 'released', time.strftime('%Y-%m-%d'))
448 close(group)
449 close(root)
451 stream = codecs.open(path, 'w', encoding = 'utf-8')
452 try:
453 doc.writexml(stream)
454 finally:
455 stream.close()
457 def find_broken_version_symlinks(libdir, mappings):
458 """libdir may be a legacy -devel package containing lib* symlinks whose
459 targets would be provided by the corresponding runtime package. If so,
460 create fixed symlinks under $TMPDIR with the real location."""
461 for x in os.listdir(libdir):
462 if x.startswith('lib') and x.endswith('.so'):
463 path = os.path.join(libdir, x)
464 if os.path.islink(path):
465 target = os.readlink(path)
466 if '/' not in target and not os.path.exists(os.path.join(libdir, target)):
467 print "Broken link %s -> %s; will relocate..." % (x, target)
468 mappings[x[3:-3]] = target
470 def set_up_mappings(mappings):
471 """Create a temporary directory with symlinks for each of the library mappings."""
472 libdirs = []
473 for d in os.environ.get('LD_LIBRARY_PATH', '').split(':'):
474 if d: libdirs.append(d)
475 libdirs += ['/lib', '/usr/lib']
477 def add_ldconf(config_file):
478 if not os.path.isfile(config_file):
479 return
480 for line in file(config_file):
481 d = line.strip()
482 if d.startswith('include '):
483 glob_pattern = d.split(' ', 1)[1]
484 for conf in glob.glob(glob_pattern):
485 add_ldconf(conf)
486 elif d and not d.startswith('#'):
487 libdirs.append(d)
488 add_ldconf('/etc/ld.so.conf')
490 def find_library(name, wanted):
491 # Takes a short-name and target name of a library and returns
492 # the full path of the library.
493 for d in libdirs:
494 path = os.path.join(d, wanted)
495 if os.path.exists(path):
496 return path
497 print "WARNING: library '%s' not found (searched '%s')!" % (wanted, libdirs)
498 return None
500 mappings_dir = os.path.join(os.environ['TMPDIR'], 'lib-mappings')
501 os.mkdir(mappings_dir)
503 old_path = os.environ.get('LIBRARY_PATH', '')
504 if old_path: old_path = ':' + old_path
505 os.environ['LIBRARY_PATH'] = mappings_dir + old_path
507 for name, wanted in mappings.items():
508 target = find_library(name, wanted)
509 if target:
510 print "Adding mapping lib%s.so -> %s" % (name, target)
511 os.symlink(target, os.path.join(mappings_dir, 'lib' + name + '.so'))
513 def dup_src(fn):
514 srcdir = os.environ['SRCDIR'] + '/'
515 for root, dirs, files in os.walk(srcdir):
516 assert root.startswith(srcdir)
517 reldir = root[len(srcdir):]
518 for f in files:
519 target = os.path.join(reldir, f)
520 #print "Copy %s -> %s" % (os.path.join(root, f), target)
521 if os.path.exists(target):
522 os.unlink(target)
523 fn(os.path.join(root, f), target)
524 for d in dirs:
525 target = os.path.join(reldir, d)
526 if not os.path.isdir(target):
527 os.mkdir(target)
529 __main__.commands.append(do_build)