Intelligent handling of legacy PKG_CONFIG_PATH dirs
[0compile.git] / build.py
blob337bf60765779c820e2aa38aad14e472d3276064
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
7 from xml.dom import minidom, XMLNS_NAMESPACE
8 from optparse import OptionParser
10 from support import *
12 # If we have to modify any pkg-config files, we put the new versions in $TMPDIR/PKG_CONFIG_OVERRIDES
13 PKG_CONFIG_OVERRIDES = 'pkg-config-overrides'
15 def env(name, value):
16 os.environ[name] = value
17 print "%s=%s" % (name, value)
19 def do_env_binding(binding, path):
20 env(binding.name, binding.get_value(path, os.environ.get(binding.name, None)))
22 def correct_for_64bit(base, rel_path):
23 """If rel_path starts lib or usr/lib and doesn't exist, try with lib64 instead."""
24 if os.path.exists(os.path.join(base, rel_path)):
25 return rel_path
27 if rel_path.startswith('lib/') or rel_path.startswith('usr/lib/'):
28 new_rel_path = rel_path.replace('lib/', 'lib64/', 1)
29 if os.path.exists(os.path.join(base, new_rel_path)):
30 return new_rel_path
32 return rel_path
34 def write_pc(name, lines):
35 overrides_dir = os.path.join(os.environ['TMPDIR'], PKG_CONFIG_OVERRIDES)
36 if not os.path.isdir(overrides_dir):
37 os.mkdir(overrides_dir)
38 stream = open(os.path.join(overrides_dir, name), 'w')
39 stream.write(''.join(lines))
40 stream.close()
42 def do_pkg_config_binding(binding, impl):
43 feed_name = impl.feed.split('/')[-1]
44 path = lookup(impl.id)
45 new_insert = correct_for_64bit(path, binding.insert)
46 if new_insert != binding.insert:
47 print "PKG_CONFIG_PATH dir <%s>/%s not found; using %s instead" % (feed_name, binding.insert, new_insert)
48 binding = model.EnvironmentBinding(binding.name,
49 new_insert,
50 binding.default,
51 binding.mode)
53 orig_path = os.path.join(path, binding.insert)
54 if os.path.isdir(orig_path):
55 for pc in os.listdir(orig_path):
56 stream = open(os.path.join(orig_path, pc))
57 lines = stream.readlines()
58 stream.close()
59 for i, line in enumerate(lines):
60 if '=' not in line: continue
61 name, value = [x.strip() for x in line.split('=', 1)]
62 if name == 'prefix' and value.startswith('/'):
63 print "Absolute prefix=%s in %s; overriding..." % (value, feed_name)
64 lines[i] = 'prefix=%s/%s\n' % (path, value[1:])
65 write_pc(pc, lines)
66 break
67 do_env_binding(binding, path)
69 def do_build_internal(options, args):
70 """build-internal"""
71 # If a sandbox is being used, we're in it now.
72 import getpass, socket, time
74 buildenv = BuildEnv()
75 sels = buildenv.get_selections()
77 builddir = os.path.realpath('build')
78 ensure_dir(buildenv.metadir)
80 build_env_xml = join(buildenv.metadir, 'build-environment.xml')
82 buildenv_doc = buildenv.get_selections().toDOM()
84 # Create build-environment.xml file
85 root = buildenv_doc.documentElement
86 info = buildenv_doc.createElementNS(XMLNS_0COMPILE, 'build-info')
87 root.appendChild(info)
88 info.setAttributeNS(None, 'time', time.strftime('%Y-%m-%d %H:%M').strip())
89 info.setAttributeNS(None, 'host', socket.getfqdn())
90 info.setAttributeNS(None, 'user', getpass.getuser())
91 uname = os.uname()
92 info.setAttributeNS(None, 'arch', '%s-%s' % (uname[0], uname[4]))
93 stream = file(build_env_xml, 'w')
94 buildenv_doc.writexml(stream, addindent=" ", newl="\n")
95 stream.close()
97 # Create local binary interface file
98 src_iface = iface_cache.get_interface(buildenv.interface)
99 src_impl = buildenv.chosen_impl(buildenv.interface)
100 write_sample_interface(buildenv, src_iface, src_impl)
102 # Check 0compile is new enough
103 min_version = parse_version(src_impl.attrs.get(XMLNS_0COMPILE + ' min-version', None))
104 if min_version and min_version > parse_version(__main__.version):
105 raise SafeException("%s-%s requires 0compile >= %s, but we are only version %s" %
106 (src_iface.get_name(), src_impl.version, format_version(min_version), __main__.version))
108 # Create the patch
109 patch_file = join(buildenv.metadir, 'from-%s.patch' % src_impl.version)
110 if buildenv.user_srcdir:
111 # (ignore errors; will already be shown on stderr)
112 os.system("diff -urN '%s' src > %s" %
113 (buildenv.orig_srcdir.replace('\\', '\\\\').replace("'", "\\'"),
114 patch_file))
115 if os.path.getsize(patch_file) == 0:
116 os.unlink(patch_file)
117 elif os.path.exists(patch_file):
118 os.unlink(patch_file)
120 env('BUILDDIR', builddir)
121 env('DISTDIR', buildenv.distdir)
122 env('SRCDIR', buildenv.user_srcdir or buildenv.orig_srcdir)
123 os.chdir(builddir)
124 print "cd", builddir
126 for needed_iface in sels.selections:
127 impl = buildenv.chosen_impl(needed_iface)
128 assert impl
129 for dep in impl.dependencies:
130 dep_iface = sels.selections[dep.interface]
131 for b in dep.bindings:
132 if isinstance(b, EnvironmentBinding):
133 dep_impl = buildenv.chosen_impl(dep.interface)
134 if b.name == 'PKG_CONFIG_PATH':
135 do_pkg_config_binding(b, dep_impl)
136 else:
137 do_env_binding(b, lookup(dep_impl.id))
139 mappings = []
140 for impl in sels.selections.values():
141 new_mappings = impl.attrs.get(XMLNS_0COMPILE + ' lib-mappings', '')
142 if new_mappings:
143 new_mappings = new_mappings.split(' ')
144 for mapping in new_mappings:
145 assert ':' in mapping, "lib-mappings missing ':' in '%s' from '%s'" % (mapping, impl.feed)
146 name, major_version = mapping.split(':', 1)
147 assert '/' not in mapping, "lib-mappings '%s' contains a / in the version number (from '%s')!" % (mapping, impl.feed)
148 mappings.append((name, major_version))
150 if mappings:
151 set_up_mappings(mappings)
153 overrides_dir = os.path.join(os.environ['TMPDIR'], PKG_CONFIG_OVERRIDES)
154 if os.path.isdir(overrides_dir):
155 add_overrides = model.EnvironmentBinding('PKG_CONFIG_PATH', PKG_CONFIG_OVERRIDES)
156 do_env_binding(add_overrides, os.environ['TMPDIR'])
158 # Some programs want to put temporary build files in the source directory.
159 # Make a copy of the source if needed.
160 dup_src_type = src_impl.attrs.get(XMLNS_0COMPILE + ' dup-src', None)
161 if dup_src_type == 'true':
162 dup_src(shutil.copy2)
163 env('SRCDIR', builddir)
164 elif dup_src_type:
165 raise Exception("Unknown dup-src value '%s'" % dup_src_type)
167 if options.shell:
168 spawn_and_check(find_in_path('sh'), [])
169 else:
170 command = src_impl.attrs[XMLNS_0COMPILE + ' command']
172 # Remove any existing log files
173 for log in ['build.log', 'build-success.log', 'build-failure.log']:
174 if os.path.exists(log):
175 os.unlink(log)
177 # Run the command, copying output to a new log
178 log = file('build.log', 'w')
179 try:
180 print >>log, "Build log for %s-%s" % (src_iface.get_name(),
181 src_impl.version)
182 print >>log, "\nBuilt using 0compile-%s" % __main__.version
183 print >>log, "\nBuild system: " + ', '.join(uname)
184 print >>log, "\n%s:\n" % ENV_FILE
185 shutil.copyfileobj(file("../" + ENV_FILE), log)
187 log.write('\n')
189 if os.path.exists(patch_file):
190 print >>log, "\nPatched with:\n"
191 shutil.copyfileobj(file(patch_file), log)
192 log.write('\n')
194 print "Executing: " + command
195 print >>log, "Executing: " + command
197 # Tee the output to the console and to the log
198 child = subprocess.Popen(command, shell = True, stdout = subprocess.PIPE, stderr = subprocess.STDOUT)
199 while True:
200 data = os.read(child.stdout.fileno(), 100)
201 if not data: break
202 sys.stdout.write(data)
203 log.write(data)
204 status = child.wait()
205 failure = None
206 if status == 0:
207 print >>log, "Build successful"
208 elif status > 0:
209 failure = "Build failed with exit code %d" % status
210 else:
211 failure = "Build failure: exited due to signal %d" % (-status)
212 if failure:
213 print >>log, failure
214 os.rename('build.log', 'build-failure.log')
215 raise SafeException("Command '%s': %s" % (command, failure))
216 else:
217 os.rename('build.log', 'build-success.log')
218 finally:
219 log.close()
221 def do_build(args):
222 """build [ --no-sandbox ] [ --shell | --force | --clean ]"""
223 buildenv = BuildEnv()
224 sels = buildenv.get_selections()
226 parser = OptionParser(usage="usage: %prog build [options]")
228 parser.add_option('', "--no-sandbox", help="disable use of sandboxing", action='store_true')
229 parser.add_option("-s", "--shell", help="run a shell instead of building", action='store_true')
230 parser.add_option("-c", "--clean", help="remove the build directories", action='store_true')
231 parser.add_option("-f", "--force", help="build even if dependencies have changed", action='store_true')
233 parser.disable_interspersed_args()
235 (options, args2) = parser.parse_args(args)
237 builddir = os.path.realpath('build')
239 changes = buildenv.get_build_changes()
240 if changes:
241 if not (options.force or options.clean):
242 raise SafeException("Build dependencies have changed:\n" +
243 '\n'.join(changes) + "\n\n" +
244 "To build anyway, use: 0compile build --force\n" +
245 "To do a clean build: 0compile build --clean")
246 if not options.no_sandbox:
247 print "Build dependencies have changed:\n" + '\n'.join(changes)
249 ensure_dir(builddir, options.clean)
250 ensure_dir(buildenv.distdir, options.clean)
252 if options.no_sandbox:
253 return do_build_internal(options, args2)
255 tmpdir = tempfile.mkdtemp(prefix = '0compile-')
256 try:
257 my_dir = os.path.dirname(__file__)
258 readable = ['.', my_dir]
259 writable = ['build', buildenv.distdir, tmpdir]
260 env('TMPDIR', tmpdir)
262 for selection in sels.selections.values():
263 readable.append(lookup(selection.id))
265 options = []
266 if __main__.options.verbose:
267 options.append('--verbose')
269 readable.append('/etc') # /etc/ld.*
271 spawn_maybe_sandboxed(readable, writable, tmpdir, sys.executable, [sys.argv[0]] + options + ['build', '--no-sandbox'] + args)
272 finally:
273 info("Deleting temporary directory '%s'" % tmpdir)
274 shutil.rmtree(tmpdir)
276 def write_sample_interface(buildenv, iface, src_impl):
277 path = buildenv.local_iface_file
278 target_arch = buildenv.target_arch
280 impl = minidom.getDOMImplementation()
282 XMLNS_IFACE = namespaces.XMLNS_IFACE
284 doc = impl.createDocument(XMLNS_IFACE, "interface", None)
286 root = doc.documentElement
287 root.setAttributeNS(XMLNS_NAMESPACE, 'xmlns', XMLNS_IFACE)
289 def addSimple(parent, name, text = None):
290 elem = doc.createElementNS(XMLNS_IFACE, name)
292 parent.appendChild(doc.createTextNode('\n' + ' ' * (1 + depth(parent))))
293 parent.appendChild(elem)
294 if text:
295 elem.appendChild(doc.createTextNode(text))
296 return elem
298 def close(element):
299 element.appendChild(doc.createTextNode('\n' + ' ' * depth(element)))
301 addSimple(root, 'name', iface.name)
302 addSimple(root, 'summary', iface.summary)
303 addSimple(root, 'description', iface.description)
304 feed_for = addSimple(root, 'feed-for')
306 uri = iface.uri
307 if uri.startswith('/') and iface.feed_for:
308 for uri in iface.feed_for:
309 print "Note: source %s is a local feed" % iface.uri
310 print "Will use <feed-for interface='%s'> instead..." % uri
311 break
313 feed_for.setAttributeNS(None, 'interface', uri)
315 group = addSimple(root, 'group')
316 main = src_impl.attrs.get(XMLNS_0COMPILE + ' binary-main', None)
317 if main:
318 group.setAttributeNS(None, 'main', main)
320 lib_mappings = src_impl.attrs.get(XMLNS_0COMPILE + ' binary-lib-mappings', None)
321 if lib_mappings:
322 root.setAttributeNS(XMLNS_NAMESPACE, 'xmlns:compile', XMLNS_0COMPILE)
323 group.setAttributeNS(XMLNS_0COMPILE, 'compile:lib-mappings', lib_mappings)
325 for d in src_impl.dependencies:
326 # 0launch < 0.32 messed up the namespace...
327 if parse_bool(d.metadata.get('include-binary', 'false')) or \
328 parse_bool(d.metadata.get(XMLNS_0COMPILE + ' include-binary', 'false')):
329 requires = addSimple(group, 'requires')
330 requires.setAttributeNS(None, 'interface', d.interface)
331 for b in d.bindings:
332 if isinstance(b, model.EnvironmentBinding):
333 env_elem = addSimple(requires, 'environment')
334 env_elem.setAttributeNS(None, 'name', b.name)
335 env_elem.setAttributeNS(None, 'insert', b.insert)
336 if b.default:
337 env_elem.setAttributeNS(None, 'default', b.default)
338 else:
339 raise Exception('Unknown binding type ' + b)
340 close(requires)
342 group.setAttributeNS(None, 'arch', target_arch)
343 impl_elem = addSimple(group, 'implementation')
344 impl_elem.setAttributeNS(None, 'version', src_impl.version)
346 version_modifier = buildenv.version_modifier
347 if version_modifier:
348 impl_elem.setAttributeNS(None, 'version-modifier', version_modifier)
350 impl_elem.setAttributeNS(None, 'id', '..')
351 impl_elem.setAttributeNS(None, 'released', time.strftime('%Y-%m-%d'))
352 close(group)
353 close(root)
355 stream = codecs.open(path, 'w', encoding = 'utf-8')
356 try:
357 doc.writexml(stream)
358 finally:
359 stream.close()
361 def set_up_mappings(mappings):
362 """Create a temporary directory with symlinks for each of the library mappings."""
363 # The find_library function takes a short-name and major version of a library and
364 # returns the full path of the library.
365 libdirs = ['/lib', '/usr/lib']
366 for d in os.environ.get('LD_LIBRARY_PATH', '').split(':'):
367 if d: libdirs.append(d)
369 def add_ldconf(config_file):
370 if not os.path.isfile(config_file):
371 return
372 for line in file(config_file):
373 d = line.strip()
374 if d.startswith('include '):
375 glob_pattern = d.split(' ', 1)[1]
376 for conf in glob.glob(glob_pattern):
377 add_ldconf(conf)
378 elif d and not d.startswith('#'):
379 libdirs.append(d)
380 add_ldconf('/etc/ld.so.conf')
382 def find_library(name, major):
383 wanted = 'lib%s.so.%s' % (name, major)
384 for d in libdirs:
385 path = os.path.join(d, wanted)
386 if os.path.exists(path):
387 return path
388 print "WARNING: library '%s' not found (searched '%s')!" % (wanted, libdirs)
389 return None
391 mappings_dir = os.path.join(os.environ['TMPDIR'], 'lib-mappings')
392 os.mkdir(mappings_dir)
394 old_path = os.environ.get('LIBRARY_PATH', '')
395 if old_path: old_path = ':' + old_path
396 os.environ['LIBRARY_PATH'] = mappings_dir + old_path
398 for name, major_version in mappings:
399 target = find_library(name, major_version)
400 if target:
401 print "Adding mapping lib%s.so -> %s" % (name, target)
402 os.symlink(target, os.path.join(mappings_dir, 'lib' + name + '.so'))
404 def dup_src(fn):
405 srcdir = os.environ['SRCDIR'] + '/'
406 for root, dirs, files in os.walk(srcdir):
407 assert root.startswith(srcdir)
408 reldir = root[len(srcdir):]
409 for f in files:
410 target = os.path.join(reldir, f)
411 #print "Copy %s -> %s" % (os.path.join(root, f), target)
412 if os.path.exists(target):
413 os.unlink(target)
414 fn(os.path.join(root, f), target)
415 for d in dirs:
416 target = os.path.join(reldir, d)
417 if not os.path.isdir(target):
418 os.mkdir(target)
420 __main__.commands.append(do_build)