50f4d7f6c69e949ddb7a89b4760063d90aea4b0e
[sfrench/samba-autobuild/.git] / third_party / waf / wafadmin / Build.py
1 #!/usr/bin/env python
2 # encoding: utf-8
3 # Thomas Nagy, 2005 (ita)
4
5 """
6 Dependency tree holder
7
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, ..)
11
12 There is only one Build object at a time (bld singleton)
13 """
14
15 import os, sys, errno, re, glob, gc, datetime, shutil
16 try: import cPickle
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 *
21
22 SAVED_ATTRS = 'root srcnode bldnode node_sigs node_deps raw_deps task_sigs id_nodes'.split()
23 "Build class members to save"
24
25 bld = None
26 "singleton - safe to use when Waf is not used as a library"
27
28 class BuildError(Utils.WafError):
29         def __init__(self, b=None, t=[]):
30                 self.bld = b
31                 self.tasks = t
32                 self.ret = 1
33                 Utils.WafError.__init__(self, self.format_error())
34
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)
40                 sep = ' '
41                 if len(lst) > 2:
42                         sep = '\n'
43                 return sep.join(lst)
44
45 def group_method(fun):
46         """
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
51
52         TODO: try it
53         """
54         def f(*k, **kw):
55                 if not k[0].is_install:
56                         return False
57
58                 postpone = True
59                 if 'postpone' in kw:
60                         postpone = kw['postpone']
61                         del kw['postpone']
62
63                 # TODO waf 1.6 in theory there should be no reference to the TaskManager internals here
64                 if postpone:
65                         m = k[0].task_manager
66                         if not m.groups: m.add_group()
67                         m.groups[m.current_group].post_funs.append((fun, k, kw))
68                         if not 'cwd' in kw:
69                                 kw['cwd'] = k[0].path
70                 else:
71                         fun(*k, **kw)
72         return f
73
74 class BuildContext(Utils.Context):
75         "holds the dependency tree"
76         def __init__(self):
77
78                 # not a singleton, but provided for compatibility
79                 global bld
80                 bld = self
81
82                 self.task_manager = Task.TaskManager()
83
84                 # instead of hashing the nodes, we assign them a unique id when they are created
85                 self.id_nodes = 0
86                 self.idx = {}
87
88                 # map names to environments, the 'default' must be defined
89                 self.all_envs = {}
90
91                 # ======================================= #
92                 # code for reading the scripts
93
94                 # project build directory - do not reset() from load_dirs()
95                 self.bdir = ''
96
97                 # the current directory from which the code is run
98                 # the folder changes everytime a wscript is read
99                 self.path = None
100
101                 # Manual dependencies.
102                 self.deps_man = Utils.DefaultDict(list)
103
104                 # ======================================= #
105                 # cache variables
106
107                 # local cache for absolute paths - cache_node_abspath[variant][node]
108                 self.cache_node_abspath = {}
109
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 = {}
113
114                 # list of targets to uninstall for removing the empty folders after uninstalling
115                 self.uninstall = []
116
117                 # ======================================= #
118                 # tasks and objects
119
120                 # build dir variants (release, debug, ..)
121                 for v in 'cache_node_abspath task_sigs node_deps raw_deps node_sigs'.split():
122                         var = {}
123                         setattr(self, v, var)
124
125                 self.cache_dir_contents = {}
126
127                 self.all_task_gen = []
128                 self.task_gen_cache_names = {}
129                 self.cache_sig_vars = {}
130                 self.log = None
131
132                 self.root = None
133                 self.srcnode = None
134                 self.bldnode = None
135
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):
139                         pass
140                 self.node_class = node_class
141                 self.node_class.__module__ = "Node"
142                 self.node_class.__name__ = "Nodu"
143                 self.node_class.bld = self
144
145                 self.is_install = None
146
147         def __copy__(self):
148                 "nodes are not supposed to be copied"
149                 raise Utils.WafError('build contexts are not supposed to be cloned')
150
151         def load(self):
152                 "load the cache from the disk"
153                 try:
154                         env = Environment.Environment(os.path.join(self.cachedir, 'build.config.py'))
155                 except (IOError, OSError):
156                         pass
157                 else:
158                         if env['version'] < HEXVERSION:
159                                 raise Utils.WafError('Version mismatch! reconfigure the project')
160                         for t in env['tools']:
161                                 self.setup(**t)
162
163                 try:
164                         gc.disable()
165                         f = data = None
166
167                         Node.Nodu = self.node_class
168
169                         try:
170                                 f = open(os.path.join(self.bdir, DBFILE), 'rb')
171                         except (IOError, EOFError):
172                                 # handle missing file/empty file
173                                 pass
174
175                         try:
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
182
183                         if data:
184                                 for x in SAVED_ATTRS: setattr(self, x, data[x])
185                         else:
186                                 debug('build: Build cache loading failed')
187
188                 finally:
189                         if f: f.close()
190                         gc.enable()
191
192         def save(self):
193                 "store the cache on disk, see self.load"
194                 gc.disable()
195                 self.root.__class__.bld = None
196
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')
201                 data = {}
202                 for x in SAVED_ATTRS: data[x] = getattr(self, x)
203                 cPickle.dump(data, file, -1)
204                 file.close()
205
206                 # do not use shutil.move
207                 try: os.unlink(db)
208                 except OSError: pass
209                 os.rename(db + '.tmp', db)
210                 self.root.__class__.bld = self
211                 gc.enable()
212
213         # ======================================= #
214
215         def clean(self):
216                 debug('build: clean called')
217
218                 # does not clean files created during the configuration
219                 precious = set([])
220                 for env in self.all_envs.values():
221                         for x in env[CFG_FILES]:
222                                 node = self.srcnode.find_resource(x)
223                                 if node:
224                                         precious.add(node.id)
225
226                 def clean_rec(node):
227                         for x in list(node.childs.keys()):
228                                 nd = node.childs[x]
229
230                                 tp = nd.id & 3
231                                 if tp == Node.DIR:
232                                         clean_rec(nd)
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))
237                                                 except OSError: pass
238                                         node.childs.__delitem__(x)
239
240                 clean_rec(self.srcnode)
241
242                 for v in 'node_sigs node_deps task_sigs raw_deps cache_node_abspath'.split():
243                         setattr(self, v, {})
244
245         def compile(self):
246                 """The cache file is not written if nothing was build at all (build is up to date)"""
247                 debug('build: compile called')
248
249                 """
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)
254                 """
255                 self.flush()
256                 #"""
257
258                 self.generator = Runner.Parallel(self, Options.options.jobs)
259
260                 def dw(on=True):
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)
264
265                 debug('build: executor starting')
266
267                 back = os.getcwd()
268                 os.chdir(self.bldnode.abspath())
269
270                 try:
271                         try:
272                                 dw(on=False)
273                                 self.generator.start()
274                         except KeyboardInterrupt:
275                                 dw()
276                                 # if self.generator.processed != 1: TODO
277                                 self.save()
278                                 raise
279                         except Exception:
280                                 dw()
281                                 # do not store anything, for something bad happened
282                                 raise
283                         else:
284                                 dw()
285                                 #if self.generator.processed != 1: TODO
286                                 self.save()
287
288                         if self.generator.error:
289                                 raise BuildError(self, self.task_manager.tasks_done)
290
291                 finally:
292                         os.chdir(back)
293
294         def install(self):
295                 "this function is called for both install and uninstall"
296                 debug('build: install called')
297
298                 self.flush()
299
300                 # remove empty folders after uninstalling
301                 if self.is_install < 0:
302                         lst = []
303                         for x in self.uninstall:
304                                 dir = os.path.dirname(x)
305                                 if not dir in lst: lst.append(dir)
306                         lst.sort()
307                         lst.reverse()
308
309                         nlst = []
310                         for y in lst:
311                                 x = y
312                                 while len(x) > 4:
313                                         if not x in nlst: nlst.append(x)
314                                         x = os.path.dirname(x)
315
316                         nlst.sort()
317                         nlst.reverse()
318                         for x in nlst:
319                                 try: os.rmdir(x)
320                                 except OSError: pass
321
322         def new_task_gen(self, *k, **kw):
323                 if self.task_gen_cache_names:
324                         self.task_gen_cache_names = {}
325
326                 kw['bld'] = self
327                 if len(k) == 0:
328                         ret = TaskGen.task_gen(*k, **kw)
329                 else:
330                         cls_name = k[0]
331
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]))
335                         ret = cls(*k, **kw)
336                 return ret
337
338         def __call__(self, *k, **kw):
339                 if self.task_gen_cache_names:
340                         self.task_gen_cache_names = {}
341
342                 kw['bld'] = self
343                 return TaskGen.task_gen(*k, **kw)
344
345         def load_envs(self):
346                 try:
347                         lst = Utils.listdir(self.cachedir)
348                 except OSError, e:
349                         if e.errno == errno.ENOENT:
350                                 raise Utils.WafError('The project was not configured: run "waf configure" first!')
351                         else:
352                                 raise
353
354                 if not lst:
355                         raise Utils.WafError('The cache directory is empty: reconfigure the project')
356
357                 for file in lst:
358                         if file.endswith(CACHE_SUFFIX):
359                                 env = Environment.Environment(os.path.join(self.cachedir, file))
360                                 name = file[:-len(CACHE_SUFFIX)]
361
362                                 self.all_envs[name] = env
363
364                 self.init_variants()
365
366                 for env in self.all_envs.values():
367                         for f in env[CFG_FILES]:
368                                 newnode = self.path.find_or_declare(f)
369                                 try:
370                                         hash = Utils.h_file(newnode.abspath(env))
371                                 except (IOError, AttributeError):
372                                         error("cannot find "+f)
373                                         hash = SIG_NIL
374                                 self.node_sigs[env.variant()][newnode.id] = hash
375
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()
380
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)
385                         return
386
387                 if not tooldir: tooldir = Options.tooldir
388
389                 module = Utils.load_tool(tool, tooldir)
390                 if hasattr(module, "setup"): module.setup(self)
391
392         def init_variants(self):
393                 debug('build: init variants')
394
395                 lstvariants = []
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
400
401                 debug('build: list of variants is %r', lstvariants)
402
403                 for name in lstvariants+[0]:
404                         for v in 'node_sigs cache_node_abspath'.split():
405                                 var = getattr(self, v)
406                                 if not name in var:
407                                         var[name] = {}
408
409         # ======================================= #
410         # node and folder handling
411
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"
415
416                 assert(os.path.isabs(srcdir))
417                 assert(os.path.isabs(blddir))
418
419                 self.cachedir = os.path.join(blddir, CACHE_DIR)
420
421                 if srcdir == blddir:
422                         raise Utils.WafError("build dir must be different from srcdir: %s <-> %s " % (srcdir, blddir))
423
424                 self.bdir = blddir
425
426                 # try to load the cache file, if it does not exist, nothing happens
427                 self.load()
428
429                 if not self.root:
430                         Node.Nodu = self.node_class
431                         self.root = Node.Nodu('', None, Node.DIR)
432
433                 if not self.srcnode:
434                         self.srcnode = self.root.ensure_dir_node_from_path(srcdir)
435                 debug('build: srcnode is %s and srcdir %s', self.srcnode.name, srcdir)
436
437                 self.path = self.srcnode
438
439                 # create this build dir if necessary
440                 try: os.makedirs(blddir)
441                 except OSError: pass
442
443                 if not self.bldnode:
444                         self.bldnode = self.root.ensure_dir_node_from_path(blddir)
445
446                 self.init_variants()
447
448         def rescan(self, src_dir_node):
449                 """
450                 look the contents of a (folder)node and update its list of childs
451
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
458
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.
461                 """
462
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
467
468                 # TODO remove in waf 1.6
469                 if hasattr(self, 'repository'): self.repository(src_dir_node)
470
471                 if not src_dir_node.name and sys.platform == 'win32':
472                         # the root has no name, contains drive letters, and cannot be listed
473                         return
474
475
476                 # first, take the case of the source directory
477                 parent_path = src_dir_node.abspath()
478                 try:
479                         lst = set(Utils.listdir(parent_path))
480                 except OSError:
481                         lst = set([])
482
483                 # TODO move this at the bottom
484                 self.cache_dir_contents[src_dir_node.id] = lst
485
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
490                         if x.name in lst:
491                                 try:
492                                         cache[x.id] = Utils.h_file(x.abspath())
493                                 except IOError:
494                                         raise Utils.WafError('The file %s is not readable or has become a dir' % x.abspath())
495                         else:
496                                 try: del cache[x.id]
497                                 except KeyError: pass
498
499                                 del src_dir_node.childs[x.name]
500
501
502                 # first obtain the differences between srcnode and src_dir_node
503                 h1 = self.srcnode.height()
504                 h2 = src_dir_node.height()
505
506                 lst = []
507                 child = src_dir_node
508                 while h2 > h1:
509                         lst.append(child.name)
510                         child = child.parent
511                         h2 -= 1
512                 lst.reverse()
513
514                 # list the files in the build dirs
515                 try:
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)
519                 except OSError:
520
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:
524                                         continue
525
526                                 for dct in self.node_sigs.values():
527                                         if node.id in dct:
528                                                 dct.__delitem__(node.id)
529
530                                 # the policy is to avoid removing nodes representing directories
531                                 src_dir_node.childs.__delitem__(node.name)
532
533                         for variant in self.lst_variants:
534                                 sub_path = os.path.join(self.bldnode.abspath(), variant , *lst)
535                                 try:
536                                         os.makedirs(sub_path)
537                                 except OSError:
538                                         pass
539
540         # ======================================= #
541         def listdir_src(self, parent_node):
542                 """do not use, kept for compatibility"""
543                 pass
544
545         def remove_node(self, node):
546                 """do not use, kept for compatibility"""
547                 pass
548
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)"""
552
553                 i_existing_nodes = [x for x in parent_node.childs.values() if x.id & 3 == Node.BUILD]
554
555                 lst = set(Utils.listdir(path))
556                 node_names = set([x.name for x in i_existing_nodes])
557                 remove_names = node_names - lst
558
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:
563                         if nid in cache:
564                                 cache.__delitem__(nid)
565
566         def get_env(self):
567                 return self.env_of_name('default')
568         def set_env(self, name, val):
569                 self.all_envs[name] = val
570
571         env = property(get_env, set_env)
572
573         def add_manual_dependency(self, path, value):
574                 if isinstance(path, Node.Node):
575                         node = path
576                 elif os.path.isabs(path):
577                         node = self.root.find_resource(path)
578                 else:
579                         node = self.path.find_resource(path)
580                 self.deps_man[node.id].append(value)
581
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
585                 try:
586                         return self.p_ln
587                 except AttributeError:
588                         self.p_ln = self.root.find_dir(Options.launch_dir)
589                         return self.p_ln
590
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)]
595                 if relative:
596                         files = [x.path_to_parent(self.path) for x in files if x]
597                 else:
598                         files = [x.abspath() for x in files if x]
599                 return files
600
601         ## the following methods are candidates for the stable apis ##
602
603         def add_group(self, *k):
604                 self.task_manager.add_group(*k)
605
606         def set_group(self, *k, **kw):
607                 self.task_manager.set_group(*k, **kw)
608
609         def hash_env_vars(self, env, vars_lst):
610                 """hash environment variables
611                 ['CXX', ..] -> [env['CXX'], ..] -> md5()"""
612
613                 # ccroot objects use the same environment for building the .o at once
614                 # the same environment and the same variables are used
615
616                 idx = str(id(env)) + str(vars_lst)
617                 try: return self.cache_sig_vars[idx]
618                 except KeyError: pass
619
620                 lst = [str(env[a]) for a in vars_lst]
621                 ret = Utils.h_list(lst)
622                 debug('envhash: %r %r', ret, lst)
623
624                 # next time
625                 self.cache_sig_vars[idx] = ret
626                 return ret
627
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
632                 if not cache:
633                         # create the index lazily
634                         for x in self.all_task_gen:
635                                 vt = x.env.variant() + '_'
636                                 if x.name:
637                                         cache[vt + x.name] = x
638                                 else:
639                                         if isinstance(x.target, str):
640                                                 target = x.target
641                                         else:
642                                                 target = ' '.join(x.target)
643                                         v = vt + target
644                                         if not cache.get(v, None):
645                                                 cache[v] = x
646                 return cache.get(env.variant() + '_' + name, None)
647
648         def flush(self, all=1):
649                 """tell the task generators to create the tasks"""
650
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)
656
657                 debug('build: delayed operation TaskGen.flush() called')
658
659                 if Options.options.compile_targets:
660                         debug('task_gen: posting objects %r listed in compile_targets', Options.options.compile_targets)
661
662                         mana = self.task_manager
663                         to_post = []
664                         min_grp = 0
665
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)
673                                         if tg:
674                                                 target_objects[target_name].append(tg)
675
676                                                 m = mana.group_idx(tg)
677                                                 if m > min_grp:
678                                                         min_grp = m
679                                                         to_post = [tg]
680                                                 elif m == min_grp:
681                                                         to_post.append(tg)
682
683                                 if not target_name in target_objects and all:
684                                         raise Utils.WafError("target '%s' does not exist" % target_name)
685
686                         debug('group: Forcing up to group %s for target %s', mana.group_name(min_grp), Options.options.compile_targets)
687
688                         # post all the task generators in previous groups
689                         for i in xrange(len(mana.groups)):
690                                 mana.current_group = i
691                                 if i == min_grp:
692                                         break
693                                 g = mana.groups[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)
697                                         t.post()
698
699                         # then post the task generators listed in compile_targets in the last group
700                         for t in to_post:
701                                 t.post()
702
703                 else:
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):
709                                 ln = self.srcnode
710
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:
715                                 ln = self.srcnode
716
717                         for i in xrange(len(self.task_manager.groups)):
718                                 g = self.task_manager.groups[i]
719                                 self.task_manager.current_group = i
720                                 if Logs.verbose:
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):
726                                                 continue
727                                         if Logs.verbose:
728                                                 Logs.debug('group: %s' % tg)
729                                         tg.post()
730
731         def env_of_name(self, name):
732                 try:
733                         return self.all_envs[name]
734                 except KeyError:
735                         error('no such environment: '+name)
736                         return None
737
738         def progress_line(self, state, total, col1, col2):
739                 n = len(str(total))
740
741                 Utils.rot_idx += 1
742                 ind = Utils.rot_chr[Utils.rot_idx % 4]
743
744                 ini = self.ini
745
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)
751
752                 cols = Utils.get_term_cols() - len(left) - len(right) + 2*len(col1) + 2*len(col2)
753                 if cols < 7: cols = 7
754
755                 ratio = int((cols*state)/total) - 1
756
757                 bar = ('='*ratio+'>').ljust(cols)
758                 msg = Utils.indicator % (left, bar, right)
759
760                 return msg
761
762
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
769                                 try:
770                                         st1 = os.stat(tgt)
771                                         st2 = os.stat(src)
772                                 except OSError:
773                                         pass
774                                 else:
775                                         # same size and identical timestamps -> make no copy
776                                         if st1.st_mtime >= st2.st_mtime and st1.st_size == st2.st_size:
777                                                 return False
778
779                         srclbl = src.replace(self.srcnode.abspath(None)+os.sep, '')
780                         info("* installing %s as %s" % (srclbl, tgt))
781
782                         # following is for shared libs and stale inodes (-_-)
783                         try: os.remove(tgt)
784                         except OSError: pass
785
786                         try:
787                                 shutil.copy2(src, tgt)
788                                 os.chmod(tgt, chmod)
789                         except IOError:
790                                 try:
791                                         os.stat(src)
792                                 except (OSError, IOError):
793                                         error('File %r does not exist' % src)
794                                 raise Utils.WafError('Could not install the file %r' % tgt)
795                         return True
796
797                 elif self.is_install < 0:
798                         info("* uninstalling %s" % tgt)
799
800                         self.uninstall.append(tgt)
801
802                         try:
803                                 os.remove(tgt)
804                         except OSError, e:
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)')
809                                         if Logs.verbose > 1:
810                                                 Logs.warn('could not remove %s (error code %r)' % (e.filename, e.errno))
811                         return True
812
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)
820                 if destdir:
821                         destpath = os.path.join(destdir, self.red.sub('', destpath))
822                 return destpath
823
824         def install_dir(self, path, env=None):
825                 """
826                 create empty folders for the installation (very rarely used)
827                 """
828                 if env:
829                         assert isinstance(env, Environment.Environment), "invalid parameter"
830                 else:
831                         env = self.env
832
833                 if not path:
834                         return []
835
836                 destpath = self.get_install_path(path, env)
837
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
844
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
848
849                 The files must be a list and contain paths as strings or as Nodes
850
851                 The relative_trick flag can be set to install folders, use bld.path.ant_glob() with it
852                 """
853                 if env:
854                         assert isinstance(env, Environment.Environment), "invalid parameter"
855                 else:
856                         env = self.env
857
858                 if not path: return []
859
860                 if not cwd:
861                         cwd = self.path
862
863                 if isinstance(files, str) and '*' in files:
864                         gl = cwd.abspath() + os.sep + files
865                         lst = glob.glob(gl)
866                 else:
867                         lst = Utils.to_list(files)
868
869                 if not getattr(lst, '__iter__', False):
870                         lst = [lst]
871
872                 destpath = self.get_install_path(path, env)
873
874                 Utils.check_dir(destpath)
875
876                 installed_files = []
877                 for filename in lst:
878                         if isinstance(filename, str) and os.path.isabs(filename):
879                                 alst = Utils.split_path(filename)
880                                 destfile = os.path.join(destpath, alst[-1])
881                         else:
882                                 if isinstance(filename, Node.Node):
883                                         nd = filename
884                                 else:
885                                         nd = cwd.find_resource(filename)
886                                 if not nd:
887                                         raise Utils.WafError("Unable to install the file %r (not found in %s)" % (filename, cwd))
888
889                                 if relative_trick:
890                                         destfile = os.path.join(destpath, filename)
891                                         Utils.check_dir(os.path.dirname(destfile))
892                                 else:
893                                         destfile = os.path.join(destpath, nd.name)
894
895                                 filename = nd.abspath(env)
896
897                         if self.do_install(filename, destfile, chmod):
898                                 installed_files.append(destfile)
899                 return installed_files
900
901         def install_as(self, path, srcfile, env=None, chmod=O644, cwd=None):
902                 """
903                 srcfile may be a string or a Node representing the file to install
904
905                 returns True if the file was effectively installed, False otherwise
906                 """
907                 if env:
908                         assert isinstance(env, Environment.Environment), "invalid parameter"
909                 else:
910                         env = self.env
911
912                 if not path:
913                         raise Utils.WafError("where do you want to install %r? (%r?)" % (srcfile, path))
914
915                 if not cwd:
916                         cwd = self.path
917
918                 destpath = self.get_install_path(path, env)
919
920                 dir, name = os.path.split(destpath)
921                 Utils.check_dir(dir)
922
923                 # the source path
924                 if isinstance(srcfile, Node.Node):
925                         src = srcfile.abspath(env)
926                 else:
927                         src = srcfile
928                         if not os.path.isabs(srcfile):
929                                 node = cwd.find_resource(srcfile)
930                                 if not node:
931                                         raise Utils.WafError("Unable to install the file %r (not found in %s)" % (srcfile, cwd))
932                                 src = node.abspath(env)
933
934                 return self.do_install(src, destpath, chmod)
935
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') """
938
939                 if sys.platform == 'win32':
940                         # well, this *cannot* work
941                         return
942
943                 if not path:
944                         raise Utils.WafError("where do you want to install %r? (%r?)" % (src, path))
945
946                 tgt = self.get_install_path(path, env)
947
948                 dir, name = os.path.split(tgt)
949                 Utils.check_dir(dir)
950
951                 if self.is_install > 0:
952                         link = False
953                         if not os.path.islink(tgt):
954                                 link = True
955                         elif os.readlink(tgt) != src:
956                                 link = True
957
958                         if link:
959                                 try: os.remove(tgt)
960                                 except OSError: pass
961
962                                 info('* symlink %s (-> %s)' % (tgt, src))
963                                 os.symlink(src, tgt)
964                         return 0
965
966                 else: # UNINSTALL
967                         try:
968                                 info('* removing %s' % (tgt))
969                                 os.remove(tgt)
970                                 return 0
971                         except OSError:
972                                 return 1
973
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)
977                 if self.log:
978                         self.log.write('%s\n' % cmd)
979                         kw['log'] = self.log
980                 try:
981                         if not kw.get('cwd', None):
982                                 kw['cwd'] = self.cwd
983                 except AttributeError:
984                         self.cwd = kw['cwd'] = self.bldnode.abspath()
985                 return Utils.exec_command(cmd, **kw)
986
987         def printout(self, s):
988                 f = self.log or sys.stderr
989                 f.write(s)
990                 f.flush()
991
992         def add_subdirs(self, dirs):
993                 self.recurse(dirs, 'build')
994
995         def pre_recurse(self, name_or_mod, path, nexdir):
996                 if not hasattr(self, 'oldpath'):
997                         self.oldpath = []
998                 self.oldpath.append(self.path)
999                 self.path = self.root.find_dir(nexdir)
1000                 return {'bld': self, 'ctx': self}
1001
1002         def post_recurse(self, name_or_mod, path, nexdir):
1003                 self.path = self.oldpath.pop()
1004
1005         ###### user-defined behaviour
1006
1007         def pre_build(self):
1008                 if hasattr(self, 'pre_funs'):
1009                         for m in self.pre_funs:
1010                                 m(self)
1011
1012         def post_build(self):
1013                 if hasattr(self, 'post_funs'):
1014                         for m in self.post_funs:
1015                                 m(self)
1016
1017         def add_pre_fun(self, meth):
1018                 try: self.pre_funs.append(meth)
1019                 except AttributeError: self.pre_funs = [meth]
1020
1021         def add_post_fun(self, meth):
1022                 try: self.post_funs.append(meth)
1023                 except AttributeError: self.post_funs = [meth]
1024
1025         def use_the_magic(self):
1026                 Task.algotype = Task.MAXPARALLEL
1027                 Task.file_deps = Task.extract_deps
1028                 self.magic = True
1029
1030         install_as = group_method(install_as)
1031         install_files = group_method(install_files)
1032         symlink_as = group_method(symlink_as)