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
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
, uname
16 from support
import spawn_and_check
, find_in_path
, ENV_FILE
, lookup
, spawn_and_check_maybe_sandboxed
, Prefixes
18 # If we have to modify any pkg-config files, we put the new versions in $TMPDIR/PKG_CONFIG_OVERRIDES
19 PKG_CONFIG_OVERRIDES
= 'pkg-config-overrides'
22 os
.environ
[name
] = value
23 print "%s=%s" % (name
, value
)
25 def do_env_binding(binding
, path
):
26 if binding
.insert
is not None and path
is None:
27 # Skip insert bindings for package implementations
29 env(binding
.name
, binding
.get_value(path
, os
.environ
.get(binding
.name
, None)))
31 def correct_for_64bit(base
, rel_path
):
32 """If rel_path starts lib or usr/lib and doesn't exist, try with lib64 instead."""
33 if os
.path
.exists(os
.path
.join(base
, rel_path
)):
36 if rel_path
.startswith('lib/') or rel_path
.startswith('usr/lib/'):
37 new_rel_path
= rel_path
.replace('lib/', 'lib64/', 1)
38 if os
.path
.exists(os
.path
.join(base
, new_rel_path
)):
43 def write_pc(name
, lines
):
44 overrides_dir
= os
.path
.join(os
.environ
['TMPDIR'], PKG_CONFIG_OVERRIDES
)
45 if not os
.path
.isdir(overrides_dir
):
46 os
.mkdir(overrides_dir
)
47 stream
= open(os
.path
.join(overrides_dir
, name
), 'w')
48 stream
.write(''.join(lines
))
51 def do_pkg_config_binding(binding
, impl
):
52 if impl
.id.startswith('package:'):
53 return # No bindings needed for native packages
54 feed_name
= impl
.feed
.split('/')[-1]
56 new_insert
= correct_for_64bit(path
, binding
.insert
)
57 if new_insert
!= binding
.insert
:
58 print "PKG_CONFIG_PATH dir <%s>/%s not found; using %s instead" % (feed_name
, binding
.insert
, new_insert
)
59 binding
= model
.EnvironmentBinding(binding
.name
,
64 orig_path
= os
.path
.join(path
, binding
.insert
)
65 if os
.path
.isdir(orig_path
):
66 for pc
in os
.listdir(orig_path
):
67 stream
= open(os
.path
.join(orig_path
, pc
))
68 lines
= stream
.readlines()
70 for i
, line
in enumerate(lines
):
71 if '=' not in line
: continue
72 name
, value
= [x
.strip() for x
in line
.split('=', 1)]
73 if name
== 'prefix' and os
.path
.isabs(value
):
74 print "Absolute prefix=%s in %s; overriding..." % (value
, feed_name
)
75 lines
[i
] = 'prefix=' + os
.path
.join(
76 path
, os
.path
.splitdrive(value
)[1][1:]) +'\n'
79 do_env_binding(binding
, path
)
81 def shorten_dynamic_library_install_name(dylib_file
):
82 # Only need to change actual library, not links to it
83 if os
.path
.islink(dylib_file
):
85 otool_args
= ['/usr/bin/otool', '-D', dylib_file
]
86 process
= subprocess
.Popen(otool_args
, stdout
=subprocess
.PIPE
)
87 output
, error
= process
.communicate()
88 retcode
= process
.poll()
89 for line
in output
.split('\n'):
90 if not line
.endswith(':'):
92 print "Absolute install name=%s in %s; fixing..." % (value
, dylib_file
)
94 shortname
= os
.path
.basename(dylib_file
)
95 subprocess
.check_call(['install_name_tool', '-id', shortname
, dylib_file
])
97 # After doing a build, remove the (dist) directory component from dynamic libraries
98 def shorten_dynamic_library_install_names():
99 for root
, dirs
, files
in os
.walk(os
.environ
['DISTDIR']):
100 if os
.path
.basename(root
) == 'lib':
102 if f
.endswith('.dylib'):
103 info("Checking dynamic library '%s'", f
)
104 shorten_dynamic_library_install_name(os
.path
.join(root
, f
))
106 def fixup_generated_pkgconfig_file(pc_file
):
107 stream
= open(pc_file
)
108 lines
= stream
.readlines()
110 for i
, line
in enumerate(lines
):
111 if '=' not in line
: continue
112 name
, value
= [x
.strip() for x
in line
.split('=', 1)]
113 if name
== 'prefix' and os
.path
.isabs(value
):
114 print "Absolute prefix=%s in %s; fixing..." % (value
, pc_file
)
115 rel_path
= os
.path
.relpath(value
, os
.path
.dirname(pc_file
))
116 lines
[i
] = 'prefix=' + os
.path
.join(
117 '${pcfiledir}', rel_path
) + '\n'
118 write_pc(pc_file
, lines
)
121 # After doing a build, check that we didn't generate pkgconfig files with absolute paths
123 def fixup_generated_pkgconfig_files():
124 for root
, dirs
, files
in os
.walk(os
.environ
['DISTDIR']):
125 if os
.path
.basename(root
) == 'pkgconfig':
127 if f
.endswith('.pc'):
128 info("Checking generated pkgconfig file '%s'", f
)
129 fixup_generated_pkgconfig_file(os
.path
.join(root
, f
))
131 def remove_la_file(path
):
132 # Read the contents...
137 # Check it really is a libtool archive...
138 if 'Please DO NOT delete this file' not in data
:
139 warn("Ignoring %s; doesn't look like a libtool archive", path
)
143 print "Removed %s (.la files contain absolute paths)" % path
145 # libtool archives contain hard-coded paths. Lucky, modern systems don't need them, so remove
147 def remove_la_files():
148 for root
, dirs
, files
in os
.walk(os
.environ
['DISTDIR']):
149 if os
.path
.basename(root
) == 'lib':
151 if f
.endswith('.la'):
152 remove_la_file(os
.path
.join(root
, f
))
154 warn("Found static archive '%s'; maybe build with --disable-static?", f
)
156 class CompileSetup(run
.Setup
):
157 def do_binding(self
, impl
, b
, iface
):
158 if isinstance(b
, model
.EnvironmentBinding
):
159 if b
.name
== 'PKG_CONFIG_PATH':
160 do_pkg_config_binding(b
, impl
)
162 do_env_binding(b
, lookup(impl
))
164 run
.Setup
.do_binding(self
, impl
, b
, iface
)
166 def do_build_internal(options
, args
):
168 # If a sandbox is being used, we're in it now.
169 import getpass
, socket
171 buildenv
= BuildEnv()
172 sels
= buildenv
.get_selections()
174 builddir
= os
.path
.realpath('build')
175 ensure_dir(buildenv
.metadir
)
177 build_env_xml
= join(buildenv
.metadir
, 'build-environment.xml')
179 buildenv_doc
= sels
.toDOM()
181 # Create build-environment.xml file
182 root
= buildenv_doc
.documentElement
183 info
= buildenv_doc
.createElementNS(XMLNS_0COMPILE
, 'build-info')
184 root
.appendChild(info
)
185 info
.setAttributeNS(None, 'time', time
.strftime('%Y-%m-%d %H:%M').strip())
186 info
.setAttributeNS(None, 'host', socket
.getfqdn())
187 info
.setAttributeNS(None, 'user', getpass
.getuser())
188 info
.setAttributeNS(None, 'arch', '%s-%s' % (uname
[0], uname
[4]))
189 stream
= file(build_env_xml
, 'w')
190 buildenv_doc
.writexml(stream
, addindent
=" ", newl
="\n")
193 # Create local binary interface file.
194 # We use the main feed for the interface as the template for the name,
195 # summary, etc (note: this is not necessarily the feed that contained
197 master_feed
= iface_cache
.get_feed(buildenv
.interface
)
198 src_impl
= buildenv
.chosen_impl(buildenv
.interface
)
199 write_sample_feed(buildenv
, master_feed
, src_impl
)
201 # Check 0compile is new enough
202 min_version
= model
.parse_version(src_impl
.attrs
.get(XMLNS_0COMPILE
+ ' min-version', None))
203 if min_version
and min_version
> model
.parse_version(__main__
.version
):
204 raise SafeException("%s-%s requires 0compile >= %s, but we are only version %s" %
205 (master_feed
.get_name(), src_impl
.version
, model
.format_version(min_version
), __main__
.version
))
208 patch_file
= join(buildenv
.metadir
, 'from-%s.patch' % src_impl
.version
)
209 if buildenv
.user_srcdir
:
210 with
open(patch_file
, 'w') as stream
:
211 # (ignore errors; will already be shown on stderr)
213 subprocess
.call(["diff", "-urN", buildenv
.orig_srcdir
, 'src'], stdout
= stream
)
214 except OSError as ex
:
215 print >>sys
.stderr
, "WARNING: Failed to run 'diff': ", ex
216 if os
.path
.getsize(patch_file
) == 0:
217 os
.unlink(patch_file
)
218 elif os
.path
.exists(patch_file
):
219 os
.unlink(patch_file
)
221 env('BUILDDIR', builddir
)
222 env('DISTDIR', buildenv
.distdir
)
223 env('SRCDIR', buildenv
.user_srcdir
or buildenv
.orig_srcdir
)
224 env('BINARYFEED', buildenv
.local_iface_file
)
228 setup
= CompileSetup(iface_cache
.stores
, sels
)
231 # These mappings are needed when mixing Zero Install -dev packages with
232 # native package binaries.
234 for impl
in sels
.selections
.values():
235 # Add mappings that have been set explicitly...
236 new_mappings
= impl
.attrs
.get(XMLNS_0COMPILE
+ ' lib-mappings', '')
238 new_mappings
= new_mappings
.split(' ')
239 for mapping
in new_mappings
:
240 assert ':' in mapping
, "lib-mappings missing ':' in '%s' from '%s'" % (mapping
, impl
.feed
)
241 name
, major_version
= mapping
.split(':', 1)
242 assert '/' not in mapping
, "lib-mappings '%s' contains a / in the version number (from '%s')!" % (mapping
, impl
.feed
)
243 if sys
.platform
== 'darwin':
244 mappings
[name
] = 'lib%s.%s.dylib' % (name
, major_version
)
246 mappings
[name
] = 'lib%s.so.%s' % (name
, major_version
)
247 # Auto-detect required mappings where possible...
248 # (if the -dev package is native, the symlinks will be OK)
249 if not is_package_impl(impl
):
250 impl_path
= lookup(impl
)
251 for libdirname
in ['lib', 'usr/lib', 'lib64', 'usr/lib64']:
252 libdir
= os
.path
.join(impl_path
, libdirname
)
253 if os
.path
.isdir(libdir
):
254 find_broken_version_symlinks(libdir
, mappings
)
257 set_up_mappings(mappings
)
259 overrides_dir
= os
.path
.join(os
.environ
['TMPDIR'], PKG_CONFIG_OVERRIDES
)
260 if os
.path
.isdir(overrides_dir
):
261 add_overrides
= model
.EnvironmentBinding('PKG_CONFIG_PATH', PKG_CONFIG_OVERRIDES
)
262 do_env_binding(add_overrides
, os
.environ
['TMPDIR'])
264 # Some programs want to put temporary build files in the source directory.
265 # Make a copy of the source if needed.
266 dup_src_type
= src_impl
.attrs
.get(XMLNS_0COMPILE
+ ' dup-src', None)
267 if dup_src_type
== 'true':
268 dup_src(shutil
.copy2
)
269 env('SRCDIR', builddir
)
271 raise Exception("Unknown dup-src value '%s'" % dup_src_type
)
274 spawn_and_check(find_in_path('sh'), [])
276 command
= sels
.commands
[0].qdom
.attrs
.get('shell-command', None)
278 # New style <command>
279 prog_args
= setup
.build_command(sels
.interface
, sels
.command
) + args
281 # Old style shell-command='...'
282 prog_args
= ['/bin/sh', '-c', command
+ ' "$@"', '-'] + args
283 assert len(sels
.commands
) == 1
285 # Remove any existing log files
286 for log
in ['build.log', 'build-success.log', 'build-failure.log']:
287 if os
.path
.exists(log
):
290 # Run the command, copying output to a new log
291 with
open('build.log', 'w') as log
:
292 print >>log
, "Build log for %s-%s" % (master_feed
.get_name(),
294 print >>log
, "\nBuilt using 0compile-%s" % __main__
.version
295 print >>log
, "\nBuild system: " + ', '.join(uname
)
296 print >>log
, "\n%s:\n" % ENV_FILE
297 with
open(os
.path
.join(os
.pardir
, ENV_FILE
)) as properties_file
:
298 shutil
.copyfileobj(properties_file
, log
)
302 if os
.path
.exists(patch_file
):
303 print >>log
, "\nPatched with:\n"
304 shutil
.copyfileobj(file(patch_file
), log
)
308 print "Executing: " + command
, args
309 print >>log
, "Executing: " + command
, args
311 print "Executing: " + str(prog_args
)
312 print >>log
, "Executing: " + str(prog_args
)
314 # Tee the output to the console and to the log
315 child
= subprocess
.Popen(prog_args
, stdout
= subprocess
.PIPE
, stderr
= subprocess
.STDOUT
)
317 data
= os
.read(child
.stdout
.fileno(), 100)
319 sys
.stdout
.write(data
)
321 status
= child
.wait()
324 print >>log
, "Build successful"
325 shorten_dynamic_library_install_names()
326 fixup_generated_pkgconfig_files()
329 failure
= "Build failed with exit code %d" % status
331 failure
= "Build failure: exited due to signal %d" % (-status
)
336 os
.rename('build.log', 'build-failure.log')
337 raise SafeException("Command '%s': %s" % (prog_args
, failure
))
339 os
.rename('build.log', 'build-success.log')
342 """build [ --no-sandbox ] [ --shell | --force | --clean ]"""
343 buildenv
= BuildEnv()
344 sels
= buildenv
.get_selections()
346 parser
= OptionParser(usage
="usage: %prog build [options]")
348 parser
.add_option('', "--no-sandbox", help="disable use of sandboxing", action
='store_true')
349 parser
.add_option("-s", "--shell", help="run a shell instead of building", action
='store_true')
350 parser
.add_option("-c", "--clean", help="remove the build directories", action
='store_true')
351 parser
.add_option("-f", "--force", help="build even if dependencies have changed", action
='store_true')
353 parser
.disable_interspersed_args()
355 (options
, args2
) = parser
.parse_args(args
)
357 builddir
= os
.path
.realpath('build')
359 changes
= buildenv
.get_build_changes()
361 if not (options
.force
or options
.clean
):
362 raise SafeException("Build dependencies have changed:\n" +
363 '\n'.join(changes
) + "\n\n" +
364 "To build anyway, use: 0compile build --force\n" +
365 "To do a clean build: 0compile build --clean")
366 if not options
.no_sandbox
:
367 print "Build dependencies have changed:\n" + '\n'.join(changes
)
369 ensure_dir(builddir
, options
.clean
)
370 ensure_dir(buildenv
.distdir
, options
.clean
)
372 if options
.no_sandbox
:
373 return do_build_internal(options
, args2
)
375 tmpdir
= tempfile
.mkdtemp(prefix
= '0compile-')
377 my_dir
= os
.path
.dirname(__file__
)
378 readable
= ['.', my_dir
]
379 writable
= ['build', buildenv
.distdir
, tmpdir
]
380 env('TMPDIR', tmpdir
)
382 for selection
in sels
.selections
.values():
383 if not is_package_impl(selection
):
384 readable
.append(lookup(selection
))
387 if __main__
.options
.verbose
:
388 options
.append('--verbose')
390 readable
.append('/etc') # /etc/ld.*
392 spawn_and_check_maybe_sandboxed(readable
, writable
, tmpdir
, sys
.executable
, ['-u', sys
.argv
[0]] + options
+ ['build', '--no-sandbox'] + args
)
394 info("Deleting temporary directory '%s'" % tmpdir
)
395 shutil
.rmtree(tmpdir
)
397 def find_feed_for(master_feed
):
398 """Determine the <feed-for> interface for the new binary's feed.
399 remote feed (http://...) => the binary is a feed for the interface with this URI
400 local feed (/feed.xml) => copy <feed-for> from feed.xml (e.g. for a Git clone)
401 local copy of remote feed (no feed-for) => feed's uri attribute
403 if hasattr(master_feed
, 'local_path'):
404 is_local
= master_feed
.local_path
is not None # 0install >= 1.7
406 is_local
= os
.path
.isabs(master_feed
.url
)
408 uri
= master_feed
.url
411 print "Note: source %s is a local feed" % uri
412 for feed_uri
in master_feed
.feed_for
or []:
414 print "Will use <feed-for interface='%s'> instead..." % uri
417 master_feed
= minidom
.parse(uri
).documentElement
418 if master_feed
.hasAttribute('uri'):
419 uri
= master_feed
.getAttribute('uri')
420 print "Will use <feed-for interface='%s'> instead..." % uri
424 def write_sample_feed(buildenv
, master_feed
, src_impl
):
425 path
= buildenv
.local_iface_file
427 old_path
= os
.path
.join(buildenv
.metadir
, buildenv
.iface_name
+ '.xml')
428 if os
.path
.exists(old_path
):
429 warn("Removing old %s file: use %s instead now", old_path
, path
)
432 impl
= minidom
.getDOMImplementation()
434 XMLNS_IFACE
= namespaces
.XMLNS_IFACE
436 doc
= impl
.createDocument(XMLNS_IFACE
, "interface", None)
438 root
= doc
.documentElement
439 root
.setAttributeNS(XMLNS_NAMESPACE
, 'xmlns', XMLNS_IFACE
)
440 prefixes
= Prefixes(XMLNS_IFACE
)
442 def addSimple(parent
, name
, text
= None):
443 elem
= doc
.createElementNS(XMLNS_IFACE
, name
)
445 parent
.appendChild(doc
.createTextNode('\n' + ' ' * (1 + depth(parent
))))
446 parent
.appendChild(elem
)
448 elem
.appendChild(doc
.createTextNode(text
))
452 element
.appendChild(doc
.createTextNode('\n' + ' ' * depth(element
)))
454 addSimple(root
, 'name', master_feed
.name
)
455 addSimple(root
, 'summary', master_feed
.summary
)
456 addSimple(root
, 'description', master_feed
.description
)
457 feed_for
= addSimple(root
, 'feed-for')
459 feed_for
.setAttributeNS(None, 'interface', find_feed_for(master_feed
))
461 group
= addSimple(root
, 'group')
462 main
= src_impl
.attrs
.get(XMLNS_0COMPILE
+ ' binary-main', None)
464 group
.setAttributeNS(None, 'main', main
)
466 lib_mappings
= src_impl
.attrs
.get(XMLNS_0COMPILE
+ ' binary-lib-mappings', None)
468 prefixes
.setAttributeNS(group
, XMLNS_0COMPILE
, 'lib-mappings', lib_mappings
)
470 for d
in src_impl
.dependencies
:
471 # 0launch < 0.32 messed up the namespace...
472 if parse_bool(d
.metadata
.get('include-binary', 'false')) or \
473 parse_bool(d
.metadata
.get(XMLNS_0COMPILE
+ ' include-binary', 'false')):
474 requires
= addSimple(group
, 'requires')
475 requires
.setAttributeNS(None, 'interface', d
.interface
)
477 if isinstance(b
, model
.EnvironmentBinding
):
478 env_elem
= addSimple(requires
, 'environment')
479 env_elem
.setAttributeNS(None, 'name', b
.name
)
480 env_elem
.setAttributeNS(None, 'insert', b
.insert
)
482 env_elem
.setAttributeNS(None, 'default', b
.default
)
484 raise Exception('Unknown binding type ' + b
)
489 impl_elem
= addSimple(group
, 'implementation')
490 impl_template
= buildenv
.get_binary_template()
492 # Copy attributes from template
493 for fullname
, value
in impl_template
.attrs
.iteritems():
494 if fullname
== 'arch':
499 ns
, localName
= fullname
.split(' ', 1)
501 ns
, localName
= None, fullname
502 prefixes
.setAttributeNS(impl_elem
, ns
, localName
, value
)
504 for child
in impl_template
.childNodes
:
505 impl_elem
.appendChild(child
.toDOM(doc
, prefixes
))
506 if impl_template
.content
:
507 impl_elem
.appendChild(doc
.createTextNode(impl_template
.content
))
510 group
.setAttributeNS(None, 'arch', buildenv
.target_arch
)
512 impl_elem
.setAttributeNS(None, 'version', src_impl
.version
)
514 version_modifier
= buildenv
.version_modifier
516 impl_elem
.setAttributeNS(None, 'version-modifier', version_modifier
)
518 impl_elem
.setAttributeNS(None, 'id', '..')
519 impl_elem
.setAttributeNS(None, 'released', time
.strftime('%Y-%m-%d'))
523 for ns
, prefix
in prefixes
.prefixes
.items():
524 root
.setAttributeNS(XMLNS_NAMESPACE
, 'xmlns:' + prefix
, ns
)
526 stream
= codecs
.open(path
, 'w', encoding
= 'utf-8')
532 def find_broken_version_symlinks(libdir
, mappings
):
533 """libdir may be a legacy -devel package containing lib* symlinks whose
534 targets would be provided by the corresponding runtime package. If so,
535 create fixed symlinks under $TMPDIR with the real location."""
537 if sys
.platform
== 'darwin':
542 for x
in os
.listdir(libdir
):
543 if x
.startswith(prefix
) and x
.endswith(extension
):
544 path
= os
.path
.join(libdir
, x
)
545 if os
.path
.islink(path
):
546 target
= os
.readlink(path
)
547 if '/' not in target
and not os
.path
.exists(os
.path
.join(libdir
, target
)):
548 print "Broken link %s -> %s; will relocate..." % (x
, target
)
549 mappings
[x
[len(prefix
):-len(extension
)]] = target
551 def set_up_mappings(mappings
):
552 """Create a temporary directory with symlinks for each of the library mappings."""
554 if sys
.platform
== 'darwin':
555 LD_LIBRARY_PATH
='DYLD_LIBRARY_PATH'
557 LD_LIBRARY_PATH
='LD_LIBRARY_PATH'
558 for d
in os
.environ
.get(LD_LIBRARY_PATH
, '').split(':'):
559 if d
: libdirs
.append(d
)
560 libdirs
+= ['/lib', '/usr/lib']
562 def add_ldconf(config_file
):
563 if not os
.path
.isfile(config_file
):
565 for line
in file(config_file
):
567 if d
.startswith('include '):
568 glob_pattern
= d
.split(' ', 1)[1]
569 for conf
in glob
.glob(glob_pattern
):
571 elif d
and not d
.startswith('#'):
573 add_ldconf('/etc/ld.so.conf')
575 def find_library(name
, wanted
):
576 # Takes a short-name and target name of a library and returns
577 # the full path of the library.
579 path
= os
.path
.join(d
, wanted
)
580 if os
.path
.exists(path
):
582 print "WARNING: library '%s' not found (searched '%s')!" % (wanted
, libdirs
)
585 mappings_dir
= os
.path
.join(os
.environ
['TMPDIR'], 'lib-mappings')
586 os
.mkdir(mappings_dir
)
588 old_path
= os
.environ
.get('LIBRARY_PATH', '')
589 if old_path
: old_path
= ':' + old_path
590 os
.environ
['LIBRARY_PATH'] = mappings_dir
+ old_path
592 if sys
.platform
== 'darwin':
596 for name
, wanted
in mappings
.items():
597 target
= find_library(name
, wanted
)
599 print "Adding mapping lib%s%s -> %s" % (name
, soext
, target
)
600 os
.symlink(target
, os
.path
.join(mappings_dir
, 'lib' + name
+ soext
))
603 srcdir
= os
.path
.join(os
.environ
['SRCDIR'], '')
604 builddir
= os
.environ
['BUILDDIR']
606 build_in_src
= srcdir
+ 'build' == builddir
608 for root
, dirs
, files
in os
.walk(srcdir
):
609 assert root
.startswith(srcdir
)
610 reldir
= root
[len(srcdir
):]
612 if reldir
== '.git' or (reldir
== 'build' and build_in_src
):
613 print "dup-src: skipping", reldir
618 target
= os
.path
.join(reldir
, f
)
619 #print "Copy %s -> %s" % (os.path.join(root, f), target)
620 if os
.path
.exists(target
):
622 fn(os
.path
.join(root
, f
), target
)
624 target
= os
.path
.join(reldir
, d
)
625 if not os
.path
.isdir(target
):
628 __main__
.commands
.append(do_build
)