Added 'clean' command
[0compile.git] / build.py
blob36d4878670225762eb2495187875452dea0ad94e
1 # Copyright (C) 2006, Thomas Leonard
2 # See http://0install.net/0compile.html
4 import sys, os, __main__, time, shutil, glob, codecs
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 def env(name, value):
13 os.environ[name] = value
14 print "%s=%s" % (name, value)
16 def do_env_binding(binding, path):
17 env(binding.name, binding.get_value(path, os.environ.get(binding.name, None)))
19 def do_build_internal(options, args):
20 """build-internal"""
21 # If a sandbox is being used, we're in it now.
22 import getpass, socket, time
24 buildenv = BuildEnv()
25 sels = buildenv.get_selections()
27 builddir = os.path.realpath('build')
28 ensure_dir(buildenv.metadir)
30 build_env_xml = join(buildenv.metadir, 'build-environment.xml')
32 buildenv_doc = buildenv.get_selections().toDOM()
34 # Create build-environment.xml file
35 root = buildenv_doc.documentElement
36 info = buildenv_doc.createElementNS(XMLNS_0COMPILE, 'build-info')
37 root.appendChild(info)
38 info.setAttributeNS(None, 'time', time.strftime('%Y-%m-%d %H:%M').strip())
39 info.setAttributeNS(None, 'host', socket.getfqdn())
40 info.setAttributeNS(None, 'user', getpass.getuser())
41 uname = os.uname()
42 info.setAttributeNS(None, 'arch', '%s-%s' % (uname[0], uname[4]))
43 stream = file(build_env_xml, 'w')
44 buildenv_doc.writexml(stream, addindent=" ", newl="\n")
45 stream.close()
47 # Create local binary interface file
48 src_iface = iface_cache.get_interface(buildenv.interface)
49 src_impl = buildenv.chosen_impl(buildenv.interface)
50 write_sample_interface(buildenv, src_iface, src_impl)
52 # Check 0compile is new enough
53 min_version = parse_version(src_impl.attrs.get(XMLNS_0COMPILE + ' min-version', None))
54 if min_version and min_version > parse_version(__main__.version):
55 raise SafeException("%s-%s requires 0compile >= %s, but we are only version %s" %
56 (src_iface.get_name(), src_impl.version, format_version(min_version), __main__.version))
58 # Create the patch
59 patch_file = join(buildenv.metadir, 'from-%s.patch' % src_impl.version)
60 if buildenv.user_srcdir:
61 # (ignore errors; will already be shown on stderr)
62 os.system("diff -urN '%s' src > %s" %
63 (buildenv.orig_srcdir.replace('\\', '\\\\').replace("'", "\\'"),
64 patch_file))
65 if os.path.getsize(patch_file) == 0:
66 os.unlink(patch_file)
67 elif os.path.exists(patch_file):
68 os.unlink(patch_file)
70 env('BUILDDIR', builddir)
71 env('DISTDIR', buildenv.distdir)
72 env('SRCDIR', buildenv.user_srcdir or buildenv.orig_srcdir)
73 os.chdir(builddir)
74 print "cd", builddir
76 for needed_iface in sels.selections:
77 impl = buildenv.chosen_impl(needed_iface)
78 assert impl
79 for dep in impl.dependencies:
80 dep_iface = sels.selections[dep.interface]
81 for b in dep.bindings:
82 if isinstance(b, EnvironmentBinding):
83 dep_impl = buildenv.chosen_impl(dep.interface)
84 do_env_binding(b, lookup(dep_impl.id))
86 mappings = []
87 for impl in sels.selections.values():
88 new_mappings = impl.attrs.get(XMLNS_0COMPILE + ' lib-mappings', '')
89 if new_mappings:
90 new_mappings = new_mappings.split(' ')
91 for mapping in new_mappings:
92 assert ':' in mapping, "lib-mappings missing ':' in '%s' from '%s'" % (mapping, impl.feed)
93 name, major_version = mapping.split(':', 1)
94 assert '/' not in mapping, "lib-mappings '%s' contains a / in the version number (from '%s')!" % (mapping, impl.feed)
95 mappings.append((name, major_version))
97 if mappings:
98 set_up_mappings(mappings)
100 # Some programs want to put temporary build files in the source directory.
101 # Make a copy of the source if needed.
102 dup_src_type = src_impl.attrs.get(XMLNS_0COMPILE + ' dup-src', None)
103 if dup_src_type == 'true':
104 dup_src(shutil.copy2)
105 env('SRCDIR', builddir)
106 elif dup_src_type:
107 raise Exception("Unknown dup-src value '%s'" % dup_src_type)
109 if options.shell:
110 spawn_and_check(find_in_path('sh'), [])
111 else:
112 command = src_impl.attrs[XMLNS_0COMPILE + ' command']
114 # Remove any existing log files
115 for log in ['build.log', 'build-success.log', 'build-failure.log']:
116 if os.path.exists(log):
117 os.unlink(log)
119 # Run the command, copying output to a new log
120 log = file('build.log', 'w')
121 try:
122 print >>log, "Build log for %s-%s" % (src_iface.get_name(),
123 src_impl.version)
124 print >>log, "\nBuilt using 0compile-%s" % __main__.version
125 print >>log, "\nBuild system: " + ', '.join(uname)
126 print >>log, "\n%s:\n" % ENV_FILE
127 shutil.copyfileobj(file("../" + ENV_FILE), log)
129 log.write('\n')
131 if os.path.exists(patch_file):
132 print >>log, "\nPatched with:\n"
133 shutil.copyfileobj(file(patch_file), log)
134 log.write('\n')
136 print "Executing: " + command
137 print >>log, "Executing: " + command
139 # Tee the output to the console and to the log
140 from popen2 import Popen4
141 child = Popen4(command)
142 child.tochild.close()
143 while True:
144 data = os.read(child.fromchild.fileno(), 100)
145 if not data: break
146 sys.stdout.write(data)
147 log.write(data)
148 status = child.wait()
149 failure = None
150 if os.WIFEXITED(status):
151 exit_code = os.WEXITSTATUS(status)
152 if exit_code == 0:
153 print >>log, "Build successful"
154 else:
155 failure = "Build failed with exit code %d" % exit_code
156 else:
157 failure = "Build failure: exited due to signal %d" % os.WTERMSIG(status)
158 if failure:
159 print >>log, failure
160 os.rename('build.log', 'build-failure.log')
161 raise SafeException("Command '%s': %s" % (command, failure))
162 else:
163 os.rename('build.log', 'build-success.log')
164 finally:
165 log.close()
167 def do_build(args):
168 """build [ --no-sandbox ] [ --shell | --force | --clean ]"""
169 buildenv = BuildEnv()
170 sels = buildenv.get_selections()
172 parser = OptionParser(usage="usage: %prog build [options]")
174 parser.add_option('', "--no-sandbox", help="disable use of sandboxing", action='store_true')
175 parser.add_option("-s", "--shell", help="run a shell instead of building", action='store_true')
176 parser.add_option("-c", "--clean", help="remove the build directories", action='store_true')
177 parser.add_option("-f", "--force", help="build even if dependencies have changed", action='store_true')
179 parser.disable_interspersed_args()
181 (options, args2) = parser.parse_args(args)
183 builddir = os.path.realpath('build')
185 changes = buildenv.get_build_changes()
186 if changes:
187 if not (options.force or options.clean):
188 raise SafeException("Build dependencies have changed:\n" +
189 '\n'.join(changes) + "\n\n" +
190 "To build anyway, use: 0compile build --force\n" +
191 "To do a clean build: 0compile build --clean")
192 if not options.no_sandbox:
193 print "Build dependencies have changed:\n" + '\n'.join(changes)
195 ensure_dir(builddir, options.clean)
196 ensure_dir(buildenv.distdir, options.clean)
198 if options.no_sandbox:
199 return do_build_internal(options, args2)
201 tmpdir = tempfile.mkdtemp(prefix = '0compile-')
202 try:
203 my_dir = os.path.dirname(__file__)
204 readable = ['.', my_dir]
205 writable = ['build', buildenv.distdir, tmpdir]
206 env('TMPDIR', tmpdir)
208 for selection in sels.selections.values():
209 readable.append(lookup(selection.id))
211 options = []
212 if __main__.options.verbose:
213 options.append('--verbose')
215 readable.append('/etc') # /etc/ld.*
217 spawn_maybe_sandboxed(readable, writable, tmpdir, sys.executable, [sys.argv[0]] + options + ['build', '--no-sandbox'] + args)
218 finally:
219 info("Deleting temporary directory '%s'" % tmpdir)
220 shutil.rmtree(tmpdir)
222 def write_sample_interface(buildenv, iface, src_impl):
223 path = buildenv.local_iface_file
224 target_arch = buildenv.target_arch
226 impl = minidom.getDOMImplementation()
228 XMLNS_IFACE = namespaces.XMLNS_IFACE
230 doc = impl.createDocument(XMLNS_IFACE, "interface", None)
232 root = doc.documentElement
233 root.setAttributeNS(XMLNS_NAMESPACE, 'xmlns', XMLNS_IFACE)
235 def addSimple(parent, name, text = None):
236 elem = doc.createElementNS(XMLNS_IFACE, name)
238 parent.appendChild(doc.createTextNode('\n' + ' ' * (1 + depth(parent))))
239 parent.appendChild(elem)
240 if text:
241 elem.appendChild(doc.createTextNode(text))
242 return elem
244 def close(element):
245 element.appendChild(doc.createTextNode('\n' + ' ' * depth(element)))
247 addSimple(root, 'name', iface.name)
248 addSimple(root, 'summary', iface.summary)
249 addSimple(root, 'description', iface.description)
250 feed_for = addSimple(root, 'feed-for')
252 uri = iface.uri
253 if uri.startswith('/') and iface.feed_for:
254 for uri in iface.feed_for:
255 print "Note: source %s is a local feed" % iface.uri
256 print "Will use <feed-for interface='%s'> instead..." % uri
257 break
259 feed_for.setAttributeNS(None, 'interface', uri)
261 group = addSimple(root, 'group')
262 main = src_impl.attrs.get(XMLNS_0COMPILE + ' binary-main', None)
263 if main:
264 group.setAttributeNS(None, 'main', main)
266 lib_mappings = src_impl.attrs.get(XMLNS_0COMPILE + ' binary-lib-mappings', None)
267 if lib_mappings:
268 root.setAttributeNS(XMLNS_NAMESPACE, 'xmlns:compile', XMLNS_0COMPILE)
269 group.setAttributeNS(XMLNS_0COMPILE, 'compile:lib-mappings', lib_mappings)
271 for d in src_impl.dependencies:
272 # 0launch < 0.32 messed up the namespace...
273 if parse_bool(d.metadata.get('include-binary', 'false')) or \
274 parse_bool(d.metadata.get(XMLNS_0COMPILE + ' include-binary', 'false')):
275 requires = addSimple(group, 'requires')
276 requires.setAttributeNS(None, 'interface', d.interface)
277 for b in d.bindings:
278 if isinstance(b, model.EnvironmentBinding):
279 env_elem = addSimple(requires, 'environment')
280 env_elem.setAttributeNS(None, 'name', b.name)
281 env_elem.setAttributeNS(None, 'insert', b.insert)
282 if b.default:
283 env_elem.setAttributeNS(None, 'default', b.default)
284 else:
285 raise Exception('Unknown binding type ' + b)
286 close(requires)
288 group.setAttributeNS(None, 'arch', target_arch)
289 impl_elem = addSimple(group, 'implementation')
290 impl_elem.setAttributeNS(None, 'version', src_impl.version)
292 version_modifier = buildenv.version_modifier
293 if version_modifier:
294 impl_elem.setAttributeNS(None, 'version-modifier', version_modifier)
296 impl_elem.setAttributeNS(None, 'id', '..')
297 impl_elem.setAttributeNS(None, 'released', time.strftime('%Y-%m-%d'))
298 close(group)
299 close(root)
301 stream = codecs.open(path, 'w', encoding = 'utf-8')
302 try:
303 doc.writexml(stream)
304 finally:
305 stream.close()
307 def set_up_mappings(mappings):
308 """Create a temporary directory with symlinks for each of the library mappings."""
309 # The find_library function takes a short-name and major version of a library and
310 # returns the full path of the library.
311 libdirs = ['/lib', '/usr/lib']
312 for d in os.environ.get('LD_LIBRARY_PATH', '').split(':'):
313 if d: libdirs.append(d)
315 def add_ldconf(config_file):
316 if not os.path.isfile(config_file):
317 return
318 for line in file(config_file):
319 d = line.strip()
320 if d.startswith('include '):
321 glob_pattern = d.split(' ', 1)[1]
322 for conf in glob.glob(glob_pattern):
323 add_ldconf(conf)
324 elif d and not d.startswith('#'):
325 libdirs.append(d)
326 add_ldconf('/etc/ld.so.conf')
328 def find_library(name, major):
329 wanted = 'lib%s.so.%s' % (name, major)
330 for d in libdirs:
331 path = os.path.join(d, wanted)
332 if os.path.exists(path):
333 return path
334 print "WARNING: library '%s' not found (searched '%s')!" % (wanted, libdirs)
335 return None
337 mappings_dir = os.path.join(os.environ['TMPDIR'], 'lib-mappings')
338 os.mkdir(mappings_dir)
340 old_path = os.environ.get('LIBRARY_PATH', '')
341 if old_path: old_path = ':' + old_path
342 os.environ['LIBRARY_PATH'] = mappings_dir + old_path
344 for name, major_version in mappings:
345 target = find_library(name, major_version)
346 if target:
347 print "Adding mapping lib%s.so -> %s" % (name, target)
348 os.symlink(target, os.path.join(mappings_dir, 'lib' + name + '.so'))
350 def dup_src(fn):
351 srcdir = os.environ['SRCDIR'] + '/'
352 for root, dirs, files in os.walk(srcdir):
353 assert root.startswith(srcdir)
354 reldir = root[len(srcdir):]
355 for f in files:
356 target = os.path.join(reldir, f)
357 #print "Copy %s -> %s" % (os.path.join(root, f), target)
358 if os.path.exists(target):
359 os.unlink(target)
360 fn(os.path.join(root, f), target)
361 for d in dirs:
362 target = os.path.join(reldir, d)
363 if not os.path.isdir(target):
364 os.mkdir(target)
366 __main__.commands.append(do_build)