Removed pointless "if" in GUI
[0compile.git] / build.py
blob5fdd61ce2d18449fea029ecbebe37fd7f5e14d4e
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
9 import tempfile
11 from zeroinstall import SafeException
12 from zeroinstall.injector import model, namespaces, run
13 from zeroinstall.injector.iface_cache import iface_cache
15 from support import BuildEnv, ensure_dir, XMLNS_0COMPILE, is_package_impl, parse_bool, depth
16 from support import spawn_and_check, find_in_path, ENV_FILE, lookup, spawn_maybe_sandboxed, Prefixes
18 if hasattr(os.path, 'relpath'):
19 relpath = os.path.relpath
20 else:
21 # Copied from Python 2.6 (GPL compatible license)
22 # Copyright (c) 2001, 2002, 2003, 2004, 2005, 2006 Python Software Foundation; All Rights Reserved
24 # Return the longest prefix of all list elements.
25 def commonprefix(m):
26 "Given a list of pathnames, returns the longest common leading component"
27 if not m: return ''
28 s1 = min(m)
29 s2 = max(m)
30 for i, c in enumerate(s1):
31 if c != s2[i]:
32 return s1[:i]
33 return s1
35 def relpath(path, start):
36 """Return a relative version of a path"""
38 if not path:
39 raise ValueError("no path specified")
41 start_list = os.path.abspath(start).split('/')
42 path_list = os.path.abspath(path).split('/')
44 # Work out how much of the filepath is shared by start and path.
45 i = len(commonprefix([start_list, path_list]))
47 rel_list = ['..'] * (len(start_list)-i) + path_list[i:]
48 if not rel_list:
49 return '.'
50 return join(*rel_list)
52 # If we have to modify any pkg-config files, we put the new versions in $TMPDIR/PKG_CONFIG_OVERRIDES
53 PKG_CONFIG_OVERRIDES = 'pkg-config-overrides'
55 def env(name, value):
56 os.environ[name] = value
57 print "%s=%s" % (name, value)
59 def do_env_binding(binding, path):
60 if binding.insert is not None and path is None:
61 # Skip insert bindings for package implementations
62 return
63 env(binding.name, binding.get_value(path, os.environ.get(binding.name, None)))
65 def correct_for_64bit(base, rel_path):
66 """If rel_path starts lib or usr/lib and doesn't exist, try with lib64 instead."""
67 if os.path.exists(os.path.join(base, rel_path)):
68 return rel_path
70 if rel_path.startswith('lib/') or rel_path.startswith('usr/lib/'):
71 new_rel_path = rel_path.replace('lib/', 'lib64/', 1)
72 if os.path.exists(os.path.join(base, new_rel_path)):
73 return new_rel_path
75 return rel_path
77 def write_pc(name, lines):
78 overrides_dir = os.path.join(os.environ['TMPDIR'], PKG_CONFIG_OVERRIDES)
79 if not os.path.isdir(overrides_dir):
80 os.mkdir(overrides_dir)
81 stream = open(os.path.join(overrides_dir, name), 'w')
82 stream.write(''.join(lines))
83 stream.close()
85 def do_pkg_config_binding(binding, impl):
86 feed_name = impl.feed.split('/')[-1]
87 path = lookup(impl)
88 new_insert = correct_for_64bit(path, binding.insert)
89 if new_insert != binding.insert:
90 print "PKG_CONFIG_PATH dir <%s>/%s not found; using %s instead" % (feed_name, binding.insert, new_insert)
91 binding = model.EnvironmentBinding(binding.name,
92 new_insert,
93 binding.default,
94 binding.mode)
96 orig_path = os.path.join(path, binding.insert)
97 if os.path.isdir(orig_path):
98 for pc in os.listdir(orig_path):
99 stream = open(os.path.join(orig_path, pc))
100 lines = stream.readlines()
101 stream.close()
102 for i, line in enumerate(lines):
103 if '=' not in line: continue
104 name, value = [x.strip() for x in line.split('=', 1)]
105 if name == 'prefix' and value.startswith('/'):
106 print "Absolute prefix=%s in %s; overriding..." % (value, feed_name)
107 lines[i] = 'prefix=%s/%s\n' % (path, value[1:])
108 write_pc(pc, lines)
109 break
110 do_env_binding(binding, path)
112 def shorten_dynamic_library_install_name(dylib_file):
113 # Only need to change actual library, not links to it
114 if os.path.islink(dylib_file):
115 return
116 otool_args = ['/usr/bin/otool', '-D', dylib_file]
117 process = subprocess.Popen(otool_args, stdout=subprocess.PIPE)
118 output, error = process.communicate()
119 retcode = process.poll()
120 for line in output.split('\n'):
121 if not line.endswith(':'):
122 value = line.strip()
123 print "Absolute install name=%s in %s; fixing..." % (value, dylib_file)
124 break
125 shortname = os.path.basename(dylib_file)
126 os.system("install_name_tool -id %s %s" % (shortname, dylib_file))
128 # After doing a build, remove the (dist) directory component from dynamic libraries
129 def shorten_dynamic_library_install_names():
130 for root, dirs, files in os.walk(os.environ['DISTDIR']):
131 if os.path.basename(root) == 'lib':
132 for f in files:
133 if f.endswith('.dylib'):
134 info("Checking dynamic library '%s'", f)
135 shorten_dynamic_library_install_name(os.path.join(root, f))
137 def fixup_generated_pkgconfig_file(pc_file):
138 stream = open(pc_file)
139 lines = stream.readlines()
140 stream.close()
141 for i, line in enumerate(lines):
142 if '=' not in line: continue
143 name, value = [x.strip() for x in line.split('=', 1)]
144 if name == 'prefix' and value.startswith('/'):
145 print "Absolute prefix=%s in %s; fixing..." % (value, pc_file)
146 rel_path = relpath(value, os.path.dirname(pc_file)) # Requires Python 2.6
147 lines[i] = 'prefix=${pcfiledir}/%s\n' % rel_path
148 write_pc(pc_file, lines)
149 break
151 # After doing a build, check that we didn't generate pkgconfig files with absolute paths
152 # Rewrite if so
153 def fixup_generated_pkgconfig_files():
154 for root, dirs, files in os.walk(os.environ['DISTDIR']):
155 if os.path.basename(root) == 'pkgconfig':
156 for f in files:
157 if f.endswith('.pc'):
158 info("Checking generated pkgconfig file '%s'", f)
159 fixup_generated_pkgconfig_file(os.path.join(root, f))
161 def remove_la_file(path):
162 # Read the contents...
163 stream = open(path)
164 data = stream.read()
165 stream.close()
167 # Check it really is a libtool archive...
168 if 'Please DO NOT delete this file' not in data:
169 warn("Ignoring %s; doesn't look like a libtool archive", path)
170 return
172 os.unlink(path)
173 print "Removed %s (.la files contain absolute paths)" % path
175 # libtool archives contain hard-coded paths. Lucky, modern systems don't need them, so remove
176 # them.
177 def remove_la_files():
178 for root, dirs, files in os.walk(os.environ['DISTDIR']):
179 if os.path.basename(root) == 'lib':
180 for f in files:
181 if f.endswith('.la'):
182 remove_la_file(os.path.join(root, f))
183 if f.endswith('.a'):
184 warn("Found static archive '%s'; maybe build with --disable-static?", f)
186 class CompileSetup(run.Setup):
187 def do_binding(self, impl, b, iface):
188 if isinstance(b, model.EnvironmentBinding):
189 if b.name == 'PKG_CONFIG_PATH':
190 do_pkg_config_binding(b, impl)
191 else:
192 do_env_binding(b, lookup(impl))
193 else:
194 run.Setup.do_binding(self, impl, b, iface)
196 def do_build_internal(options, args):
197 """build-internal"""
198 # If a sandbox is being used, we're in it now.
199 import getpass, socket
201 buildenv = BuildEnv()
202 sels = buildenv.get_selections()
204 builddir = os.path.realpath('build')
205 ensure_dir(buildenv.metadir)
207 build_env_xml = join(buildenv.metadir, 'build-environment.xml')
209 buildenv_doc = sels.toDOM()
211 # Create build-environment.xml file
212 root = buildenv_doc.documentElement
213 info = buildenv_doc.createElementNS(XMLNS_0COMPILE, 'build-info')
214 root.appendChild(info)
215 info.setAttributeNS(None, 'time', time.strftime('%Y-%m-%d %H:%M').strip())
216 info.setAttributeNS(None, 'host', socket.getfqdn())
217 info.setAttributeNS(None, 'user', getpass.getuser())
218 uname = os.uname()
219 info.setAttributeNS(None, 'arch', '%s-%s' % (uname[0], uname[4]))
220 stream = file(build_env_xml, 'w')
221 buildenv_doc.writexml(stream, addindent=" ", newl="\n")
222 stream.close()
224 # Create local binary interface file
225 src_iface = iface_cache.get_interface(buildenv.interface)
226 src_impl = buildenv.chosen_impl(buildenv.interface)
227 write_sample_interface(buildenv, src_iface, src_impl)
229 # Check 0compile is new enough
230 min_version = model.parse_version(src_impl.attrs.get(XMLNS_0COMPILE + ' min-version', None))
231 if min_version and min_version > model.parse_version(__main__.version):
232 raise SafeException("%s-%s requires 0compile >= %s, but we are only version %s" %
233 (src_iface.get_name(), src_impl.version, model.format_version(min_version), __main__.version))
235 # Create the patch
236 patch_file = join(buildenv.metadir, 'from-%s.patch' % src_impl.version)
237 if buildenv.user_srcdir:
238 # (ignore errors; will already be shown on stderr)
239 os.system("diff -urN '%s' src > %s" %
240 (buildenv.orig_srcdir.replace('\\', '\\\\').replace("'", "\\'"),
241 patch_file))
242 if os.path.getsize(patch_file) == 0:
243 os.unlink(patch_file)
244 elif os.path.exists(patch_file):
245 os.unlink(patch_file)
247 env('BUILDDIR', builddir)
248 env('DISTDIR', buildenv.distdir)
249 env('SRCDIR', buildenv.user_srcdir or buildenv.orig_srcdir)
250 env('BINARYFEED', buildenv.local_iface_file)
251 os.chdir(builddir)
252 print "cd", builddir
254 setup = CompileSetup(iface_cache.stores, sels)
255 setup.prepare_env()
257 # These mappings are needed when mixing Zero Install -dev packages with
258 # native package binaries.
259 mappings = {}
260 for impl in sels.selections.values():
261 # Add mappings that have been set explicitly...
262 new_mappings = impl.attrs.get(XMLNS_0COMPILE + ' lib-mappings', '')
263 if new_mappings:
264 new_mappings = new_mappings.split(' ')
265 for mapping in new_mappings:
266 assert ':' in mapping, "lib-mappings missing ':' in '%s' from '%s'" % (mapping, impl.feed)
267 name, major_version = mapping.split(':', 1)
268 assert '/' not in mapping, "lib-mappings '%s' contains a / in the version number (from '%s')!" % (mapping, impl.feed)
269 if sys.platform == 'darwin':
270 mappings[name] = 'lib%s.%s.dylib' % (name, major_version)
271 else:
272 mappings[name] = 'lib%s.so.%s' % (name, major_version)
273 # Auto-detect required mappings where possible...
274 # (if the -dev package is native, the symlinks will be OK)
275 if not is_package_impl(impl):
276 impl_path = lookup(impl)
277 for libdirname in ['lib', 'usr/lib', 'lib64', 'usr/lib64']:
278 libdir = os.path.join(impl_path, libdirname)
279 if os.path.isdir(libdir):
280 find_broken_version_symlinks(libdir, mappings)
282 if mappings:
283 set_up_mappings(mappings)
285 overrides_dir = os.path.join(os.environ['TMPDIR'], PKG_CONFIG_OVERRIDES)
286 if os.path.isdir(overrides_dir):
287 add_overrides = model.EnvironmentBinding('PKG_CONFIG_PATH', PKG_CONFIG_OVERRIDES)
288 do_env_binding(add_overrides, os.environ['TMPDIR'])
290 # Some programs want to put temporary build files in the source directory.
291 # Make a copy of the source if needed.
292 dup_src_type = src_impl.attrs.get(XMLNS_0COMPILE + ' dup-src', None)
293 if dup_src_type == 'true':
294 dup_src(shutil.copy2)
295 env('SRCDIR', builddir)
296 elif dup_src_type:
297 raise Exception("Unknown dup-src value '%s'" % dup_src_type)
299 if options.shell:
300 spawn_and_check(find_in_path('sh'), [])
301 else:
302 command = sels.commands[0].qdom.attrs.get('shell-command', None)
303 if command is None:
304 # New style <command>
305 prog_args = setup.build_command(sels.interface, sels.command) + args
306 else:
307 # Old style shell-command='...'
308 prog_args = ['/bin/sh', '-c', command + ' "$@"', '-'] + args
309 assert len(sels.commands) == 1
311 # Remove any existing log files
312 for log in ['build.log', 'build-success.log', 'build-failure.log']:
313 if os.path.exists(log):
314 os.unlink(log)
316 # Run the command, copying output to a new log
317 log = file('build.log', 'w')
318 try:
319 print >>log, "Build log for %s-%s" % (src_iface.get_name(),
320 src_impl.version)
321 print >>log, "\nBuilt using 0compile-%s" % __main__.version
322 print >>log, "\nBuild system: " + ', '.join(uname)
323 print >>log, "\n%s:\n" % ENV_FILE
324 shutil.copyfileobj(file("../" + ENV_FILE), log)
326 log.write('\n')
328 if os.path.exists(patch_file):
329 print >>log, "\nPatched with:\n"
330 shutil.copyfileobj(file(patch_file), log)
331 log.write('\n')
333 if command:
334 print "Executing: " + command, args
335 print >>log, "Executing: " + command, args
336 else:
337 print "Executing: " + str(prog_args)
338 print >>log, "Executing: " + str(prog_args)
340 # Tee the output to the console and to the log
341 child = subprocess.Popen(prog_args, stdout = subprocess.PIPE, stderr = subprocess.STDOUT)
342 while True:
343 data = os.read(child.stdout.fileno(), 100)
344 if not data: break
345 sys.stdout.write(data)
346 log.write(data)
347 status = child.wait()
348 failure = None
349 if status == 0:
350 print >>log, "Build successful"
351 shorten_dynamic_library_install_names()
352 fixup_generated_pkgconfig_files()
353 remove_la_files()
354 elif status > 0:
355 failure = "Build failed with exit code %d" % status
356 else:
357 failure = "Build failure: exited due to signal %d" % (-status)
358 if failure:
359 print >>log, failure
360 os.rename('build.log', 'build-failure.log')
361 raise SafeException("Command '%s': %s" % (prog_args, failure))
362 else:
363 os.rename('build.log', 'build-success.log')
364 finally:
365 log.close()
367 def do_build(args):
368 """build [ --no-sandbox ] [ --shell | --force | --clean ]"""
369 buildenv = BuildEnv()
370 sels = buildenv.get_selections()
372 parser = OptionParser(usage="usage: %prog build [options]")
374 parser.add_option('', "--no-sandbox", help="disable use of sandboxing", action='store_true')
375 parser.add_option("-s", "--shell", help="run a shell instead of building", action='store_true')
376 parser.add_option("-c", "--clean", help="remove the build directories", action='store_true')
377 parser.add_option("-f", "--force", help="build even if dependencies have changed", action='store_true')
379 parser.disable_interspersed_args()
381 (options, args2) = parser.parse_args(args)
383 builddir = os.path.realpath('build')
385 changes = buildenv.get_build_changes()
386 if changes:
387 if not (options.force or options.clean):
388 raise SafeException("Build dependencies have changed:\n" +
389 '\n'.join(changes) + "\n\n" +
390 "To build anyway, use: 0compile build --force\n" +
391 "To do a clean build: 0compile build --clean")
392 if not options.no_sandbox:
393 print "Build dependencies have changed:\n" + '\n'.join(changes)
395 ensure_dir(builddir, options.clean)
396 ensure_dir(buildenv.distdir, options.clean)
398 if options.no_sandbox:
399 return do_build_internal(options, args2)
401 tmpdir = tempfile.mkdtemp(prefix = '0compile-')
402 try:
403 my_dir = os.path.dirname(__file__)
404 readable = ['.', my_dir]
405 writable = ['build', buildenv.distdir, tmpdir]
406 env('TMPDIR', tmpdir)
408 for selection in sels.selections.values():
409 if not is_package_impl(selection):
410 readable.append(lookup(selection))
412 options = []
413 if __main__.options.verbose:
414 options.append('--verbose')
416 readable.append('/etc') # /etc/ld.*
418 spawn_maybe_sandboxed(readable, writable, tmpdir, sys.executable, ['-u', sys.argv[0]] + options + ['build', '--no-sandbox'] + args)
419 finally:
420 info("Deleting temporary directory '%s'" % tmpdir)
421 shutil.rmtree(tmpdir)
423 def write_sample_interface(buildenv, iface, src_impl):
424 path = buildenv.local_iface_file
426 impl = minidom.getDOMImplementation()
428 XMLNS_IFACE = namespaces.XMLNS_IFACE
430 doc = impl.createDocument(XMLNS_IFACE, "interface", None)
432 root = doc.documentElement
433 root.setAttributeNS(XMLNS_NAMESPACE, 'xmlns', XMLNS_IFACE)
434 prefixes = Prefixes(XMLNS_IFACE)
436 def addSimple(parent, name, text = None):
437 elem = doc.createElementNS(XMLNS_IFACE, name)
439 parent.appendChild(doc.createTextNode('\n' + ' ' * (1 + depth(parent))))
440 parent.appendChild(elem)
441 if text:
442 elem.appendChild(doc.createTextNode(text))
443 return elem
445 def close(element):
446 element.appendChild(doc.createTextNode('\n' + ' ' * depth(element)))
448 addSimple(root, 'name', iface.name)
449 addSimple(root, 'summary', iface.summary)
450 addSimple(root, 'description', iface.description)
451 feed_for = addSimple(root, 'feed-for')
453 uri = iface.uri
454 if uri.startswith('/'):
455 print "Note: source %s is a local feed" % iface.uri
456 for feed_uri in iface.feed_for or []:
457 uri = feed_uri
458 print "Will use <feed-for interface='%s'> instead..." % uri
459 break
460 else:
461 master_feed = minidom.parse(uri).documentElement
462 if master_feed.hasAttribute('uri'):
463 uri = master_feed.getAttribute('uri')
464 print "Will use <feed-for interface='%s'> instead..." % uri
466 feed_for.setAttributeNS(None, 'interface', uri)
468 group = addSimple(root, 'group')
469 main = src_impl.attrs.get(XMLNS_0COMPILE + ' binary-main', None)
470 if main:
471 group.setAttributeNS(None, 'main', main)
473 lib_mappings = src_impl.attrs.get(XMLNS_0COMPILE + ' binary-lib-mappings', None)
474 if lib_mappings:
475 prefixes.setAttributeNS(group, XMLNS_0COMPILE, 'lib-mappings', lib_mappings)
477 for d in src_impl.dependencies:
478 # 0launch < 0.32 messed up the namespace...
479 if parse_bool(d.metadata.get('include-binary', 'false')) or \
480 parse_bool(d.metadata.get(XMLNS_0COMPILE + ' include-binary', 'false')):
481 requires = addSimple(group, 'requires')
482 requires.setAttributeNS(None, 'interface', d.interface)
483 for b in d.bindings:
484 if isinstance(b, model.EnvironmentBinding):
485 env_elem = addSimple(requires, 'environment')
486 env_elem.setAttributeNS(None, 'name', b.name)
487 env_elem.setAttributeNS(None, 'insert', b.insert)
488 if b.default:
489 env_elem.setAttributeNS(None, 'default', b.default)
490 else:
491 raise Exception('Unknown binding type ' + b)
492 close(requires)
494 set_arch = True
496 impl_elem = addSimple(group, 'implementation')
497 impl_template = buildenv.get_binary_template()
498 if impl_template:
499 # Copy attributes from template
500 for fullname, value in impl_template.attrs.iteritems():
501 if fullname == 'arch':
502 set_arch = False
503 if value == '*-*':
504 continue
505 if ' ' in fullname:
506 ns, localName = fullname.split(' ', 1)
507 else:
508 ns, localName = None, fullname
509 prefixes.setAttributeNS(impl_elem, ns, localName, value)
510 # Copy child nodes
511 for child in impl_template.childNodes:
512 impl_elem.appendChild(child.toDOM(doc, prefixes))
513 if impl_template.content:
514 impl_elem.appendChild(doc.createTextNode(impl_template.content))
516 if set_arch:
517 group.setAttributeNS(None, 'arch', buildenv.target_arch)
519 impl_elem.setAttributeNS(None, 'version', src_impl.version)
521 version_modifier = buildenv.version_modifier
522 if version_modifier:
523 impl_elem.setAttributeNS(None, 'version-modifier', version_modifier)
525 impl_elem.setAttributeNS(None, 'id', '..')
526 impl_elem.setAttributeNS(None, 'released', time.strftime('%Y-%m-%d'))
527 close(group)
528 close(root)
530 for ns, prefix in prefixes.prefixes.items():
531 root.setAttributeNS(XMLNS_NAMESPACE, 'xmlns:' + prefix, ns)
533 stream = codecs.open(path, 'w', encoding = 'utf-8')
534 try:
535 doc.writexml(stream)
536 finally:
537 stream.close()
539 def find_broken_version_symlinks(libdir, mappings):
540 """libdir may be a legacy -devel package containing lib* symlinks whose
541 targets would be provided by the corresponding runtime package. If so,
542 create fixed symlinks under $TMPDIR with the real location."""
543 prefix = 'lib'
544 if sys.platform == 'darwin':
545 extension = '.dylib'
546 else:
547 extension = '.so'
549 for x in os.listdir(libdir):
550 if x.startswith(prefix) and x.endswith(extension):
551 path = os.path.join(libdir, x)
552 if os.path.islink(path):
553 target = os.readlink(path)
554 if '/' not in target and not os.path.exists(os.path.join(libdir, target)):
555 print "Broken link %s -> %s; will relocate..." % (x, target)
556 mappings[x[len(prefix):-len(extension)]] = target
558 def set_up_mappings(mappings):
559 """Create a temporary directory with symlinks for each of the library mappings."""
560 libdirs = []
561 if sys.platform == 'darwin':
562 LD_LIBRARY_PATH='DYLD_LIBRARY_PATH'
563 else:
564 LD_LIBRARY_PATH='LD_LIBRARY_PATH'
565 for d in os.environ.get(LD_LIBRARY_PATH, '').split(':'):
566 if d: libdirs.append(d)
567 libdirs += ['/lib', '/usr/lib']
569 def add_ldconf(config_file):
570 if not os.path.isfile(config_file):
571 return
572 for line in file(config_file):
573 d = line.strip()
574 if d.startswith('include '):
575 glob_pattern = d.split(' ', 1)[1]
576 for conf in glob.glob(glob_pattern):
577 add_ldconf(conf)
578 elif d and not d.startswith('#'):
579 libdirs.append(d)
580 add_ldconf('/etc/ld.so.conf')
582 def find_library(name, wanted):
583 # Takes a short-name and target name of a library and returns
584 # the full path of the library.
585 for d in libdirs:
586 path = os.path.join(d, wanted)
587 if os.path.exists(path):
588 return path
589 print "WARNING: library '%s' not found (searched '%s')!" % (wanted, libdirs)
590 return None
592 mappings_dir = os.path.join(os.environ['TMPDIR'], 'lib-mappings')
593 os.mkdir(mappings_dir)
595 old_path = os.environ.get('LIBRARY_PATH', '')
596 if old_path: old_path = ':' + old_path
597 os.environ['LIBRARY_PATH'] = mappings_dir + old_path
599 if sys.platform == 'darwin':
600 soext='.dylib'
601 else:
602 soext='.so'
603 for name, wanted in mappings.items():
604 target = find_library(name, wanted)
605 if target:
606 print "Adding mapping lib%s%s -> %s" % (name, soext, target)
607 os.symlink(target, os.path.join(mappings_dir, 'lib' + name + soext))
609 def dup_src(fn):
610 srcdir = os.environ['SRCDIR'] + '/'
611 builddir = os.environ['BUILDDIR']
613 build_in_src = srcdir + 'build' == builddir
615 for root, dirs, files in os.walk(srcdir):
616 assert root.startswith(srcdir)
617 reldir = root[len(srcdir):]
619 if reldir == '.git' or (reldir == 'build' and build_in_src):
620 print "dup-src: skipping", reldir
621 dirs[:] = []
622 continue
624 for f in files:
625 target = os.path.join(reldir, f)
626 #print "Copy %s -> %s" % (os.path.join(root, f), target)
627 if os.path.exists(target):
628 os.unlink(target)
629 fn(os.path.join(root, f), target)
630 for d in dirs:
631 target = os.path.join(reldir, d)
632 if not os.path.isdir(target):
633 os.mkdir(target)
635 __main__.commands.append(do_build)