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 flush(self, all=1):
649 """tell the task generators to create the tasks"""
651 self.ini = datetime.datetime.now()
652 # force the initialization of the mapping name->object in flush
653 # name_to_obj can be used in userland scripts, in that case beware of incomplete mapping
654 self.task_gen_cache_names = {}
655 self.name_to_obj('', self.env)
657 debug('build: delayed operation TaskGen.flush() called')
659 if Options.options.compile_targets:
660 debug('task_gen: posting objects %r listed in compile_targets', Options.options.compile_targets)
662 mana = self.task_manager
666 # ensure the target names exist, fail before any post()
667 target_objects = Utils.DefaultDict(list)
668 for target_name in Options.options.compile_targets.split(','):
669 # trim target_name (handle cases when the user added spaces to targets)
670 target_name = target_name.strip()
671 for env in self.all_envs.values():
672 tg = self.name_to_obj(target_name, env)
674 target_objects[target_name].append(tg)
676 m = mana.group_idx(tg)
683 if not target_name in target_objects and all:
684 raise Utils.WafError("target '%s' does not exist" % target_name)
686 debug('group: Forcing up to group %s for target %s', mana.group_name(min_grp), Options.options.compile_targets)
688 # post all the task generators in previous groups
689 for i in xrange(len(mana.groups)):
690 mana.current_group = i
694 debug('group: Forcing group %s', mana.group_name(g))
695 for t in g.tasks_gen:
696 debug('group: Posting %s', t.name or t.target)
699 # then post the task generators listed in compile_targets in the last group
704 debug('task_gen: posting objects (normal)')
705 ln = self.launch_node()
706 # if the build is started from the build directory, do as if it was started from the top-level
707 # for the pretty-printing (Node.py), the two lines below cannot be moved to Build::launch_node
708 if ln.is_child_of(self.bldnode) or not ln.is_child_of(self.srcnode):
711 # if the project file is located under the source directory, build all targets by default
712 # else 'waf configure build' does nothing
713 proj_node = self.root.find_dir(os.path.split(Utils.g_module.root_path)[0])
714 if proj_node.id != self.srcnode.id:
717 for i in xrange(len(self.task_manager.groups)):
718 g = self.task_manager.groups[i]
719 self.task_manager.current_group = i
721 groups = [x for x in self.task_manager.groups_names if id(self.task_manager.groups_names[x]) == id(g)]
722 name = groups and groups[0] or 'unnamed'
723 Logs.debug('group: group', name)
724 for tg in g.tasks_gen:
725 if not tg.path.is_child_of(ln):
728 Logs.debug('group: %s' % tg)
731 def env_of_name(self, name):
733 return self.all_envs[name]
735 error('no such environment: '+name)
738 def progress_line(self, state, total, col1, col2):
742 ind = Utils.rot_chr[Utils.rot_idx % 4]
746 pc = (100.*state)/total
747 eta = Utils.get_elapsed_time(ini)
748 fs = "[%%%dd/%%%dd][%%s%%2d%%%%%%s][%s][" % (n, n, ind)
749 left = fs % (state, total, col1, pc, col2)
750 right = '][%s%s%s]' % (col1, eta, col2)
752 cols = Utils.get_term_cols() - len(left) - len(right) + 2*len(col1) + 2*len(col2)
753 if cols < 7: cols = 7
755 ratio = int((cols*state)/total) - 1
757 bar = ('='*ratio+'>').ljust(cols)
758 msg = Utils.indicator % (left, bar, right)
763 # do_install is not used anywhere
764 def do_install(self, src, tgt, chmod=O644):
765 """returns true if the file was effectively installed or uninstalled, false otherwise"""
766 if self.is_install > 0:
767 if not Options.options.force:
768 # check if the file is already there to avoid a copy
775 # same size and identical timestamps -> make no copy
776 if st1.st_mtime >= st2.st_mtime and st1.st_size == st2.st_size:
779 srclbl = src.replace(self.srcnode.abspath(None)+os.sep, '')
780 info("* installing %s as %s" % (srclbl, tgt))
782 # following is for shared libs and stale inodes (-_-)
787 shutil.copy2(src, tgt)
792 except (OSError, IOError):
793 error('File %r does not exist' % src)
794 raise Utils.WafError('Could not install the file %r' % tgt)
797 elif self.is_install < 0:
798 info("* uninstalling %s" % tgt)
800 self.uninstall.append(tgt)
805 if e.errno != errno.ENOENT:
806 if not getattr(self, 'uninstall_error', None):
807 self.uninstall_error = True
808 Logs.warn('build: some files could not be uninstalled (retry with -vv to list them)')
810 Logs.warn('could not remove %s (error code %r)' % (e.filename, e.errno))
813 red = re.compile(r"^([A-Za-z]:)?[/\\\\]*")
814 def get_install_path(self, path, env=None):
815 "installation path prefixed by the destdir, the variables like in '${PREFIX}/bin' are substituted"
816 if not env: env = self.env
817 destdir = env.get_destdir()
818 path = path.replace('/', os.sep)
819 destpath = Utils.subst_vars(path, env)
821 destpath = os.path.join(destdir, self.red.sub('', destpath))
824 def install_dir(self, path, env=None):
826 create empty folders for the installation (very rarely used)
829 assert isinstance(env, Environment.Environment), "invalid parameter"
836 destpath = self.get_install_path(path, env)
838 if self.is_install > 0:
839 info('* creating %s' % destpath)
840 Utils.check_dir(destpath)
841 elif self.is_install < 0:
842 info('* removing %s' % destpath)
843 self.uninstall.append(destpath + '/xxx') # yes, ugly
845 def install_files(self, path, files, env=None, chmod=O644, relative_trick=False, cwd=None):
846 """To install files only after they have been built, put the calls in a method named
847 post_build on the top-level wscript
849 The files must be a list and contain paths as strings or as Nodes
851 The relative_trick flag can be set to install folders, use bld.path.ant_glob() with it
854 assert isinstance(env, Environment.Environment), "invalid parameter"
858 if not path: return []
863 if isinstance(files, str) and '*' in files:
864 gl = cwd.abspath() + os.sep + files
867 lst = Utils.to_list(files)
869 if not getattr(lst, '__iter__', False):
872 destpath = self.get_install_path(path, env)
874 Utils.check_dir(destpath)
878 if isinstance(filename, str) and os.path.isabs(filename):
879 alst = Utils.split_path(filename)
880 destfile = os.path.join(destpath, alst[-1])
882 if isinstance(filename, Node.Node):
885 nd = cwd.find_resource(filename)
887 raise Utils.WafError("Unable to install the file %r (not found in %s)" % (filename, cwd))
890 destfile = os.path.join(destpath, filename)
891 Utils.check_dir(os.path.dirname(destfile))
893 destfile = os.path.join(destpath, nd.name)
895 filename = nd.abspath(env)
897 if self.do_install(filename, destfile, chmod):
898 installed_files.append(destfile)
899 return installed_files
901 def install_as(self, path, srcfile, env=None, chmod=O644, cwd=None):
903 srcfile may be a string or a Node representing the file to install
905 returns True if the file was effectively installed, False otherwise
908 assert isinstance(env, Environment.Environment), "invalid parameter"
913 raise Utils.WafError("where do you want to install %r? (%r?)" % (srcfile, path))
918 destpath = self.get_install_path(path, env)
920 dir, name = os.path.split(destpath)
924 if isinstance(srcfile, Node.Node):
925 src = srcfile.abspath(env)
928 if not os.path.isabs(srcfile):
929 node = cwd.find_resource(srcfile)
931 raise Utils.WafError("Unable to install the file %r (not found in %s)" % (srcfile, cwd))
932 src = node.abspath(env)
934 return self.do_install(src, destpath, chmod)
936 def symlink_as(self, path, src, env=None, cwd=None):
937 """example: bld.symlink_as('${PREFIX}/lib/libfoo.so', 'libfoo.so.1.2.3') """
939 if sys.platform == 'win32':
940 # well, this *cannot* work
944 raise Utils.WafError("where do you want to install %r? (%r?)" % (src, path))
946 tgt = self.get_install_path(path, env)
948 dir, name = os.path.split(tgt)
951 if self.is_install > 0:
953 if not os.path.islink(tgt):
955 elif os.readlink(tgt) != src:
962 info('* symlink %s (-> %s)' % (tgt, src))
968 info('* removing %s' % (tgt))
974 def exec_command(self, cmd, **kw):
975 # 'runner' zone is printed out for waf -v, see wafadmin/Options.py
976 debug('runner: system command -> %s', cmd)
978 self.log.write('%s\n' % cmd)
981 if not kw.get('cwd', None):
983 except AttributeError:
984 self.cwd = kw['cwd'] = self.bldnode.abspath()
985 return Utils.exec_command(cmd, **kw)
987 def printout(self, s):
988 f = self.log or sys.stderr
992 def add_subdirs(self, dirs):
993 self.recurse(dirs, 'build')
995 def pre_recurse(self, name_or_mod, path, nexdir):
996 if not hasattr(self, 'oldpath'):
998 self.oldpath.append(self.path)
999 self.path = self.root.find_dir(nexdir)
1000 return {'bld': self, 'ctx': self}
1002 def post_recurse(self, name_or_mod, path, nexdir):
1003 self.path = self.oldpath.pop()
1005 ###### user-defined behaviour
1007 def pre_build(self):
1008 if hasattr(self, 'pre_funs'):
1009 for m in self.pre_funs:
1012 def post_build(self):
1013 if hasattr(self, 'post_funs'):
1014 for m in self.post_funs:
1017 def add_pre_fun(self, meth):
1018 try: self.pre_funs.append(meth)
1019 except AttributeError: self.pre_funs = [meth]
1021 def add_post_fun(self, meth):
1022 try: self.post_funs.append(meth)
1023 except AttributeError: self.post_funs = [meth]
1025 def use_the_magic(self):
1026 Task.algotype = Task.MAXPARALLEL
1027 Task.file_deps = Task.extract_deps
1030 install_as = group_method(install_as)
1031 install_files = group_method(install_files)
1032 symlink_as = group_method(symlink_as)