3 # Thomas Nagy, 2005 (ita)
6 Node: filesystem structure, contains lists of nodes
9 1. Each file/folder is represented by exactly one node.
11 2. Most would-be class properties are stored in Build: nodes to depend on, signature, flags, ..
12 unused class members increase the .wafpickle file size sensibly with lots of objects.
14 3. The build is launched from the top of the build dir (for example, in _build_/).
16 4. Node should not be instantiated directly.
17 Each instance of Build.BuildContext has a Node subclass.
18 (aka: 'Nodu', see BuildContext initializer)
19 The BuildContext is referenced here as self.__class__.bld
20 Its Node class is referenced here as self.__class__
22 The public and advertised apis are the following:
23 ${TGT} -> dir/to/file.ext
24 ${TGT[0].base()} -> dir/to/file
25 ${TGT[0].dir(env)} -> dir/to
26 ${TGT[0].file()} -> file.ext
27 ${TGT[0].file_base()} -> file
28 ${TGT[0].suffix()} -> .ext
29 ${TGT[0].abspath(env)} -> /path/to/dir/to/file.ext
33 import os
, sys
, fnmatch
, re
, stat
34 import Utils
, Constants
41 type_to_string
= {UNDEFINED
: "unk", DIR
: "dir", FILE
: "src", BUILD
: "bld"}
43 # These fnmatch expressions are used by default to prune the directory tree
44 # while doing the recursive traversal in the find_iter method of the Node class.
45 prune_pats
= '.git .bzr .hg .svn _MTN _darcs CVS SCCS'.split()
47 # These fnmatch expressions are used by default to exclude files and dirs
48 # while doing the recursive traversal in the find_iter method of the Node class.
49 exclude_pats
= prune_pats
+ '*~ #*# .#* %*% ._* .gitignore .cvsignore vssver.scc .DS_Store'.split()
51 # These Utils.jar_regexp expressions are used by default to exclude files and dirs and also prune the directory tree
52 # while doing the recursive traversal in the ant_glob method of the Node class.
81 __slots__
= ("name", "parent", "id", "childs")
82 def __init__(self
, name
, parent
, node_type
= UNDEFINED
):
86 # assumption: one build object at a time
87 self
.__class
__.bld
.id_nodes
+= 4
88 self
.id = self
.__class
__.bld
.id_nodes
+ node_type
90 if node_type
== DIR
: self
.childs
= {}
92 # We do not want to add another type attribute (memory)
93 # use the id to find out: type = id & 3
94 # for setting: new type = type + x - type & 3
96 if parent
and name
in parent
.childs
:
97 raise Utils
.WafError('node %s exists in the parent files %r already' % (name
, parent
))
99 if parent
: parent
.childs
[name
] = self
101 def __setstate__(self
, data
):
103 (self
.parent
, self
.name
, self
.id, self
.childs
) = data
105 (self
.parent
, self
.name
, self
.id) = data
107 def __getstate__(self
):
108 if getattr(self
, 'childs', None) is None:
109 return (self
.parent
, self
.name
, self
.id)
111 return (self
.parent
, self
.name
, self
.id, self
.childs
)
114 if not self
.parent
: return ''
115 return "%s://%s" % (type_to_string
[self
.id & 3], self
.abspath())
118 return self
.__str
__()
121 "expensive, make certain it is not used"
122 raise Utils
.WafError('nodes, you are doing it wrong')
125 "nodes are not supposed to be copied"
126 raise Utils
.WafError('nodes are not supposed to be cloned')
131 def set_type(self
, t
):
132 "dangerous, you are not supposed to use this"
133 self
.id = self
.id + t
- self
.id & 3
136 return [x
for x
in self
.childs
.values() if x
.id & 3 == DIR
]
139 return [x
for x
in self
.childs
.values() if x
.id & 3 == FILE
]
141 def get_dir(self
, name
, default
=None):
142 node
= self
.childs
.get(name
, None)
143 if not node
or node
.id & 3 != DIR
: return default
146 def get_file(self
, name
, default
=None):
147 node
= self
.childs
.get(name
, None)
148 if not node
or node
.id & 3 != FILE
: return default
151 def get_build(self
, name
, default
=None):
152 node
= self
.childs
.get(name
, None)
153 if not node
or node
.id & 3 != BUILD
: return default
156 def find_resource(self
, lst
):
157 "Find an existing input file: either a build node declared previously or a source node"
158 if isinstance(lst
, str):
159 lst
= Utils
.split_path(lst
)
164 parent
= self
.find_dir(lst
[:-1])
165 if not parent
: return None
166 self
.__class
__.bld
.rescan(parent
)
169 node
= parent
.childs
.get(name
, None)
172 if tp
== FILE
or tp
== BUILD
:
177 tree
= self
.__class
__.bld
178 if not name
in tree
.cache_dir_contents
[parent
.id]:
181 path
= parent
.abspath() + os
.sep
+ name
183 st
= Utils
.h_file(path
)
187 child
= self
.__class
__(name
, parent
, FILE
)
188 tree
.node_sigs
[0][child
.id] = st
191 def find_or_declare(self
, lst
):
192 "Used for declaring a build node representing a file being built"
193 if isinstance(lst
, str):
194 lst
= Utils
.split_path(lst
)
199 parent
= self
.find_dir(lst
[:-1])
200 if not parent
: return None
201 self
.__class
__.bld
.rescan(parent
)
204 node
= parent
.childs
.get(name
, None)
208 raise Utils
.WafError('find_or_declare found a source file where a build file was expected %r' % '/'.join(lst
))
210 node
= self
.__class
__(name
, parent
, BUILD
)
213 def find_dir(self
, lst
):
214 "search a folder in the filesystem"
216 if isinstance(lst
, str):
217 lst
= Utils
.split_path(lst
)
221 self
.__class
__.bld
.rescan(current
)
224 if not current
.parent
and name
== current
.name
:
231 current
= current
.parent
or current
233 current
= prev
.childs
.get(name
, None)
235 dir_cont
= self
.__class
__.bld
.cache_dir_contents
236 if prev
.id in dir_cont
and name
in dir_cont
[prev
.id]:
239 # cygwin //machine/share
240 dirname
= os
.sep
+ name
246 dirname
= prev
.abspath() + os
.sep
+ name
247 if not os
.path
.isdir(dirname
):
249 current
= self
.__class
__(name
, prev
, DIR
)
250 elif (not prev
.name
and len(name
) == 2 and name
[1] == ':') or name
.startswith('\\\\'):
251 # drive letter or \\ path for windows
252 current
= self
.__class
__(name
, prev
, DIR
)
256 if current
.id & 3 != DIR
:
260 def ensure_dir_node_from_path(self
, lst
):
261 "used very rarely, force the construction of a branch of node instance for representing folders"
263 if isinstance(lst
, str):
264 lst
= Utils
.split_path(lst
)
273 current
= current
.parent
or current
276 current
= prev
.childs
.get(name
, None)
278 current
= self
.__class
__(name
, prev
, DIR
)
281 def exclusive_build_node(self
, path
):
283 create a hierarchy in the build dir (no source folders) for ill-behaving compilers
284 the node is not hashed, so you must do it manually
286 after declaring such a node, find_dir and find_resource should work as expected
288 lst
= Utils
.split_path(path
)
293 parent
= self
.find_dir(lst
[:-1])
297 parent
= self
.ensure_dir_node_from_path(lst
[:-1])
298 self
.__class
__.bld
.rescan(parent
)
301 self
.__class
__.bld
.rescan(parent
)
307 node
= parent
.childs
.get(name
, None)
309 node
= self
.__class
__(name
, parent
, BUILD
)
313 def path_to_parent(self
, parent
):
314 "path relative to a direct ancestor, as string"
325 ret
= os
.path
.join(*lst
)
330 def find_ancestor(self
, node
):
331 "find a common ancestor for two nodes - for the shortest path in hierarchy"
332 dist
= self
.height() - node
.height()
333 if dist
< 0: return node
.find_ancestor(self
)
339 if cand
== node
: return cand
343 cursor
= cursor
.parent
344 if cand
== cursor
: return cand
346 def relpath_gen(self
, from_node
):
347 "string representing a relative path between self to another node"
349 if self
== from_node
: return '.'
350 if from_node
.parent
== self
: return '..'
352 # up_path is '../../../' and down_path is 'dir/subdir/subdir/file'
353 ancestor
= self
.find_ancestor(from_node
)
356 while not cand
.id == ancestor
.id:
357 lst
.append(cand
.name
)
360 while not cand
.id == ancestor
.id:
364 return os
.sep
.join(lst
)
366 def nice_path(self
, env
=None):
367 "printed in the console, open files easily from the launch directory"
368 tree
= self
.__class
__.bld
369 ln
= tree
.launch_node()
371 if self
.id & 3 == FILE
: return self
.relpath_gen(ln
)
372 else: return os
.path
.join(tree
.bldnode
.relpath_gen(ln
), env
.variant(), self
.relpath_gen(tree
.srcnode
))
374 def is_child_of(self
, node
):
375 "does this node belong to the subtree node"
377 diff
= self
.height() - node
.height()
381 return p
.id == node
.id
383 def variant(self
, env
):
384 "variant, or output directory for this node, a source has for variant 0"
386 elif self
.id & 3 == FILE
: return 0
387 else: return env
.variant()
391 # README a cache can be added here if necessary
399 # helpers for building things
401 def abspath(self
, env
=None):
404 @param env [Environment]:
405 * obligatory for build nodes: build/variant/src/dir/bar.o
406 * optional for dirs: get either src/dir or build/variant/src/dir
407 * excluded for source nodes: src/dir/bar.c
409 Instead of computing the absolute path each time again,
410 store the already-computed absolute paths in one of (variants+1) dictionaries:
411 bld.cache_node_abspath[0] holds absolute paths for source nodes.
412 bld.cache_node_abspath[variant] holds the absolute path for the build nodes
413 which reside in the variant given by env.
415 ## absolute path - hot zone, so do not touch
418 variant
= (env
and (self
.id & 3 != FILE
) and env
.variant()) or 0
420 ret
= self
.__class
__.bld
.cache_node_abspath
[variant
].get(self
.id, None)
426 val
= os
.sep
== '/' and os
.sep
or ''
427 elif not self
.parent
.name
: # root
428 val
= (os
.sep
== '/' and os
.sep
or '') + self
.name
430 val
= self
.parent
.abspath() + os
.sep
+ self
.name
433 val
= os
.sep
.join((self
.__class
__.bld
.bldnode
.abspath(), variant
, self
.path_to_parent(self
.__class
__.bld
.srcnode
)))
434 self
.__class
__.bld
.cache_node_abspath
[variant
][self
.id] = val
437 def change_ext(self
, ext
):
438 "node of the same path, but with a different extension - hot zone so do not touch"
442 name
= name
[:k
] + ext
446 return self
.parent
.find_or_declare([name
])
448 def src_dir(self
, env
):
449 "src path without the file name"
450 return self
.parent
.srcpath(env
)
452 def bld_dir(self
, env
):
453 "build path without the file name"
454 return self
.parent
.bldpath(env
)
456 def bld_base(self
, env
):
457 "build path without the extension: src/dir/foo(.cpp)"
458 s
= os
.path
.splitext(self
.name
)[0]
459 return os
.path
.join(self
.bld_dir(env
), s
)
461 def bldpath(self
, env
=None):
462 "path seen from the build dir default/src/foo.cpp"
463 if self
.id & 3 == FILE
:
464 return self
.relpath_gen(self
.__class
__.bld
.bldnode
)
465 p
= self
.path_to_parent(self
.__class
__.bld
.srcnode
)
467 return env
.variant() + os
.sep
+ p
470 def srcpath(self
, env
=None):
471 "path in the srcdir from the build dir ../src/foo.cpp"
472 if self
.id & 3 == BUILD
:
473 return self
.bldpath(env
)
474 return self
.relpath_gen(self
.__class
__.bld
.bldnode
)
477 "get the contents of a file, it is not used anywhere for the moment"
478 return Utils
.readf(self
.abspath(env
))
482 return self
.parent
.abspath(env
)
490 return os
.path
.splitext(self
.name
)[0]
493 "scons-like - hot zone so do not touch"
494 k
= max(0, self
.name
.rfind('.'))
497 def find_iter_impl(self
, src
=True, bld
=True, dir=True, accept_name
=None, is_prune
=None, maxdepth
=25):
498 """find nodes in the filesystem hierarchy, try to instanciate the nodes passively; same gotcha as ant_glob"""
499 bld_ctx
= self
.__class
__.bld
501 for name
in bld_ctx
.cache_dir_contents
[self
.id]:
502 if accept_name(self
, name
):
503 node
= self
.find_resource(name
)
505 if src
and node
.id & 3 == FILE
:
508 node
= self
.find_dir(name
)
509 if node
and node
.id != bld_ctx
.bldnode
.id:
512 if not is_prune(self
, name
):
514 for k
in node
.find_iter_impl(src
, bld
, dir, accept_name
, is_prune
, maxdepth
=maxdepth
- 1):
517 if not is_prune(self
, name
):
518 node
= self
.find_resource(name
)
520 # not a file, it is a dir
521 node
= self
.find_dir(name
)
522 if node
and node
.id != bld_ctx
.bldnode
.id:
524 for k
in node
.find_iter_impl(src
, bld
, dir, accept_name
, is_prune
, maxdepth
=maxdepth
- 1):
528 for node
in self
.childs
.values():
529 if node
.id == bld_ctx
.bldnode
.id:
531 if node
.id & 3 == BUILD
:
532 if accept_name(self
, node
.name
):
536 def find_iter(self
, in_pat
=['*'], ex_pat
=exclude_pats
, prune_pat
=prune_pats
, src
=True, bld
=True, dir=False, maxdepth
=25, flat
=False):
537 """find nodes recursively, this returns everything but folders by default; same gotcha as ant_glob"""
539 if not (src
or bld
or dir):
542 if self
.id & 3 != DIR
:
545 in_pat
= Utils
.to_list(in_pat
)
546 ex_pat
= Utils
.to_list(ex_pat
)
547 prune_pat
= Utils
.to_list(prune_pat
)
549 def accept_name(node
, name
):
551 if fnmatch
.fnmatchcase(name
, pat
):
554 if fnmatch
.fnmatchcase(name
, pat
):
558 def is_prune(node
, name
):
559 for pat
in prune_pat
:
560 if fnmatch
.fnmatchcase(name
, pat
):
564 ret
= self
.find_iter_impl(src
, bld
, dir, accept_name
, is_prune
, maxdepth
=maxdepth
)
566 return " ".join([x
.relpath_gen(self
) for x
in ret
])
570 def ant_glob(self
, *k
, **kw
):
572 known gotcha: will enumerate the files, but only if the folder exists in the source directory
578 excl
= kw
.get('excl', exclude_regs
)
579 incl
= k
and k
[0] or kw
.get('incl', '**')
582 lst
= Utils
.to_list(s
)
585 x
= x
.replace('//', '/')
594 k
= k
.replace('.', '[.]').replace('*', '.*').replace('?', '.')
597 accu
.append(re
.compile(k
))
601 def filtre(name
, nn
):
609 if lst
[1].match(name
):
613 elif lst
[0].match(name
):
617 def accept(name
, pats
):
618 nacc
= filtre(name
, pats
[0])
619 nrej
= filtre(name
, pats
[1])
624 def ant_iter(nodi
, maxdepth
=25, pats
=[]):
625 nodi
.__class
__.bld
.rescan(nodi
)
626 tmp
= list(nodi
.__class
__.bld
.cache_dir_contents
[nodi
.id])
629 npats
= accept(name
, pats
)
630 if npats
and npats
[0]:
631 accepted
= [] in npats
[0]
632 #print accepted, nodi, name
634 node
= nodi
.find_resource(name
)
635 if node
and accepted
:
636 if src
and node
.id & 3 == FILE
:
639 node
= nodi
.find_dir(name
)
640 if node
and node
.id != nodi
.__class
__.bld
.bldnode
.id:
644 for k
in ant_iter(node
, maxdepth
=maxdepth
- 1, pats
=npats
):
647 for node
in nodi
.childs
.values():
648 if node
.id == nodi
.__class
__.bld
.bldnode
.id:
650 if node
.id & 3 == BUILD
:
651 npats
= accept(node
.name
, pats
)
652 if npats
and npats
[0] and [] in npats
[0]:
656 ret
= [x
for x
in ant_iter(self
, pats
=[to_pat(incl
), to_pat(excl
)])]
658 if kw
.get('flat', True):
659 return " ".join([x
.relpath_gen(self
) for x
in ret
])
663 def update_build_dir(self
, env
=None):
666 for env
in bld
.all_envs
:
667 self
.update_build_dir(env
)
670 path
= self
.abspath(env
)
672 lst
= Utils
.listdir(path
)
674 self
.__class
__.bld
.cache_dir_contents
[self
.id].update(lst
)
676 self
.__class
__.bld
.cache_dir_contents
[self
.id] = set(lst
)
677 self
.__class
__.bld
.cache_scanned_folders
[self
.id] = True
680 npath
= path
+ os
.sep
+ k
682 if stat
.S_ISREG(st
[stat
.ST_MODE
]):
683 ick
= self
.find_or_declare(k
)
684 if not (ick
.id in self
.__class
__.bld
.node_sigs
[env
.variant()]):
685 self
.__class
__.bld
.node_sigs
[env
.variant()][ick
.id] = Constants
.SIG_NIL
686 elif stat
.S_ISDIR(st
[stat
.ST_MODE
]):
687 child
= self
.find_dir(k
)
689 child
= self
.ensure_dir_node_from_path(k
)
690 child
.update_build_dir(env
)