3 # Thomas Nagy, 2005 (ita)
8 The class Build holds all the info related to a build:
9 * file system representation (tree of Node instances)
10 * various cached objects (task signatures, file scan results, ..)
12 There is only one Build object at a time (bld singleton)
15 import os, sys, errno, re, glob, gc, datetime, shutil
17 except: import pickle as cPickle
18 import Runner, TaskGen, Node, Scripting, Utils, Environment, Task, Logs, Options
19 from Logs import debug, error, info
20 from Constants import *
22 SAVED_ATTRS = 'root srcnode bldnode node_sigs node_deps raw_deps task_sigs id_nodes'.split()
23 "Build class members to save"
26 "singleton - safe to use when Waf is not used as a library"
28 class BuildError(Utils.WafError):
29 def __init__(self, b=None, t=[]):
33 Utils.WafError.__init__(self, self.format_error())
35 def format_error(self):
36 lst = ['Build failed:']
37 for tsk in self.tasks:
38 txt = tsk.format_error()
39 if txt: lst.append(txt)
45 def group_method(fun):
47 sets a build context method to execute after the current group has finished executing
48 this is useful for installing build files:
49 * calling install_files/install_as will fail if called too early
50 * people do not want to define install method in their task classes
55 if not k[0].is_install:
60 postpone = kw['postpone']
63 # TODO waf 1.6 in theory there should be no reference to the TaskManager internals here
66 if not m.groups: m.add_group()
67 m.groups[m.current_group].post_funs.append((fun, k, kw))
74 class BuildContext(Utils.Context):
75 "holds the dependency tree"
78 # not a singleton, but provided for compatibility
82 self.task_manager = Task.TaskManager()
84 # instead of hashing the nodes, we assign them a unique id when they are created
88 # map names to environments, the 'default' must be defined
91 # ======================================= #
92 # code for reading the scripts
94 # project build directory - do not reset() from load_dirs()
97 # the current directory from which the code is run
98 # the folder changes everytime a wscript is read
101 # Manual dependencies.
102 self.deps_man = Utils.DefaultDict(list)
104 # ======================================= #
107 # local cache for absolute paths - cache_node_abspath[variant][node]
108 self.cache_node_abspath = {}
110 # list of folders that are already scanned
111 # so that we do not need to stat them one more time
112 self.cache_scanned_folders = {}
114 # list of targets to uninstall for removing the empty folders after uninstalling
117 # ======================================= #
120 # build dir variants (release, debug, ..)
121 for v in 'cache_node_abspath task_sigs node_deps raw_deps node_sigs'.split():
123 setattr(self, v, var)
125 self.cache_dir_contents = {}
127 self.all_task_gen = []
128 self.task_gen_cache_names = {}
129 self.cache_sig_vars = {}
136 # bind the build context to the nodes in use
137 # this means better encapsulation and no build context singleton
138 class node_class(Node.Node):
140 self.node_class = node_class
141 self.node_class.__module__ = "Node"
142 self.node_class.__name__ = "Nodu"
143 self.node_class.bld = self
145 self.is_install = None
148 "nodes are not supposed to be copied"
149 raise Utils.WafError('build contexts are not supposed to be cloned')
152 "load the cache from the disk"
154 env = Environment.Environment(os.path.join(self.cachedir, 'build.config.py'))
155 except (IOError, OSError):
158 if env['version'] < HEXVERSION:
159 raise Utils.WafError('Version mismatch! reconfigure the project')
160 for t in env['tools']:
167 Node.Nodu = self.node_class
170 f = open(os.path.join(self.bdir, DBFILE), 'rb')
171 except (IOError, EOFError):
172 # handle missing file/empty file
176 if f: data = cPickle.load(f)
177 except AttributeError:
178 # handle file of an old Waf version
179 # that has an attribute which no longer exist
180 # (e.g. AttributeError: 'module' object has no attribute 'BuildDTO')
181 if Logs.verbose > 1: raise
184 for x in SAVED_ATTRS: setattr(self, x, data[x])
186 debug('build: Build cache loading failed')
193 "store the cache on disk, see self.load"
195 self.root.__class__.bld = None
197 # some people are very nervous with ctrl+c so we have to make a temporary file
198 Node.Nodu = self.node_class
199 db = os.path.join(self.bdir, DBFILE)
200 file = open(db + '.tmp', 'wb')
202 for x in SAVED_ATTRS: data[x] = getattr(self, x)
203 cPickle.dump(data, file, -1)
206 # do not use shutil.move
209 os.rename(db + '.tmp', db)
210 self.root.__class__.bld = self
213 # ======================================= #
216 debug('build: clean called')
218 # does not clean files created during the configuration
220 for env in self.all_envs.values():
221 for x in env[CFG_FILES]:
222 node = self.srcnode.find_resource(x)
224 precious.add(node.id)
227 for x in list(node.childs.keys()):
233 elif tp == Node.BUILD:
234 if nd.id in precious: continue
235 for env in self.all_envs.values():
236 try: os.remove(nd.abspath(env))
238 node.childs.__delitem__(x)
240 clean_rec(self.srcnode)
242 for v in 'node_sigs node_deps task_sigs raw_deps cache_node_abspath'.split():
246 """The cache file is not written if nothing was build at all (build is up to date)"""
247 debug('build: compile called')
250 import cProfile, pstats
251 cProfile.run("import Build\nBuild.bld.flush()", 'profi.txt')
252 p = pstats.Stats('profi.txt')
253 p.sort_stats('cumulative').print_stats(80)
258 self.generator = Runner.Parallel(self, Options.options.jobs)
261 if Options.options.progress_bar:
262 if on: sys.stderr.write(Logs.colors.cursor_on)
263 else: sys.stderr.write(Logs.colors.cursor_off)
265 debug('build: executor starting')
268 os.chdir(self.bldnode.abspath())
273 self.generator.start()
274 except KeyboardInterrupt:
276 # if self.generator.processed != 1: TODO
281 # do not store anything, for something bad happened
285 #if self.generator.processed != 1: TODO
288 if self.generator.error:
289 raise BuildError(self, self.task_manager.tasks_done)
295 "this function is called for both install and uninstall"
296 debug('build: install called')
300 # remove empty folders after uninstalling
301 if self.is_install < 0:
303 for x in self.uninstall:
304 dir = os.path.dirname(x)
305 if not dir in lst: lst.append(dir)
313 if not x in nlst: nlst.append(x)
314 x = os.path.dirname(x)
322 def new_task_gen(self, *k, **kw):
323 if self.task_gen_cache_names:
324 self.task_gen_cache_names = {}
328 ret = TaskGen.task_gen(*k, **kw)
332 try: cls = TaskGen.task_gen.classes[cls_name]
333 except KeyError: raise Utils.WscriptError('%s is not a valid task generator -> %s' %
334 (cls_name, [x for x in TaskGen.task_gen.classes]))
338 def __call__(self, *k, **kw):
339 if self.task_gen_cache_names:
340 self.task_gen_cache_names = {}
343 return TaskGen.task_gen(*k, **kw)
347 lst = Utils.listdir(self.cachedir)
349 if e.errno == errno.ENOENT:
350 raise Utils.WafError('The project was not configured: run "waf configure" first!')
355 raise Utils.WafError('The cache directory is empty: reconfigure the project')
358 if file.endswith(CACHE_SUFFIX):
359 env = Environment.Environment(os.path.join(self.cachedir, file))
360 name = file[:-len(CACHE_SUFFIX)]
362 self.all_envs[name] = env
366 for env in self.all_envs.values():
367 for f in env[CFG_FILES]:
368 newnode = self.path.find_or_declare(f)
370 hash = Utils.h_file(newnode.abspath(env))
371 except (IOError, AttributeError):
372 error("cannot find "+f)
374 self.node_sigs[env.variant()][newnode.id] = hash
376 # TODO: hmmm, these nodes are removed from the tree when calling rescan()
377 self.bldnode = self.root.find_dir(self.bldnode.abspath())
378 self.path = self.srcnode = self.root.find_dir(self.srcnode.abspath())
379 self.cwd = self.bldnode.abspath()
381 def setup(self, tool, tooldir=None, funs=None):
382 "setup tools for build process"
383 if isinstance(tool, list):
384 for i in tool: self.setup(i, tooldir)
387 if not tooldir: tooldir = Options.tooldir
389 module = Utils.load_tool(tool, tooldir)
390 if hasattr(module, "setup"): module.setup(self)
392 def init_variants(self):
393 debug('build: init variants')
396 for env in self.all_envs.values():
397 if not env.variant() in lstvariants:
398 lstvariants.append(env.variant())
399 self.lst_variants = lstvariants
401 debug('build: list of variants is %r', lstvariants)
403 for name in lstvariants+[0]:
404 for v in 'node_sigs cache_node_abspath'.split():
405 var = getattr(self, v)
409 # ======================================= #
410 # node and folder handling
412 # this should be the main entry point
413 def load_dirs(self, srcdir, blddir, load_cache=1):
414 "this functions should be the start of everything"
416 assert(os.path.isabs(srcdir))
417 assert(os.path.isabs(blddir))
419 self.cachedir = os.path.join(blddir, CACHE_DIR)
422 raise Utils.WafError("build dir must be different from srcdir: %s <-> %s " % (srcdir, blddir))
426 # try to load the cache file, if it does not exist, nothing happens
430 Node.Nodu = self.node_class
431 self.root = Node.Nodu('', None, Node.DIR)
434 self.srcnode = self.root.ensure_dir_node_from_path(srcdir)
435 debug('build: srcnode is %s and srcdir %s', self.srcnode.name, srcdir)
437 self.path = self.srcnode
439 # create this build dir if necessary
440 try: os.makedirs(blddir)
444 self.bldnode = self.root.ensure_dir_node_from_path(blddir)
448 def rescan(self, src_dir_node):
450 look the contents of a (folder)node and update its list of childs
452 The intent is to perform the following steps
453 * remove the nodes for the files that have disappeared
454 * remove the signatures for the build files that have disappeared
455 * cache the results of os.listdir
456 * create the build folder equivalent (mkdir) for each variant
457 src/bar -> build/default/src/bar, build/release/src/bar
459 when a folder in the source directory is removed, we do not check recursively
460 to remove the unused nodes. To do that, call 'waf clean' and build again.
463 # do not rescan over and over again
464 # TODO use a single variable in waf 1.6
465 if self.cache_scanned_folders.get(src_dir_node.id, None): return
466 self.cache_scanned_folders[src_dir_node.id] = True
468 # TODO remove in waf 1.6
469 if hasattr(self, 'repository'): self.repository(src_dir_node)
471 if not src_dir_node.name and sys.platform == 'win32':
472 # the root has no name, contains drive letters, and cannot be listed
476 # first, take the case of the source directory
477 parent_path = src_dir_node.abspath()
479 lst = set(Utils.listdir(parent_path))
483 # TODO move this at the bottom
484 self.cache_dir_contents[src_dir_node.id] = lst
486 # hash the existing source files, remove the others
487 cache = self.node_sigs[0]
488 for x in src_dir_node.childs.values():
489 if x.id & 3 != Node.FILE: continue
492 cache[x.id] = Utils.h_file(x.abspath())
494 raise Utils.WafError('The file %s is not readable or has become a dir' % x.abspath())
497 except KeyError: pass
499 del src_dir_node.childs[x.name]
502 # first obtain the differences between srcnode and src_dir_node
503 h1 = self.srcnode.height()
504 h2 = src_dir_node.height()
509 lst.append(child.name)
514 # list the files in the build dirs
516 for variant in self.lst_variants:
517 sub_path = os.path.join(self.bldnode.abspath(), variant , *lst)
518 self.listdir_bld(src_dir_node, sub_path, variant)
521 # listdir failed, remove the build node signatures for all variants
522 for node in src_dir_node.childs.values():
523 if node.id & 3 != Node.BUILD:
526 for dct in self.node_sigs.values():
528 dct.__delitem__(node.id)
530 # the policy is to avoid removing nodes representing directories
531 src_dir_node.childs.__delitem__(node.name)
533 for variant in self.lst_variants:
534 sub_path = os.path.join(self.bldnode.abspath(), variant , *lst)
536 os.makedirs(sub_path)
540 # ======================================= #
541 def listdir_src(self, parent_node):
542 """do not use, kept for compatibility"""
545 def remove_node(self, node):
546 """do not use, kept for compatibility"""
549 def listdir_bld(self, parent_node, path, variant):
550 """in this method we do not add timestamps but we remove them
551 when the files no longer exist (file removed in the build dir)"""
553 i_existing_nodes = [x for x in parent_node.childs.values() if x.id & 3 == Node.BUILD]
555 lst = set(Utils.listdir(path))
556 node_names = set([x.name for x in i_existing_nodes])
557 remove_names = node_names - lst
559 # remove the stamps of the build nodes that no longer exist on the filesystem
560 ids_to_remove = [x.id for x in i_existing_nodes if x.name in remove_names]
561 cache = self.node_sigs[variant]
562 for nid in ids_to_remove:
564 cache.__delitem__(nid)
567 return self.env_of_name('default')
568 def set_env(self, name, val):
569 self.all_envs[name] = val
571 env = property(get_env, set_env)
573 def add_manual_dependency(self, path, value):
574 if isinstance(path, Node.Node):
576 elif os.path.isabs(path):
577 node = self.root.find_resource(path)
579 node = self.path.find_resource(path)
580 self.deps_man[node.id].append(value)
582 def launch_node(self):
583 """return the launch directory as a node"""
584 # p_ln is kind of private, but public in case if
587 except AttributeError:
588 self.p_ln = self.root.find_dir(Options.launch_dir)
591 def glob(self, pattern, relative=True):
592 "files matching the pattern, seen from the current folder"
593 path = self.path.abspath()
594 files = [self.root.find_resource(x) for x in glob.glob(path+os.sep+pattern)]
596 files = [x.path_to_parent(self.path) for x in files if x]
598 files = [x.abspath() for x in files if x]
601 ## the following methods are candidates for the stable apis ##
603 def add_group(self, *k):
604 self.task_manager.add_group(*k)
606 def set_group(self, *k, **kw):
607 self.task_manager.set_group(*k, **kw)
609 def hash_env_vars(self, env, vars_lst):
610 """hash environment variables
611 ['CXX', ..] -> [env['CXX'], ..] -> md5()"""
613 # ccroot objects use the same environment for building the .o at once
614 # the same environment and the same variables are used
616 idx = str(id(env)) + str(vars_lst)
617 try: return self.cache_sig_vars[idx]
618 except KeyError: pass
620 lst = [str(env[a]) for a in vars_lst]
621 ret = Utils.h_list(lst)
622 debug('envhash: %r %r', ret, lst)
625 self.cache_sig_vars[idx] = ret
628 def name_to_obj(self, name, env):
629 """retrieve a task generator from its name or its target name
630 remember that names must be unique"""
631 cache = self.task_gen_cache_names
633 # create the index lazily
634 for x in self.all_task_gen:
635 vt = x.env.variant() + '_'
637 cache[vt + x.name] = x
639 if isinstance(x.target, str):
642 target = ' '.join(x.target)
644 if not cache.get(v, None):
646 return cache.get(env.variant() + '_' + name, None)
648 def get_tgen_by_name(self, name):
650 return self.name_to_obj(name, self.env)
652 def flush(self, all=1):
653 """tell the task generators to create the tasks"""
655 self.ini = datetime.datetime.now()
656 # force the initialization of the mapping name->object in flush
657 # name_to_obj can be used in userland scripts, in that case beware of incomplete mapping
658 self.task_gen_cache_names = {}
659 self.name_to_obj('', self.env)
661 debug('build: delayed operation TaskGen.flush() called')
663 if Options.options.compile_targets:
664 debug('task_gen: posting objects %r listed in compile_targets', Options.options.compile_targets)
666 mana = self.task_manager
670 # ensure the target names exist, fail before any post()
671 target_objects = Utils.DefaultDict(list)
672 for target_name in Options.options.compile_targets.split(','):
673 # trim target_name (handle cases when the user added spaces to targets)
674 target_name = target_name.strip()
675 for env in self.all_envs.values():
676 tg = self.name_to_obj(target_name, env)
678 target_objects[target_name].append(tg)
680 m = mana.group_idx(tg)
687 if not target_name in target_objects and all:
688 raise Utils.WafError("target '%s' does not exist" % target_name)
690 debug('group: Forcing up to group %s for target %s', mana.group_name(min_grp), Options.options.compile_targets)
692 # post all the task generators in previous groups
693 for i in xrange(len(mana.groups)):
694 mana.current_group = i
698 debug('group: Forcing group %s', mana.group_name(g))
699 for t in g.tasks_gen:
700 debug('group: Posting %s', t.name or t.target)
703 # then post the task generators listed in compile_targets in the last group
708 debug('task_gen: posting objects (normal)')
709 ln = self.launch_node()
710 # if the build is started from the build directory, do as if it was started from the top-level
711 # for the pretty-printing (Node.py), the two lines below cannot be moved to Build::launch_node
712 if ln.is_child_of(self.bldnode) or not ln.is_child_of(self.srcnode):
715 # if the project file is located under the source directory, build all targets by default
716 # else 'waf configure build' does nothing
717 proj_node = self.root.find_dir(os.path.split(Utils.g_module.root_path)[0])
718 if proj_node.id != self.srcnode.id:
721 for i in xrange(len(self.task_manager.groups)):
722 g = self.task_manager.groups[i]
723 self.task_manager.current_group = i
725 groups = [x for x in self.task_manager.groups_names if id(self.task_manager.groups_names[x]) == id(g)]
726 name = groups and groups[0] or 'unnamed'
727 Logs.debug('group: group', name)
728 for tg in g.tasks_gen:
729 if not tg.path.is_child_of(ln):
732 Logs.debug('group: %s' % tg)
735 def env_of_name(self, name):
737 return self.all_envs[name]
739 error('no such environment: '+name)
742 def progress_line(self, state, total, col1, col2):
746 ind = Utils.rot_chr[Utils.rot_idx % 4]
750 pc = (100.*state)/total
751 eta = Utils.get_elapsed_time(ini)
752 fs = "[%%%dd/%%%dd][%%s%%2d%%%%%%s][%s][" % (n, n, ind)
753 left = fs % (state, total, col1, pc, col2)
754 right = '][%s%s%s]' % (col1, eta, col2)
756 cols = Utils.get_term_cols() - len(left) - len(right) + 2*len(col1) + 2*len(col2)
757 if cols < 7: cols = 7
759 ratio = int((cols*state)/total) - 1
761 bar = ('='*ratio+'>').ljust(cols)
762 msg = Utils.indicator % (left, bar, right)
767 # do_install is not used anywhere
768 def do_install(self, src, tgt, chmod=O644):
769 """returns true if the file was effectively installed or uninstalled, false otherwise"""
770 if self.is_install > 0:
771 if not Options.options.force:
772 # check if the file is already there to avoid a copy
779 # same size and identical timestamps -> make no copy
780 if st1.st_mtime >= st2.st_mtime and st1.st_size == st2.st_size:
783 srclbl = src.replace(self.srcnode.abspath(None)+os.sep, '')
784 info("* installing %s as %s" % (srclbl, tgt))
786 # following is for shared libs and stale inodes (-_-)
791 shutil.copy2(src, tgt)
796 except (OSError, IOError):
797 error('File %r does not exist' % src)
798 raise Utils.WafError('Could not install the file %r' % tgt)
801 elif self.is_install < 0:
802 info("* uninstalling %s" % tgt)
804 self.uninstall.append(tgt)
809 if e.errno != errno.ENOENT:
810 if not getattr(self, 'uninstall_error', None):
811 self.uninstall_error = True
812 Logs.warn('build: some files could not be uninstalled (retry with -vv to list them)')
814 Logs.warn('could not remove %s (error code %r)' % (e.filename, e.errno))
817 red = re.compile(r"^([A-Za-z]:)?[/\\\\]*")
818 def get_install_path(self, path, env=None):
819 "installation path prefixed by the destdir, the variables like in '${PREFIX}/bin' are substituted"
820 if not env: env = self.env
821 destdir = env.get_destdir()
822 path = path.replace('/', os.sep)
823 destpath = Utils.subst_vars(path, env)
825 destpath = os.path.join(destdir, self.red.sub('', destpath))
828 def install_dir(self, path, env=None):
830 create empty folders for the installation (very rarely used)
833 assert isinstance(env, Environment.Environment), "invalid parameter"
840 destpath = self.get_install_path(path, env)
842 if self.is_install > 0:
843 info('* creating %s' % destpath)
844 Utils.check_dir(destpath)
845 elif self.is_install < 0:
846 info('* removing %s' % destpath)
847 self.uninstall.append(destpath + '/xxx') # yes, ugly
849 def install_files(self, path, files, env=None, chmod=O644, relative_trick=False, cwd=None):
850 """To install files only after they have been built, put the calls in a method named
851 post_build on the top-level wscript
853 The files must be a list and contain paths as strings or as Nodes
855 The relative_trick flag can be set to install folders, use bld.path.ant_glob() with it
858 assert isinstance(env, Environment.Environment), "invalid parameter"
862 if not path: return []
867 if isinstance(files, str) and '*' in files:
868 gl = cwd.abspath() + os.sep + files
871 lst = Utils.to_list(files)
873 if not getattr(lst, '__iter__', False):
876 destpath = self.get_install_path(path, env)
878 Utils.check_dir(destpath)
882 if isinstance(filename, str) and os.path.isabs(filename):
883 alst = Utils.split_path(filename)
884 destfile = os.path.join(destpath, alst[-1])
886 if isinstance(filename, Node.Node):
889 nd = cwd.find_resource(filename)
891 raise Utils.WafError("Unable to install the file %r (not found in %s)" % (filename, cwd))
894 destfile = os.path.join(destpath, filename)
895 Utils.check_dir(os.path.dirname(destfile))
897 destfile = os.path.join(destpath, nd.name)
899 filename = nd.abspath(env)
901 if self.do_install(filename, destfile, chmod):
902 installed_files.append(destfile)
903 return installed_files
905 def install_as(self, path, srcfile, env=None, chmod=O644, cwd=None):
907 srcfile may be a string or a Node representing the file to install
909 returns True if the file was effectively installed, False otherwise
912 assert isinstance(env, Environment.Environment), "invalid parameter"
917 raise Utils.WafError("where do you want to install %r? (%r?)" % (srcfile, path))
922 destpath = self.get_install_path(path, env)
924 dir, name = os.path.split(destpath)
928 if isinstance(srcfile, Node.Node):
929 src = srcfile.abspath(env)
932 if not os.path.isabs(srcfile):
933 node = cwd.find_resource(srcfile)
935 raise Utils.WafError("Unable to install the file %r (not found in %s)" % (srcfile, cwd))
936 src = node.abspath(env)
938 return self.do_install(src, destpath, chmod)
940 def symlink_as(self, path, src, env=None, cwd=None):
941 """example: bld.symlink_as('${PREFIX}/lib/libfoo.so', 'libfoo.so.1.2.3') """
943 if sys.platform == 'win32':
944 # well, this *cannot* work
948 raise Utils.WafError("where do you want to install %r? (%r?)" % (src, path))
950 tgt = self.get_install_path(path, env)
952 dir, name = os.path.split(tgt)
955 if self.is_install > 0:
957 if not os.path.islink(tgt):
959 elif os.readlink(tgt) != src:
966 info('* symlink %s (-> %s)' % (tgt, src))
972 info('* removing %s' % (tgt))
978 def exec_command(self, cmd, **kw):
979 # 'runner' zone is printed out for waf -v, see wafadmin/Options.py
980 debug('runner: system command -> %s', cmd)
982 self.log.write('%s\n' % cmd)
985 if not kw.get('cwd', None):
987 except AttributeError:
988 self.cwd = kw['cwd'] = self.bldnode.abspath()
989 return Utils.exec_command(cmd, **kw)
991 def printout(self, s):
992 f = self.log or sys.stderr
996 def add_subdirs(self, dirs):
997 self.recurse(dirs, 'build')
999 def pre_recurse(self, name_or_mod, path, nexdir):
1000 if not hasattr(self, 'oldpath'):
1002 self.oldpath.append(self.path)
1003 self.path = self.root.find_dir(nexdir)
1004 return {'bld': self, 'ctx': self}
1006 def post_recurse(self, name_or_mod, path, nexdir):
1007 self.path = self.oldpath.pop()
1009 ###### user-defined behaviour
1011 def pre_build(self):
1012 if hasattr(self, 'pre_funs'):
1013 for m in self.pre_funs:
1016 def post_build(self):
1017 if hasattr(self, 'post_funs'):
1018 for m in self.post_funs:
1021 def add_pre_fun(self, meth):
1022 try: self.pre_funs.append(meth)
1023 except AttributeError: self.pre_funs = [meth]
1025 def add_post_fun(self, meth):
1026 try: self.post_funs.append(meth)
1027 except AttributeError: self.post_funs = [meth]
1029 def use_the_magic(self):
1030 Task.algotype = Task.MAXPARALLEL
1031 Task.file_deps = Task.extract_deps
1034 install_as = group_method(install_as)
1035 install_files = group_method(install_files)
1036 symlink_as = group_method(symlink_as)