1 """scons.Node.FS
2
3 File system nodes.
4
5 These Nodes represent the canonical external objects that people think
6 of when they think of building software: files and directories.
7
8 This holds a "default_fs" variable that should be initialized with an FS
9 that can be used by scripts or modules looking for the canonical default.
10
11 """
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35 __revision__ = "src/engine/SCons/Node/FS.py 2014/03/02 14:18:15 garyo"
36
37 import fnmatch
38 import os
39 import re
40 import shutil
41 import stat
42 import sys
43 import time
44 import codecs
45
46 import SCons.Action
47 import SCons.Debug
48 from SCons.Debug import logInstanceCreation
49 import SCons.Errors
50 import SCons.Memoize
51 import SCons.Node
52 import SCons.Node.Alias
53 import SCons.Subst
54 import SCons.Util
55 import SCons.Warnings
56
57 from SCons.Debug import Trace
58
59 do_store_info = True
60 print_duplicate = 0
61
62
63 -class EntryProxyAttributeError(AttributeError):
64 """
65 An AttributeError subclass for recording and displaying the name
66 of the underlying Entry involved in an AttributeError exception.
67 """
68 - def __init__(self, entry_proxy, attribute):
69 AttributeError.__init__(self)
70 self.entry_proxy = entry_proxy
71 self.attribute = attribute
73 entry = self.entry_proxy.get()
74 fmt = "%s instance %s has no attribute %s"
75 return fmt % (entry.__class__.__name__,
76 repr(entry.name),
77 repr(self.attribute))
78
79
80
81 default_max_drift = 2*24*60*60
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101 Save_Strings = None
102
106
107
108
109
110
111
112
113
114 do_splitdrive = None
115 _my_splitdrive =None
116
135 else:
136 def splitdrive(p):
137 if p[1:2] == ':':
138 return p[:2], p[2:]
139 return '', p
140 _my_splitdrive = splitdrive
141
142
143
144 global OS_SEP
145 global UNC_PREFIX
146 global os_sep_is_slash
147
148 OS_SEP = os.sep
149 UNC_PREFIX = OS_SEP + OS_SEP
150 os_sep_is_slash = OS_SEP == '/'
151
152 initialize_do_splitdrive()
153
154
155 needs_normpath_check = re.compile(
156 r'''
157 # We need to renormalize the path if it contains any consecutive
158 # '/' characters.
159 .*// |
160
161 # We need to renormalize the path if it contains a '..' directory.
162 # Note that we check for all the following cases:
163 #
164 # a) The path is a single '..'
165 # b) The path starts with '..'. E.g. '../' or '../moredirs'
166 # but we not match '..abc/'.
167 # c) The path ends with '..'. E.g. '/..' or 'dirs/..'
168 # d) The path contains a '..' in the middle.
169 # E.g. dirs/../moredirs
170
171 (.*/)?\.\.(?:/|$) |
172
173 # We need to renormalize the path if it contains a '.'
174 # directory, but NOT if it is a single '.' '/' characters. We
175 # do not want to match a single '.' because this case is checked
176 # for explicitely since this is common enough case.
177 #
178 # Note that we check for all the following cases:
179 #
180 # a) We don't match a single '.'
181 # b) We match if the path starts with '.'. E.g. './' or
182 # './moredirs' but we not match '.abc/'.
183 # c) We match if the path ends with '.'. E.g. '/.' or
184 # 'dirs/.'
185 # d) We match if the path contains a '.' in the middle.
186 # E.g. dirs/./moredirs
187
188 \./|.*/\.(?:/|$)
189
190 ''',
191 re.VERBOSE
192 )
193 needs_normpath_match = needs_normpath_check.match
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209 if hasattr(os, 'link'):
222 else:
223 _hardlink_func = None
224
225 if hasattr(os, 'symlink'):
228 else:
229 _softlink_func = None
230
235
236
237 Valid_Duplicates = ['hard-soft-copy', 'soft-hard-copy',
238 'hard-copy', 'soft-copy', 'copy']
239
240 Link_Funcs = []
241
264
266
267
268
269
270
271 src = source[0].abspath
272 dest = target[0].abspath
273 dir, file = os.path.split(dest)
274 if dir and not target[0].fs.isdir(dir):
275 os.makedirs(dir)
276 if not Link_Funcs:
277
278 set_duplicate('hard-soft-copy')
279 fs = source[0].fs
280
281 for func in Link_Funcs:
282 try:
283 func(fs, src, dest)
284 break
285 except (IOError, OSError):
286
287
288
289
290
291
292 if func == Link_Funcs[-1]:
293
294 raise
295 return 0
296
297 Link = SCons.Action.Action(LinkFunc, None)
299 return 'Local copy of %s from %s' % (target[0], source[0])
300
301 LocalCopy = SCons.Action.Action(LinkFunc, LocalString)
302
304 t = target[0]
305 t.fs.unlink(t.abspath)
306 return 0
307
308 Unlink = SCons.Action.Action(UnlinkFunc, None)
309
311 t = target[0]
312 if not t.exists():
313 t.fs.mkdir(t.abspath)
314 return 0
315
316 Mkdir = SCons.Action.Action(MkdirFunc, None, presub=None)
317
318 MkdirBuilder = None
319
334
337
338 _null = _Null()
339
340 DefaultSCCSBuilder = None
341 DefaultRCSBuilder = None
342
354
366
367
368 _is_cygwin = sys.platform == "cygwin"
369 if os.path.normcase("TeSt") == os.path.normpath("TeSt") and not _is_cygwin:
372 else:
375
376
377
380 self.type = type
381 self.do = do
382 self.ignore = ignore
383 self.func = do
385 return self.func(*args, **kw)
386 - def set(self, list):
387 if self.type in list:
388 self.func = self.do
389 else:
390 self.func = self.ignore
391
393 result = predicate()
394 try:
395
396
397
398
399
400
401 if node._memo['stat'] is None:
402 del node._memo['stat']
403 except (AttributeError, KeyError):
404 pass
405 if result:
406 raise TypeError(errorfmt % node.abspath)
407
410
412 try:
413 rcs_dir = node.rcs_dir
414 except AttributeError:
415 if node.entry_exists_on_disk('RCS'):
416 rcs_dir = node.Dir('RCS')
417 else:
418 rcs_dir = None
419 node.rcs_dir = rcs_dir
420 if rcs_dir:
421 return rcs_dir.entry_exists_on_disk(name+',v')
422 return None
423
426
428 try:
429 sccs_dir = node.sccs_dir
430 except AttributeError:
431 if node.entry_exists_on_disk('SCCS'):
432 sccs_dir = node.Dir('SCCS')
433 else:
434 sccs_dir = None
435 node.sccs_dir = sccs_dir
436 if sccs_dir:
437 return sccs_dir.entry_exists_on_disk('s.'+name)
438 return None
439
442
443 diskcheck_match = DiskChecker('match', do_diskcheck_match, ignore_diskcheck_match)
444 diskcheck_rcs = DiskChecker('rcs', do_diskcheck_rcs, ignore_diskcheck_rcs)
445 diskcheck_sccs = DiskChecker('sccs', do_diskcheck_sccs, ignore_diskcheck_sccs)
446
447 diskcheckers = [
448 diskcheck_match,
449 diskcheck_rcs,
450 diskcheck_sccs,
451 ]
452
456
459
460
461
462 -class EntryProxy(SCons.Util.Proxy):
463
464 __str__ = SCons.Util.Delegate('__str__')
465
466 - def __get_abspath(self):
467 entry = self.get()
468 return SCons.Subst.SpecialAttrWrapper(entry.get_abspath(),
469 entry.name + "_abspath")
470
471 - def __get_filebase(self):
472 name = self.get().name
473 return SCons.Subst.SpecialAttrWrapper(SCons.Util.splitext(name)[0],
474 name + "_filebase")
475
476 - def __get_suffix(self):
477 name = self.get().name
478 return SCons.Subst.SpecialAttrWrapper(SCons.Util.splitext(name)[1],
479 name + "_suffix")
480
481 - def __get_file(self):
482 name = self.get().name
483 return SCons.Subst.SpecialAttrWrapper(name, name + "_file")
484
485 - def __get_base_path(self):
486 """Return the file's directory and file name, with the
487 suffix stripped."""
488 entry = self.get()
489 return SCons.Subst.SpecialAttrWrapper(SCons.Util.splitext(entry.get_path())[0],
490 entry.name + "_base")
491
493 """Return the path with / as the path separator,
494 regardless of platform."""
495 if os_sep_is_slash:
496 return self
497 else:
498 entry = self.get()
499 r = entry.get_path().replace(OS_SEP, '/')
500 return SCons.Subst.SpecialAttrWrapper(r, entry.name + "_posix")
501
503 """Return the path with \ as the path separator,
504 regardless of platform."""
505 if OS_SEP == '\\':
506 return self
507 else:
508 entry = self.get()
509 r = entry.get_path().replace(OS_SEP, '\\')
510 return SCons.Subst.SpecialAttrWrapper(r, entry.name + "_windows")
511
512 - def __get_srcnode(self):
513 return EntryProxy(self.get().srcnode())
514
515 - def __get_srcdir(self):
516 """Returns the directory containing the source node linked to this
517 node via VariantDir(), or the directory of this node if not linked."""
518 return EntryProxy(self.get().srcnode().dir)
519
520 - def __get_rsrcnode(self):
521 return EntryProxy(self.get().srcnode().rfile())
522
523 - def __get_rsrcdir(self):
524 """Returns the directory containing the source node linked to this
525 node via VariantDir(), or the directory of this node if not linked."""
526 return EntryProxy(self.get().srcnode().rfile().dir)
527
528 - def __get_dir(self):
529 return EntryProxy(self.get().dir)
530
531 dictSpecialAttrs = { "base" : __get_base_path,
532 "posix" : __get_posix_path,
533 "windows" : __get_windows_path,
534 "win32" : __get_windows_path,
535 "srcpath" : __get_srcnode,
536 "srcdir" : __get_srcdir,
537 "dir" : __get_dir,
538 "abspath" : __get_abspath,
539 "filebase" : __get_filebase,
540 "suffix" : __get_suffix,
541 "file" : __get_file,
542 "rsrcpath" : __get_rsrcnode,
543 "rsrcdir" : __get_rsrcdir,
544 }
545
546 - def __getattr__(self, name):
547
548
549 try:
550 attr_function = self.dictSpecialAttrs[name]
551 except KeyError:
552 try:
553 attr = SCons.Util.Proxy.__getattr__(self, name)
554 except AttributeError, e:
555
556
557
558 raise EntryProxyAttributeError(self, name)
559 return attr
560 else:
561 return attr_function(self)
562
563 -class Base(SCons.Node.Node):
564 """A generic class for file system entries. This class is for
565 when we don't know yet whether the entry being looked up is a file
566 or a directory. Instances of this class can morph into either
567 Dir or File objects by a later, more precise lookup.
568
569 Note: this class does not define __cmp__ and __hash__ for
570 efficiency reasons. SCons does a lot of comparing of
571 Node.FS.{Base,Entry,File,Dir} objects, so those operations must be
572 as fast as possible, which means we want to use Python's built-in
573 object identity comparisons.
574 """
575
576 memoizer_counters = []
577
578 - def __init__(self, name, directory, fs):
579 """Initialize a generic Node.FS.Base object.
580
581 Call the superclass initialization, take care of setting up
582 our relative and absolute paths, identify our parent
583 directory, and indicate that this node should use
584 signatures."""
585 if SCons.Debug.track_instances: logInstanceCreation(self, 'Node.FS.Base')
586 SCons.Node.Node.__init__(self)
587
588
589
590
591
592
593 self.name = SCons.Util.silent_intern(name)
594
595 self.suffix = SCons.Util.silent_intern(SCons.Util.splitext(name)[1])
596 self.fs = fs
597
598 assert directory, "A directory must be provided"
599
600 self.abspath = SCons.Util.silent_intern(directory.entry_abspath(name))
601 self.labspath = SCons.Util.silent_intern(directory.entry_labspath(name))
602 if directory.path == '.':
603 self.path = SCons.Util.silent_intern(name)
604 else:
605 self.path = SCons.Util.silent_intern(directory.entry_path(name))
606 if directory.tpath == '.':
607 self.tpath = SCons.Util.silent_intern(name)
608 else:
609 self.tpath = SCons.Util.silent_intern(directory.entry_tpath(name))
610 self.path_elements = directory.path_elements + [self]
611
612 self.dir = directory
613 self.cwd = None
614 self.duplicate = directory.duplicate
615
617 return '"' + self.__str__() + '"'
618
620 """
621 This node, which already existed, is being looked up as the
622 specified klass. Raise an exception if it isn't.
623 """
624 if isinstance(self, klass) or klass is Entry:
625 return
626 raise TypeError("Tried to lookup %s '%s' as a %s." %\
627 (self.__class__.__name__, self.path, klass.__name__))
628
631
634
637
645
646 memoizer_counters.append(SCons.Memoize.CountValue('_save_str'))
647
649 try:
650 return self._memo['_save_str']
651 except KeyError:
652 pass
653 result = sys.intern(self._get_str())
654 self._memo['_save_str'] = result
655 return result
656
681
682 rstr = __str__
683
684 memoizer_counters.append(SCons.Memoize.CountValue('stat'))
685
687 try: return self._memo['stat']
688 except KeyError: pass
689 try: result = self.fs.stat(self.abspath)
690 except os.error: result = None
691 self._memo['stat'] = result
692 return result
693
695 return self.stat() is not None
696
699
701 st = self.stat()
702 if st: return st[stat.ST_MTIME]
703 else: return None
704
706 st = self.stat()
707 if st: return st[stat.ST_SIZE]
708 else: return None
709
711 st = self.stat()
712 return st is not None and stat.S_ISDIR(st[stat.ST_MODE])
713
715 st = self.stat()
716 return st is not None and stat.S_ISREG(st[stat.ST_MODE])
717
718 if hasattr(os, 'symlink'):
720 try: st = self.fs.lstat(self.abspath)
721 except os.error: return 0
722 return stat.S_ISLNK(st[stat.ST_MODE])
723 else:
726
728 if self is dir:
729 return 1
730 else:
731 return self.dir.is_under(dir)
732
735
747
749 """Return path relative to the current working directory of the
750 Node.FS.Base object that owns us."""
751 if not dir:
752 dir = self.fs.getcwd()
753 if self == dir:
754 return '.'
755 path_elems = self.path_elements
756 pathname = ''
757 try: i = path_elems.index(dir)
758 except ValueError:
759 for p in path_elems[:-1]:
760 pathname += p.dirname
761 else:
762 for p in path_elems[i+1:-1]:
763 pathname += p.dirname
764 return pathname + path_elems[-1].name
765
767 """Set the source code builder for this node."""
768 self.sbuilder = builder
769 if not self.has_builder():
770 self.builder_set(builder)
771
773 """Fetch the source code builder for this node.
774
775 If there isn't one, we cache the source code builder specified
776 for the directory (which in turn will cache the value from its
777 parent directory, and so on up to the file system root).
778 """
779 try:
780 scb = self.sbuilder
781 except AttributeError:
782 scb = self.dir.src_builder()
783 self.sbuilder = scb
784 return scb
785
787 """Get the absolute path of the file."""
788 return self.abspath
789
791
792
793
794 return self.name
795
797 try:
798 return self._proxy
799 except AttributeError:
800 ret = EntryProxy(self)
801 self._proxy = ret
802 return ret
803
805 """
806
807 Generates a target entry that corresponds to this entry (usually
808 a source file) with the specified prefix and suffix.
809
810 Note that this method can be overridden dynamically for generated
811 files that need different behavior. See Tool/swig.py for
812 an example.
813 """
814 return self.dir.Entry(prefix + splitext(self.name)[0] + suffix)
815
818
819 memoizer_counters.append(SCons.Memoize.CountDict('Rfindalldirs', _Rfindalldirs_key))
820
822 """
823 Return all of the directories for a given path list, including
824 corresponding "backing" directories in any repositories.
825
826 The Node lookups are relative to this Node (typically a
827 directory), so memoizing result saves cycles from looking
828 up the same path for each target in a given directory.
829 """
830 try:
831 memo_dict = self._memo['Rfindalldirs']
832 except KeyError:
833 memo_dict = {}
834 self._memo['Rfindalldirs'] = memo_dict
835 else:
836 try:
837 return memo_dict[pathlist]
838 except KeyError:
839 pass
840
841 create_dir_relative_to_self = self.Dir
842 result = []
843 for path in pathlist:
844 if isinstance(path, SCons.Node.Node):
845 result.append(path)
846 else:
847 dir = create_dir_relative_to_self(path)
848 result.extend(dir.get_all_rdirs())
849
850 memo_dict[pathlist] = result
851
852 return result
853
854 - def RDirs(self, pathlist):
855 """Search for a list of directories in the Repository list."""
856 cwd = self.cwd or self.fs._cwd
857 return cwd.Rfindalldirs(pathlist)
858
859 memoizer_counters.append(SCons.Memoize.CountValue('rentry'))
860
862 try:
863 return self._memo['rentry']
864 except KeyError:
865 pass
866 result = self
867 if not self.exists():
868 norm_name = _my_normcase(self.name)
869 for dir in self.dir.get_all_rdirs():
870 try:
871 node = dir.entries[norm_name]
872 except KeyError:
873 if dir.entry_exists_on_disk(self.name):
874 result = dir.Entry(self.name)
875 break
876 self._memo['rentry'] = result
877 return result
878
879 - def _glob1(self, pattern, ondisk=True, source=False, strings=False):
881
883 """This is the class for generic Node.FS entries--that is, things
884 that could be a File or a Dir, but we're just not sure yet.
885 Consequently, the methods in this class really exist just to
886 transform their associated object into the right class when the
887 time comes, and then call the same-named method in the transformed
888 class."""
889
890 - def diskcheck_match(self):
892
893 - def disambiguate(self, must_exist=None):
894 """
895 """
896 if self.isdir():
897 self.__class__ = Dir
898 self._morph()
899 elif self.isfile():
900 self.__class__ = File
901 self._morph()
902 self.clear()
903 else:
904
905
906
907
908
909
910
911
912
913 srcdir = self.dir.srcnode()
914 if srcdir != self.dir and \
915 srcdir.entry_exists_on_disk(self.name) and \
916 self.srcnode().isdir():
917 self.__class__ = Dir
918 self._morph()
919 elif must_exist:
920 msg = "No such file or directory: '%s'" % self.abspath
921 raise SCons.Errors.UserError(msg)
922 else:
923 self.__class__ = File
924 self._morph()
925 self.clear()
926 return self
927
929 """We're a generic Entry, but the caller is actually looking for
930 a File at this point, so morph into one."""
931 self.__class__ = File
932 self._morph()
933 self.clear()
934 return File.rfile(self)
935
936 - def scanner_key(self):
937 return self.get_suffix()
938
939 - def get_contents(self):
940 """Fetch the contents of the entry. Returns the exact binary
941 contents of the file."""
942 try:
943 self = self.disambiguate(must_exist=1)
944 except SCons.Errors.UserError:
945
946
947
948
949
950 return ''
951 else:
952 return self.get_contents()
953
955 """Fetch the decoded text contents of a Unicode encoded Entry.
956
957 Since this should return the text contents from the file
958 system, we check to see into what sort of subclass we should
959 morph this Entry."""
960 try:
961 self = self.disambiguate(must_exist=1)
962 except SCons.Errors.UserError:
963
964
965
966
967
968 return ''
969 else:
970 return self.get_text_contents()
971
972 - def must_be_same(self, klass):
973 """Called to make sure a Node is a Dir. Since we're an
974 Entry, we can morph into one."""
975 if self.__class__ is not klass:
976 self.__class__ = klass
977 self._morph()
978 self.clear()
979
980
981
982
983
984
985
986
987
988
989
990
992 """Return if the Entry exists. Check the file system to see
993 what we should turn into first. Assume a file if there's no
994 directory."""
995 return self.disambiguate().exists()
996
997 - def rel_path(self, other):
998 d = self.disambiguate()
999 if d.__class__ is Entry:
1000 raise Exception("rel_path() could not disambiguate File/Dir")
1001 return d.rel_path(other)
1002
1003 - def new_ninfo(self):
1004 return self.disambiguate().new_ninfo()
1005
1006 - def changed_since_last_build(self, target, prev_ni):
1007 return self.disambiguate().changed_since_last_build(target, prev_ni)
1008
1009 - def _glob1(self, pattern, ondisk=True, source=False, strings=False):
1010 return self.disambiguate()._glob1(pattern, ondisk, source, strings)
1011
1012 - def get_subst_proxy(self):
1014
1015
1016
1017 _classEntry = Entry
1018
1019
1021
1022 if SCons.Memoize.use_memoizer:
1023 __metaclass__ = SCons.Memoize.Memoized_Metaclass
1024
1025
1026
1027
1028
1029
1030
1031
1032
1033
1034
1035
1036
1037
1038
1039
1040
1041 - def chmod(self, path, mode):
1043 - def copy(self, src, dst):
1044 return shutil.copy(src, dst)
1045 - def copy2(self, src, dst):
1046 return shutil.copy2(src, dst)
1057 - def link(self, src, dst):
1058 return os.link(src, dst)
1068 return os.rename(old, new)
1069 - def stat(self, path):
1073 - def open(self, path):
1077
1078 if hasattr(os, 'symlink'):
1081 else:
1084
1085 if hasattr(os, 'readlink'):
1088 else:
1091
1092
1093
1094
1095
1096
1097
1098
1099
1100
1101
1103
1104 memoizer_counters = []
1105
1107 """Initialize the Node.FS subsystem.
1108
1109 The supplied path is the top of the source tree, where we
1110 expect to find the top-level build file. If no path is
1111 supplied, the current directory is the default.
1112
1113 The path argument must be a valid absolute path.
1114 """
1115 if SCons.Debug.track_instances: logInstanceCreation(self, 'Node.FS')
1116
1117 self._memo = {}
1118
1119 self.Root = {}
1120 self.SConstruct_dir = None
1121 self.max_drift = default_max_drift
1122
1123 self.Top = None
1124 if path is None:
1125 self.pathTop = os.getcwd()
1126 else:
1127 self.pathTop = path
1128 self.defaultDrive = _my_normcase(_my_splitdrive(self.pathTop)[0])
1129
1130 self.Top = self.Dir(self.pathTop)
1131 self.Top.path = '.'
1132 self.Top.tpath = '.'
1133 self._cwd = self.Top
1134
1135 DirNodeInfo.fs = self
1136 FileNodeInfo.fs = self
1137
1139 self.SConstruct_dir = dir
1140
1142 return self.max_drift
1143
1145 self.max_drift = max_drift
1146
1148 if hasattr(self, "_cwd"):
1149 return self._cwd
1150 else:
1151 return "<no cwd>"
1152
1153 - def chdir(self, dir, change_os_dir=0):
1154 """Change the current working directory for lookups.
1155 If change_os_dir is true, we will also change the "real" cwd
1156 to match.
1157 """
1158 curr=self._cwd
1159 try:
1160 if dir is not None:
1161 self._cwd = dir
1162 if change_os_dir:
1163 os.chdir(dir.abspath)
1164 except OSError:
1165 self._cwd = curr
1166 raise
1167
1169 """
1170 Returns the root directory for the specified drive, creating
1171 it if necessary.
1172 """
1173 drive = _my_normcase(drive)
1174 try:
1175 return self.Root[drive]
1176 except KeyError:
1177 root = RootDir(drive, self)
1178 self.Root[drive] = root
1179 if not drive:
1180 self.Root[self.defaultDrive] = root
1181 elif drive == self.defaultDrive:
1182 self.Root[''] = root
1183 return root
1184
1185 - def _lookup(self, p, directory, fsclass, create=1):
1186 """
1187 The generic entry point for Node lookup with user-supplied data.
1188
1189 This translates arbitrary input into a canonical Node.FS object
1190 of the specified fsclass. The general approach for strings is
1191 to turn it into a fully normalized absolute path and then call
1192 the root directory's lookup_abs() method for the heavy lifting.
1193
1194 If the path name begins with '#', it is unconditionally
1195 interpreted relative to the top-level directory of this FS. '#'
1196 is treated as a synonym for the top-level SConstruct directory,
1197 much like '~' is treated as a synonym for the user's home
1198 directory in a UNIX shell. So both '#foo' and '#/foo' refer
1199 to the 'foo' subdirectory underneath the top-level SConstruct
1200 directory.
1201
1202 If the path name is relative, then the path is looked up relative
1203 to the specified directory, or the current directory (self._cwd,
1204 typically the SConscript directory) if the specified directory
1205 is None.
1206 """
1207 if isinstance(p, Base):
1208
1209
1210 p.must_be_same(fsclass)
1211 return p
1212
1213 p = str(p)
1214
1215 if not os_sep_is_slash:
1216 p = p.replace(OS_SEP, '/')
1217
1218 if p[0:1] == '#':
1219
1220
1221
1222 p = p[1:]
1223 directory = self.Top
1224
1225
1226
1227
1228
1229
1230
1231
1232
1233
1234
1235 if do_splitdrive:
1236 drive, p = _my_splitdrive(p)
1237 if drive:
1238 root = self.get_root(drive)
1239 else:
1240 root = directory.root
1241 else:
1242 root = directory.root
1243
1244
1245
1246 p = p.strip('/')
1247
1248 needs_normpath = needs_normpath_match(p)
1249
1250
1251 if p in ('', '.'):
1252 p = directory.labspath
1253 else:
1254 p = directory.labspath + '/' + p
1255 else:
1256 if do_splitdrive:
1257 drive, p = _my_splitdrive(p)
1258 if drive and not p:
1259
1260
1261
1262 p = '/'
1263 else:
1264 drive = ''
1265
1266
1267
1268 if p != '/':
1269 p = p.rstrip('/')
1270
1271 needs_normpath = needs_normpath_match(p)
1272
1273 if p[0:1] == '/':
1274
1275 root = self.get_root(drive)
1276 else:
1277
1278
1279
1280
1281 if directory:
1282 if not isinstance(directory, Dir):
1283 directory = self.Dir(directory)
1284 else:
1285 directory = self._cwd
1286
1287 if p in ('', '.'):
1288 p = directory.labspath
1289 else:
1290 p = directory.labspath + '/' + p
1291
1292 if drive:
1293 root = self.get_root(drive)
1294 else:
1295 root = directory.root
1296
1297 if needs_normpath is not None:
1298
1299
1300
1301
1302
1303
1304
1305
1306
1307 ins = p.split('/')[1:]
1308 outs = []
1309 for d in ins:
1310 if d == '..':
1311 try:
1312 outs.pop()
1313 except IndexError:
1314 pass
1315 elif d not in ('', '.'):
1316 outs.append(d)
1317 p = '/' + '/'.join(outs)
1318
1319 return root._lookup_abs(p, fsclass, create)
1320
1321 - def Entry(self, name, directory = None, create = 1):
1322 """Look up or create a generic Entry node with the specified name.
1323 If the name is a relative path (begins with ./, ../, or a file
1324 name), then it is looked up relative to the supplied directory
1325 node, or to the top level directory of the FS (supplied at
1326 construction time) if no directory is supplied.
1327 """
1328 return self._lookup(name, directory, Entry, create)
1329
1330 - def File(self, name, directory = None, create = 1):
1331 """Look up or create a File node with the specified name. If
1332 the name is a relative path (begins with ./, ../, or a file name),
1333 then it is looked up relative to the supplied directory node,
1334 or to the top level directory of the FS (supplied at construction
1335 time) if no directory is supplied.
1336
1337 This method will raise TypeError if a directory is found at the
1338 specified path.
1339 """
1340 return self._lookup(name, directory, File, create)
1341
1342 - def Dir(self, name, directory = None, create = True):
1343 """Look up or create a Dir node with the specified name. If
1344 the name is a relative path (begins with ./, ../, or a file name),
1345 then it is looked up relative to the supplied directory node,
1346 or to the top level directory of the FS (supplied at construction
1347 time) if no directory is supplied.
1348
1349 This method will raise TypeError if a normal file is found at the
1350 specified path.
1351 """
1352 return self._lookup(name, directory, Dir, create)
1353
1354 - def VariantDir(self, variant_dir, src_dir, duplicate=1):
1355 """Link the supplied variant directory to the source directory
1356 for purposes of building files."""
1357
1358 if not isinstance(src_dir, SCons.Node.Node):
1359 src_dir = self.Dir(src_dir)
1360 if not isinstance(variant_dir, SCons.Node.Node):
1361 variant_dir = self.Dir(variant_dir)
1362 if src_dir.is_under(variant_dir):
1363 raise SCons.Errors.UserError("Source directory cannot be under variant directory.")
1364 if variant_dir.srcdir:
1365 if variant_dir.srcdir == src_dir:
1366 return
1367 raise SCons.Errors.UserError("'%s' already has a source directory: '%s'."%(variant_dir, variant_dir.srcdir))
1368 variant_dir.link(src_dir, duplicate)
1369
1376
1378 """Create targets in corresponding variant directories
1379
1380 Climb the directory tree, and look up path names
1381 relative to any linked variant directories we find.
1382
1383 Even though this loops and walks up the tree, we don't memoize
1384 the return value because this is really only used to process
1385 the command-line targets.
1386 """
1387 targets = []
1388 message = None
1389 fmt = "building associated VariantDir targets: %s"
1390 start_dir = dir
1391 while dir:
1392 for bd in dir.variant_dirs:
1393 if start_dir.is_under(bd):
1394
1395 return [orig], fmt % str(orig)
1396 p = os.path.join(bd.path, *tail)
1397 targets.append(self.Entry(p))
1398 tail = [dir.name] + tail
1399 dir = dir.up()
1400 if targets:
1401 message = fmt % ' '.join(map(str, targets))
1402 return targets, message
1403
1404 - def Glob(self, pathname, ondisk=True, source=True, strings=False, cwd=None):
1405 """
1406 Globs
1407
1408 This is mainly a shim layer
1409 """
1410 if cwd is None:
1411 cwd = self.getcwd()
1412 return cwd.glob(pathname, ondisk, source, strings)
1413
1430
1433
1434 glob_magic_check = re.compile('[*?[]')
1435
1438
1440 """A class for directories in a file system.
1441 """
1442
1443 memoizer_counters = []
1444
1445 NodeInfo = DirNodeInfo
1446 BuildInfo = DirBuildInfo
1447
1448 - def __init__(self, name, directory, fs):
1452
1454 """Turn a file system Node (either a freshly initialized directory
1455 object or a separate Entry object) into a proper directory object.
1456
1457 Set up this directory's entries and hook it into the file
1458 system tree. Specify that directories (this Node) don't use
1459 signatures for calculating whether they're current.
1460 """
1461
1462 self.repositories = []
1463 self.srcdir = None
1464
1465 self.entries = {}
1466 self.entries['.'] = self
1467 self.entries['..'] = self.dir
1468 self.cwd = self
1469 self.searched = 0
1470 self._sconsign = None
1471 self.variant_dirs = []
1472 self.root = self.dir.root
1473
1474
1475
1476
1477
1478
1479
1480
1481
1482
1483 self.dirname = self.name + OS_SEP
1484
1485
1486
1487
1488
1489
1490
1491
1492
1493 if not hasattr(self, 'executor'):
1494 self.builder = get_MkdirBuilder()
1495 self.get_executor().set_action_list(self.builder.action)
1496 else:
1497
1498 l = self.get_executor().action_list
1499 a = get_MkdirBuilder().action
1500 l.insert(0, a)
1501 self.get_executor().set_action_list(l)
1502
1506
1508 """Called when we change the repository(ies) for a directory.
1509 This clears any cached information that is invalidated by changing
1510 the repository."""
1511
1512 for node in self.entries.values():
1513 if node != self.dir:
1514 if node != self and isinstance(node, Dir):
1515 node.__clearRepositoryCache(duplicate)
1516 else:
1517 node.clear()
1518 try:
1519 del node._srcreps
1520 except AttributeError:
1521 pass
1522 if duplicate is not None:
1523 node.duplicate=duplicate
1524
1526 if node != self:
1527 node.duplicate = node.get_dir().duplicate
1528
1529 - def Entry(self, name):
1530 """
1531 Looks up or creates an entry node named 'name' relative to
1532 this directory.
1533 """
1534 return self.fs.Entry(name, self)
1535
1536 - def Dir(self, name, create=True):
1537 """
1538 Looks up or creates a directory node named 'name' relative to
1539 this directory.
1540 """
1541 return self.fs.Dir(name, self, create)
1542
1543 - def File(self, name):
1544 """
1545 Looks up or creates a file node named 'name' relative to
1546 this directory.
1547 """
1548 return self.fs.File(name, self)
1549
1550 - def link(self, srcdir, duplicate):
1551 """Set this directory as the variant directory for the
1552 supplied source directory."""
1553 self.srcdir = srcdir
1554 self.duplicate = duplicate
1555 self.__clearRepositoryCache(duplicate)
1556 srcdir.variant_dirs.append(self)
1557
1559 """Returns a list of repositories for this directory.
1560 """
1561 if self.srcdir and not self.duplicate:
1562 return self.srcdir.get_all_rdirs() + self.repositories
1563 return self.repositories
1564
1565 memoizer_counters.append(SCons.Memoize.CountValue('get_all_rdirs'))
1566
1568 try:
1569 return list(self._memo['get_all_rdirs'])
1570 except KeyError:
1571 pass
1572
1573 result = [self]
1574 fname = '.'
1575 dir = self
1576 while dir:
1577 for rep in dir.getRepositories():
1578 result.append(rep.Dir(fname))
1579 if fname == '.':
1580 fname = dir.name
1581 else:
1582 fname = dir.name + OS_SEP + fname
1583 dir = dir.up()
1584
1585 self._memo['get_all_rdirs'] = list(result)
1586
1587 return result
1588
1590 if dir != self and not dir in self.repositories:
1591 self.repositories.append(dir)
1592 dir.tpath = '.'
1593 self.__clearRepositoryCache()
1594
1597
1600
1601 memoizer_counters.append(SCons.Memoize.CountDict('rel_path', _rel_path_key))
1602
1604 """Return a path to "other" relative to this directory.
1605 """
1606
1607
1608
1609
1610
1611
1612
1613
1614
1615
1616
1617 try:
1618 memo_dict = self._memo['rel_path']
1619 except KeyError:
1620 memo_dict = {}
1621 self._memo['rel_path'] = memo_dict
1622 else:
1623 try:
1624 return memo_dict[other]
1625 except KeyError:
1626 pass
1627
1628 if self is other:
1629 result = '.'
1630
1631 elif not other in self.path_elements:
1632 try:
1633 other_dir = other.get_dir()
1634 except AttributeError:
1635 result = str(other)
1636 else:
1637 if other_dir is None:
1638 result = other.name
1639 else:
1640 dir_rel_path = self.rel_path(other_dir)
1641 if dir_rel_path == '.':
1642 result = other.name
1643 else:
1644 result = dir_rel_path + OS_SEP + other.name
1645 else:
1646 i = self.path_elements.index(other) + 1
1647
1648 path_elems = ['..'] * (len(self.path_elements) - i) \
1649 + [n.name for n in other.path_elements[i:]]
1650
1651 result = OS_SEP.join(path_elems)
1652
1653 memo_dict[other] = result
1654
1655 return result
1656
1660
1664
1666 """Return this directory's implicit dependencies.
1667
1668 We don't bother caching the results because the scan typically
1669 shouldn't be requested more than once (as opposed to scanning
1670 .h file contents, which can be requested as many times as the
1671 files is #included by other files).
1672 """
1673 if not scanner:
1674 return []
1675
1676
1677
1678
1679
1680
1681
1682
1683 self.clear()
1684 return scanner(self, env, path)
1685
1686
1687
1688
1689
1692
1698
1699
1700
1701
1702
1704 """Create this directory, silently and without worrying about
1705 whether the builder is the default or not."""
1706 listDirs = []
1707 parent = self
1708 while parent:
1709 if parent.exists():
1710 break
1711 listDirs.append(parent)
1712 p = parent.up()
1713 if p is None:
1714
1715
1716 raise SCons.Errors.StopError(parent.path)
1717 parent = p
1718 listDirs.reverse()
1719 for dirnode in listDirs:
1720 try:
1721
1722
1723
1724
1725 SCons.Node.Node.build(dirnode)
1726 dirnode.get_executor().nullify()
1727
1728
1729
1730
1731 dirnode.clear()
1732 except OSError:
1733 pass
1734
1738
1740 """Return any corresponding targets in a variant directory.
1741 """
1742 return self.fs.variant_dir_target_climb(self, self, [])
1743
1745 """A directory does not get scanned."""
1746 return None
1747
1749 """We already emit things in text, so just return the binary
1750 version."""
1751 return self.get_contents()
1752
1753 - def get_contents(self):
1754 """Return content signatures and names of all our children
1755 separated by new-lines. Ensure that the nodes are sorted."""
1756 contents = []
1757 for node in sorted(self.children(), key=lambda t: t.name):
1758 contents.append('%s %s\n' % (node.get_csig(), node.name))
1759 return ''.join(contents)
1760
1762 """Compute the content signature for Directory nodes. In
1763 general, this is not needed and the content signature is not
1764 stored in the DirNodeInfo. However, if get_contents on a Dir
1765 node is called which has a child directory, the child
1766 directory should return the hash of its contents."""
1767 contents = self.get_contents()
1768 return SCons.Util.MD5signature(contents)
1769
1772
1773 changed_since_last_build = SCons.Node.Node.state_has_changed
1774
1785
1787 if not self.exists():
1788 norm_name = _my_normcase(self.name)
1789 for dir in self.dir.get_all_rdirs():
1790 try: node = dir.entries[norm_name]
1791 except KeyError: node = dir.dir_on_disk(self.name)
1792 if node and node.exists() and \
1793 (isinstance(dir, Dir) or isinstance(dir, Entry)):
1794 return node
1795 return self
1796
1798 """Return the .sconsign file info for this directory,
1799 creating it first if necessary."""
1800 if not self._sconsign:
1801 import SCons.SConsign
1802 self._sconsign = SCons.SConsign.ForDirectory(self)
1803 return self._sconsign
1804
1806 """Dir has a special need for srcnode()...if we
1807 have a srcdir attribute set, then that *is* our srcnode."""
1808 if self.srcdir:
1809 return self.srcdir
1810 return Base.srcnode(self)
1811
1813 """Return the latest timestamp from among our children"""
1814 stamp = 0
1815 for kid in self.children():
1816 if kid.get_timestamp() > stamp:
1817 stamp = kid.get_timestamp()
1818 return stamp
1819
1820 - def entry_abspath(self, name):
1821 return self.abspath + OS_SEP + name
1822
1823 - def entry_labspath(self, name):
1824 return self.labspath + '/' + name
1825
1826 - def entry_path(self, name):
1827 return self.path + OS_SEP + name
1828
1829 - def entry_tpath(self, name):
1830 return self.tpath + OS_SEP + name
1831
1832 - def entry_exists_on_disk(self, name):
1833 try:
1834 d = self.on_disk_entries
1835 except AttributeError:
1836 d = {}
1837 try:
1838 entries = os.listdir(self.abspath)
1839 except OSError:
1840 pass
1841 else:
1842 for entry in map(_my_normcase, entries):
1843 d[entry] = True
1844 self.on_disk_entries = d
1845 if sys.platform == 'win32' or sys.platform == 'cygwin':
1846 name = _my_normcase(name)
1847 result = d.get(name)
1848 if result is None:
1849
1850
1851 result = os.path.exists(self.abspath + OS_SEP + name)
1852 d[name] = result
1853 return result
1854 else:
1855 return name in d
1856
1857 memoizer_counters.append(SCons.Memoize.CountValue('srcdir_list'))
1858
1860 try:
1861 return self._memo['srcdir_list']
1862 except KeyError:
1863 pass
1864
1865 result = []
1866
1867 dirname = '.'
1868 dir = self
1869 while dir:
1870 if dir.srcdir:
1871 result.append(dir.srcdir.Dir(dirname))
1872 dirname = dir.name + OS_SEP + dirname
1873 dir = dir.up()
1874
1875 self._memo['srcdir_list'] = result
1876
1877 return result
1878
1895
1898
1899 memoizer_counters.append(SCons.Memoize.CountDict('srcdir_find_file', _srcdir_find_file_key))
1900
1902 try:
1903 memo_dict = self._memo['srcdir_find_file']
1904 except KeyError:
1905 memo_dict = {}
1906 self._memo['srcdir_find_file'] = memo_dict
1907 else:
1908 try:
1909 return memo_dict[filename]
1910 except KeyError:
1911 pass
1912
1913 def func(node):
1914 if (isinstance(node, File) or isinstance(node, Entry)) and \
1915 (node.is_derived() or node.exists()):
1916 return node
1917 return None
1918
1919 norm_name = _my_normcase(filename)
1920
1921 for rdir in self.get_all_rdirs():
1922 try: node = rdir.entries[norm_name]
1923 except KeyError: node = rdir.file_on_disk(filename)
1924 else: node = func(node)
1925 if node:
1926 result = (node, self)
1927 memo_dict[filename] = result
1928 return result
1929
1930 for srcdir in self.srcdir_list():
1931 for rdir in srcdir.get_all_rdirs():
1932 try: node = rdir.entries[norm_name]
1933 except KeyError: node = rdir.file_on_disk(filename)
1934 else: node = func(node)
1935 if node:
1936 result = (File(filename, self, self.fs), srcdir)
1937 memo_dict[filename] = result
1938 return result
1939
1940 result = (None, None)
1941 memo_dict[filename] = result
1942 return result
1943
1952
1963
1964 - def walk(self, func, arg):
1965 """
1966 Walk this directory tree by calling the specified function
1967 for each directory in the tree.
1968
1969 This behaves like the os.path.walk() function, but for in-memory
1970 Node.FS.Dir objects. The function takes the same arguments as
1971 the functions passed to os.path.walk():
1972
1973 func(arg, dirname, fnames)
1974
1975 Except that "dirname" will actually be the directory *Node*,
1976 not the string. The '.' and '..' entries are excluded from
1977 fnames. The fnames list may be modified in-place to filter the
1978 subdirectories visited or otherwise impose a specific order.
1979 The "arg" argument is always passed to func() and may be used
1980 in any way (or ignored, passing None is common).
1981 """
1982 entries = self.entries
1983 names = list(entries.keys())
1984 names.remove('.')
1985 names.remove('..')
1986 func(arg, self, names)
1987 for dirname in [n for n in names if isinstance(entries[n], Dir)]:
1988 entries[dirname].walk(func, arg)
1989
1990 - def glob(self, pathname, ondisk=True, source=False, strings=False):
1991 """
1992 Returns a list of Nodes (or strings) matching a specified
1993 pathname pattern.
1994
1995 Pathname patterns follow UNIX shell semantics: * matches
1996 any-length strings of any characters, ? matches any character,
1997 and [] can enclose lists or ranges of characters. Matches do
1998 not span directory separators.
1999
2000 The matches take into account Repositories, returning local
2001 Nodes if a corresponding entry exists in a Repository (either
2002 an in-memory Node or something on disk).
2003
2004 By defafult, the glob() function matches entries that exist
2005 on-disk, in addition to in-memory Nodes. Setting the "ondisk"
2006 argument to False (or some other non-true value) causes the glob()
2007 function to only match in-memory Nodes. The default behavior is
2008 to return both the on-disk and in-memory Nodes.
2009
2010 The "source" argument, when true, specifies that corresponding
2011 source Nodes must be returned if you're globbing in a build
2012 directory (initialized with VariantDir()). The default behavior
2013 is to return Nodes local to the VariantDir().
2014
2015 The "strings" argument, when true, returns the matches as strings,
2016 not Nodes. The strings are path names relative to this directory.
2017
2018 The underlying algorithm is adapted from the glob.glob() function
2019 in the Python library (but heavily modified), and uses fnmatch()
2020 under the covers.
2021 """
2022 dirname, basename = os.path.split(pathname)
2023 if not dirname:
2024 return sorted(self._glob1(basename, ondisk, source, strings),
2025 key=lambda t: str(t))
2026 if has_glob_magic(dirname):
2027 list = self.glob(dirname, ondisk, source, strings=False)
2028 else:
2029 list = [self.Dir(dirname, create=True)]
2030 result = []
2031 for dir in list:
2032 r = dir._glob1(basename, ondisk, source, strings)
2033 if strings:
2034 r = [os.path.join(str(dir), x) for x in r]
2035 result.extend(r)
2036 return sorted(result, key=lambda a: str(a))
2037
2038 - def _glob1(self, pattern, ondisk=True, source=False, strings=False):
2039 """
2040 Globs for and returns a list of entry names matching a single
2041 pattern in this directory.
2042
2043 This searches any repositories and source directories for
2044 corresponding entries and returns a Node (or string) relative
2045 to the current directory if an entry is found anywhere.
2046
2047 TODO: handle pattern with no wildcard
2048 """
2049 search_dir_list = self.get_all_rdirs()
2050 for srcdir in self.srcdir_list():
2051 search_dir_list.extend(srcdir.get_all_rdirs())
2052
2053 selfEntry = self.Entry
2054 names = []
2055 for dir in search_dir_list:
2056
2057
2058
2059 node_names = [ v.name for k, v in dir.entries.items()
2060 if k not in ('.', '..') ]
2061 names.extend(node_names)
2062 if not strings:
2063
2064
2065 for name in node_names: selfEntry(name)
2066 if ondisk:
2067 try:
2068 disk_names = os.listdir(dir.abspath)
2069 except os.error:
2070 continue
2071 names.extend(disk_names)
2072 if not strings:
2073
2074
2075
2076
2077
2078
2079
2080
2081 if pattern[0] != '.':
2082
2083 disk_names = [x for x in disk_names if x[0] != '.']
2084 disk_names = fnmatch.filter(disk_names, pattern)
2085 dirEntry = dir.Entry
2086 for name in disk_names:
2087
2088
2089 name = './' + name
2090 node = dirEntry(name).disambiguate()
2091 n = selfEntry(name)
2092 if n.__class__ != node.__class__:
2093 n.__class__ = node.__class__
2094 n._morph()
2095
2096 names = set(names)
2097 if pattern[0] != '.':
2098
2099 names = [x for x in names if x[0] != '.']
2100 names = fnmatch.filter(names, pattern)
2101
2102 if strings:
2103 return names
2104
2105
2106 return [self.entries[_my_normcase(n)] for n in names]
2107
2109 """A class for the root directory of a file system.
2110
2111 This is the same as a Dir class, except that the path separator
2112 ('/' or '\\') is actually part of the name, so we don't need to
2113 add a separator when creating the path names of entries within
2114 this directory.
2115 """
2117 if SCons.Debug.track_instances: logInstanceCreation(self, 'Node.FS.RootDir')
2118
2119
2120
2121 self.abspath = ''
2122 self.labspath = ''
2123 self.path = ''
2124 self.tpath = ''
2125 self.path_elements = []
2126 self.duplicate = 0
2127 self.root = self
2128
2129
2130 if drive == '':
2131
2132 name = OS_SEP
2133 dirname = OS_SEP
2134 elif drive == '//':
2135
2136 name = UNC_PREFIX
2137 dirname = UNC_PREFIX
2138 else:
2139
2140 name = drive
2141 dirname = drive + OS_SEP
2142
2143 Base.__init__(self, name, self, fs)
2144
2145
2146
2147
2148
2149
2150 self.abspath = dirname
2151 self.labspath = ''
2152 self.path = dirname
2153 self.tpath = dirname
2154 self._morph()
2155
2156
2157 self.dirname = dirname
2158
2159 self._lookupDict = {}
2160
2161 self._lookupDict[''] = self
2162 self._lookupDict['/'] = self
2163
2164
2165
2166
2167 if not has_unc:
2168 self._lookupDict['//'] = self
2169
2174
2176 """
2177 Fast (?) lookup of a *normalized* absolute path.
2178
2179 This method is intended for use by internal lookups with
2180 already-normalized path data. For general-purpose lookups,
2181 use the FS.Entry(), FS.Dir() or FS.File() methods.
2182
2183 The caller is responsible for making sure we're passed a
2184 normalized absolute path; we merely let Python's dictionary look
2185 up and return the One True Node.FS object for the path.
2186
2187 If a Node for the specified "p" doesn't already exist, and
2188 "create" is specified, the Node may be created after recursive
2189 invocation to find or create the parent directory or directories.
2190 """
2191 k = _my_normcase(p)
2192 try:
2193 result = self._lookupDict[k]
2194 except KeyError:
2195 if not create:
2196 msg = "No such file or directory: '%s' in '%s' (and create is False)" % (p, str(self))
2197 raise SCons.Errors.UserError(msg)
2198
2199
2200
2201
2202
2203 last_slash = p.rindex('/')
2204 if (last_slash >= 0):
2205 dir_name = p[:last_slash]
2206 file_name = p[last_slash+1:]
2207 else:
2208 dir_name = p
2209 file_name = ''
2210
2211 dir_node = self._lookup_abs(dir_name, Dir)
2212 result = klass(file_name, dir_node, self.fs)
2213
2214
2215
2216 result.diskcheck_match()
2217
2218 self._lookupDict[k] = result
2219 dir_node.entries[_my_normcase(file_name)] = result
2220 dir_node.implicit = None
2221 else:
2222
2223
2224 result.must_be_same(klass)
2225 return result
2226
2229
2230 - def entry_abspath(self, name):
2231 return self.abspath + name
2232
2233 - def entry_labspath(self, name):
2235
2236 - def entry_path(self, name):
2237 return self.path + name
2238
2239 - def entry_tpath(self, name):
2240 return self.tpath + name
2241
2243 if self is dir:
2244 return 1
2245 else:
2246 return 0
2247
2250
2253
2256
2275
2277 current_version_id = 1
2278
2280 """
2281 Converts this FileBuildInfo object for writing to a .sconsign file
2282
2283 This replaces each Node in our various dependency lists with its
2284 usual string representation: relative to the top-level SConstruct
2285 directory, or an absolute path if it's outside.
2286 """
2287 if os_sep_is_slash:
2288 node_to_str = str
2289 else:
2290 def node_to_str(n):
2291 try:
2292 s = n.path
2293 except AttributeError:
2294 s = str(n)
2295 else:
2296 s = s.replace(OS_SEP, '/')
2297 return s
2298 for attr in ['bsources', 'bdepends', 'bimplicit']:
2299 try:
2300 val = getattr(self, attr)
2301 except AttributeError:
2302 pass
2303 else:
2304 setattr(self, attr, list(map(node_to_str, val)))
2306 """
2307 Converts a newly-read FileBuildInfo object for in-SCons use
2308
2309 For normal up-to-date checking, we don't have any conversion to
2310 perform--but we're leaving this method here to make that clear.
2311 """
2312 pass
2314 """
2315 Prepares a FileBuildInfo object for explaining what changed
2316
2317 The bsources, bdepends and bimplicit lists have all been
2318 stored on disk as paths relative to the top-level SConstruct
2319 directory. Convert the strings to actual Nodes (for use by the
2320 --debug=explain code and --implicit-cache).
2321 """
2322 attrs = [
2323 ('bsources', 'bsourcesigs'),
2324 ('bdepends', 'bdependsigs'),
2325 ('bimplicit', 'bimplicitsigs'),
2326 ]
2327 for (nattr, sattr) in attrs:
2328 try:
2329 strings = getattr(self, nattr)
2330 nodeinfos = getattr(self, sattr)
2331 except AttributeError:
2332 continue
2333 nodes = []
2334 for s, ni in zip(strings, nodeinfos):
2335 if not isinstance(s, SCons.Node.Node):
2336 s = ni.str_to_node(s)
2337 nodes.append(s)
2338 setattr(self, nattr, nodes)
2348
2350 """A class for files in a file system.
2351 """
2352
2353 memoizer_counters = []
2354
2355 NodeInfo = FileNodeInfo
2356 BuildInfo = FileBuildInfo
2357
2358 md5_chunksize = 64
2359
2363
2364 - def __init__(self, name, directory, fs):
2368
2369 - def Entry(self, name):
2370 """Create an entry node named 'name' relative to
2371 the directory of this file."""
2372 return self.dir.Entry(name)
2373
2374 - def Dir(self, name, create=True):
2375 """Create a directory node named 'name' relative to
2376 the directory of this file."""
2377 return self.dir.Dir(name, create=create)
2378
2379 - def Dirs(self, pathlist):
2380 """Create a list of directories relative to the SConscript
2381 directory of this file."""
2382 return [self.Dir(p) for p in pathlist]
2383
2384 - def File(self, name):
2385 """Create a file node named 'name' relative to
2386 the directory of this file."""
2387 return self.dir.File(name)
2388
2389
2390
2391
2392
2393
2394
2395
2397 """Turn a file system node into a File object."""
2398 self.scanner_paths = {}
2399 if not hasattr(self, '_local'):
2400 self._local = 0
2401 if not hasattr(self, 'released_target_info'):
2402 self.released_target_info = False
2403
2404
2405
2406
2407
2408
2409
2410
2411
2412
2413
2414 if self.has_builder():
2415 self.changed_since_last_build = self.decide_target
2416
2419
2420 - def get_contents(self):
2421 if not self.rexists():
2422 return ''
2423 fname = self.rfile().abspath
2424 try:
2425 contents = open(fname, "rb").read()
2426 except EnvironmentError, e:
2427 if not e.filename:
2428 e.filename = fname
2429 raise
2430 return contents
2431
2432
2433
2434
2436 contents = self.get_contents()
2437
2438
2439
2440
2441
2442
2443 if contents.startswith(codecs.BOM_UTF8):
2444 return contents[len(codecs.BOM_UTF8):].decode('utf-8')
2445 if contents.startswith(codecs.BOM_UTF16_LE):
2446 return contents[len(codecs.BOM_UTF16_LE):].decode('utf-16-le')
2447 if contents.startswith(codecs.BOM_UTF16_BE):
2448 return contents[len(codecs.BOM_UTF16_BE):].decode('utf-16-be')
2449 return contents
2450
2451 - def get_content_hash(self):
2452 """
2453 Compute and return the MD5 hash for this file.
2454 """
2455 if not self.rexists():
2456 return SCons.Util.MD5signature('')
2457 fname = self.rfile().abspath
2458 try:
2459 cs = SCons.Util.MD5filesignature(fname,
2460 chunksize=SCons.Node.FS.File.md5_chunksize*1024)
2461 except EnvironmentError, e:
2462 if not e.filename:
2463 e.filename = fname
2464 raise
2465 return cs
2466
2467
2468 memoizer_counters.append(SCons.Memoize.CountValue('get_size'))
2469
2471 try:
2472 return self._memo['get_size']
2473 except KeyError:
2474 pass
2475
2476 if self.rexists():
2477 size = self.rfile().getsize()
2478 else:
2479 size = 0
2480
2481 self._memo['get_size'] = size
2482
2483 return size
2484
2485 memoizer_counters.append(SCons.Memoize.CountValue('get_timestamp'))
2486
2488 try:
2489 return self._memo['get_timestamp']
2490 except KeyError:
2491 pass
2492
2493 if self.rexists():
2494 timestamp = self.rfile().getmtime()
2495 else:
2496 timestamp = 0
2497
2498 self._memo['get_timestamp'] = timestamp
2499
2500 return timestamp
2501
2509
2510 convert_copy_attrs = [
2511 'bsources',
2512 'bimplicit',
2513 'bdepends',
2514 'bact',
2515 'bactsig',
2516 'ninfo',
2517 ]
2518
2519
2520 convert_sig_attrs = [
2521 'bsourcesigs',
2522 'bimplicitsigs',
2523 'bdependsigs',
2524 ]
2525
2526 - def convert_old_entry(self, old_entry):
2527
2528
2529
2530
2531
2532
2533
2534
2535
2536
2537
2538
2539
2540
2541
2542
2543
2544
2545
2546
2547
2548
2549
2550
2551
2552
2553
2554
2555
2556
2557
2558
2559
2560
2561
2562
2563
2564
2565
2566
2567
2568
2569
2570
2571
2572
2573
2574
2575
2576
2577
2578
2579
2580
2581
2582
2583
2584
2585
2586
2587
2588
2589
2590
2591
2592
2593
2594 import SCons.SConsign
2595 new_entry = SCons.SConsign.SConsignEntry()
2596 new_entry.binfo = self.new_binfo()
2597 binfo = new_entry.binfo
2598 for attr in self.convert_copy_attrs:
2599 try:
2600 value = getattr(old_entry, attr)
2601 except AttributeError:
2602 continue
2603 setattr(binfo, attr, value)
2604 delattr(old_entry, attr)
2605 for attr in self.convert_sig_attrs:
2606 try:
2607 sig_list = getattr(old_entry, attr)
2608 except AttributeError:
2609 continue
2610 value = []
2611 for sig in sig_list:
2612 ninfo = self.new_ninfo()
2613 if len(sig) == 32:
2614 ninfo.csig = sig
2615 else:
2616 ninfo.timestamp = sig
2617 value.append(ninfo)
2618 setattr(binfo, attr, value)
2619 delattr(old_entry, attr)
2620 return new_entry
2621
2622 memoizer_counters.append(SCons.Memoize.CountValue('get_stored_info'))
2623
2625 try:
2626 return self._memo['get_stored_info']
2627 except KeyError:
2628 pass
2629
2630 try:
2631 sconsign_entry = self.dir.sconsign().get_entry(self.name)
2632 except (KeyError, EnvironmentError):
2633 import SCons.SConsign
2634 sconsign_entry = SCons.SConsign.SConsignEntry()
2635 sconsign_entry.binfo = self.new_binfo()
2636 sconsign_entry.ninfo = self.new_ninfo()
2637 else:
2638 if isinstance(sconsign_entry, FileBuildInfo):
2639
2640
2641 sconsign_entry = self.convert_old_entry(sconsign_entry)
2642 try:
2643 delattr(sconsign_entry.ninfo, 'bsig')
2644 except AttributeError:
2645 pass
2646
2647 self._memo['get_stored_info'] = sconsign_entry
2648
2649 return sconsign_entry
2650
2656
2659
2661 return (id(env), id(scanner), path)
2662
2663 memoizer_counters.append(SCons.Memoize.CountDict('get_found_includes', _get_found_includes_key))
2664
2666 """Return the included implicit dependencies in this file.
2667 Cache results so we only scan the file once per path
2668 regardless of how many times this information is requested.
2669 """
2670 memo_key = (id(env), id(scanner), path)
2671 try:
2672 memo_dict = self._memo['get_found_includes']
2673 except KeyError:
2674 memo_dict = {}
2675 self._memo['get_found_includes'] = memo_dict
2676 else:
2677 try:
2678 return memo_dict[memo_key]
2679 except KeyError:
2680 pass
2681
2682 if scanner:
2683
2684 result = scanner(self, env, path)
2685 result = [N.disambiguate() for N in result]
2686 else:
2687 result = []
2688
2689 memo_dict[memo_key] = result
2690
2691 return result
2692
2697
2713
2715 """Try to retrieve the node's content from a cache
2716
2717 This method is called from multiple threads in a parallel build,
2718 so only do thread safe stuff here. Do thread unsafe stuff in
2719 built().
2720
2721 Returns true if the node was successfully retrieved.
2722 """
2723 if self.nocache:
2724 return None
2725 if not self.is_derived():
2726 return None
2727 return self.get_build_env().get_CacheDir().retrieve(self)
2728
2751
2753 """Called just after this node has been marked
2754 up-to-date or was built completely.
2755
2756 This is where we try to release as many target node infos
2757 as possible for clean builds and update runs, in order
2758 to minimize the overall memory consumption.
2759
2760 We'd like to remove a lot more attributes like self.sources
2761 and self.sources_set, but they might get used
2762 in a next build step. For example, during configuration
2763 the source files for a built *.o file are used to figure out
2764 which linker to use for the resulting Program (gcc vs. g++)!
2765 That's why we check for the 'keep_targetinfo' attribute,
2766 config Nodes and the Interactive mode just don't allow
2767 an early release of most variables.
2768
2769 In the same manner, we can't simply remove the self.attributes
2770 here. The smart linking relies on the shared flag, and some
2771 parts of the java Tool use it to transport information
2772 about nodes...
2773
2774 @see: built() and Node.release_target_info()
2775 """
2776 if (self.released_target_info or SCons.Node.interactive):
2777 return
2778
2779 if not hasattr(self.attributes, 'keep_targetinfo'):
2780
2781
2782 self.changed(allowcache=True)
2783 self.get_contents_sig()
2784 self.get_build_env()
2785
2786 self.executor = None
2787 self._memo.pop('rfile', None)
2788 self.prerequisites = None
2789
2790 if not len(self.ignore_set):
2791 self.ignore_set = None
2792 if not len(self.implicit_set):
2793 self.implicit_set = None
2794 if not len(self.depends_set):
2795 self.depends_set = None
2796 if not len(self.ignore):
2797 self.ignore = None
2798 if not len(self.depends):
2799 self.depends = None
2800
2801
2802 self.released_target_info = True
2803
2823
2825 """Return whether this Node has a source builder or not.
2826
2827 If this Node doesn't have an explicit source code builder, this
2828 is where we figure out, on the fly, if there's a transparent
2829 source code builder for it.
2830
2831 Note that if we found a source builder, we also set the
2832 self.builder attribute, so that all of the methods that actually
2833 *build* this file don't have to do anything different.
2834 """
2835 try:
2836 scb = self.sbuilder
2837 except AttributeError:
2838 scb = self.sbuilder = self.find_src_builder()
2839 return scb is not None
2840
2842 """Return any corresponding targets in a variant directory.
2843 """
2844 if self.is_derived():
2845 return [], None
2846 return self.fs.variant_dir_target_climb(self, self.dir, [self.name])
2847
2855
2856
2857
2858
2859
2863
2878
2879
2880
2881
2882
2884 """Remove this file."""
2885 if self.exists() or self.islink():
2886 self.fs.unlink(self.path)
2887 return 1
2888 return None
2889
2905
2906 memoizer_counters.append(SCons.Memoize.CountValue('exists'))
2907
2909 try:
2910 return self._memo['exists']
2911 except KeyError:
2912 pass
2913
2914 if self.duplicate and not self.is_derived() and not self.linked:
2915 src = self.srcnode()
2916 if src is not self:
2917
2918 src = src.rfile()
2919 if src.abspath != self.abspath:
2920 if src.exists():
2921 self.do_duplicate(src)
2922
2923
2924 else:
2925
2926
2927 if print_duplicate:
2928 print "dup: no src for %s, unlinking old variant copy"%self
2929 if Base.exists(self) or self.islink():
2930 self.fs.unlink(self.path)
2931
2932
2933 self._memo['exists'] = None
2934 return None
2935 result = Base.exists(self)
2936 self._memo['exists'] = result
2937 return result
2938
2939
2940
2941
2942
2944 """
2945 Returns the content signature currently stored for this node
2946 if it's been unmodified longer than the max_drift value, or the
2947 max_drift value is 0. Returns None otherwise.
2948 """
2949 old = self.get_stored_info()
2950 mtime = self.get_timestamp()
2951
2952 max_drift = self.fs.max_drift
2953 if max_drift > 0:
2954 if (time.time() - mtime) > max_drift:
2955 try:
2956 n = old.ninfo
2957 if n.timestamp and n.csig and n.timestamp == mtime:
2958 return n.csig
2959 except AttributeError:
2960 pass
2961 elif max_drift == 0:
2962 try:
2963 return old.ninfo.csig
2964 except AttributeError:
2965 pass
2966
2967 return None
2968
2970 """
2971 Generate a node's content signature, the digested signature
2972 of its content.
2973
2974 node - the node
2975 cache - alternate node to use for the signature cache
2976 returns - the content signature
2977 """
2978 ninfo = self.get_ninfo()
2979 try:
2980 return ninfo.csig
2981 except AttributeError:
2982 pass
2983
2984 csig = self.get_max_drift_csig()
2985 if csig is None:
2986
2987 try:
2988 if self.get_size() < SCons.Node.FS.File.md5_chunksize:
2989 contents = self.get_contents()
2990 else:
2991 csig = self.get_content_hash()
2992 except IOError:
2993
2994
2995
2996
2997 csig = ''
2998 else:
2999 if not csig:
3000 csig = SCons.Util.MD5signature(contents)
3001
3002 ninfo.csig = csig
3003
3004 return csig
3005
3006
3007
3008
3009
3013
3015 """Called just after this File node is successfully built.
3016
3017 Just like for 'release_target_info' we try to release
3018 some more target node attributes in order to minimize the
3019 overall memory consumption.
3020
3021 @see: release_target_info
3022 """
3023
3024 SCons.Node.Node.built(self)
3025
3026 if (not SCons.Node.interactive and
3027 not hasattr(self.attributes, 'keep_targetinfo')):
3028
3029 self.store_info()
3030
3031 self._specific_sources = False
3032 self.labspath = None
3033 self._save_str()
3034 self.cwd = None
3035
3036 self.scanner_paths = None
3037
3038 - def changed(self, node=None, allowcache=False):
3039 """
3040 Returns if the node is up-to-date with respect to the BuildInfo
3041 stored last time it was built.
3042
3043 For File nodes this is basically a wrapper around Node.changed(),
3044 but we allow the return value to get cached after the reference
3045 to the Executor got released in release_target_info().
3046
3047 @see: Node.changed()
3048 """
3049 if node is None:
3050 try:
3051 return self._memo['changed']
3052 except KeyError:
3053 pass
3054
3055 has_changed = SCons.Node.Node.changed(self, node)
3056 if allowcache:
3057 self._memo['changed'] = has_changed
3058 return has_changed
3059
3060 - def changed_content(self, target, prev_ni):
3061 cur_csig = self.get_csig()
3062 try:
3063 return cur_csig != prev_ni.csig
3064 except AttributeError:
3065 return 1
3066
3069
3070 - def changed_timestamp_then_content(self, target, prev_ni):
3071 if not self.changed_timestamp_match(target, prev_ni):
3072 try:
3073 self.get_ninfo().csig = prev_ni.csig
3074 except AttributeError:
3075 pass
3076 return False
3077 return self.changed_content(target, prev_ni)
3078
3084
3086 try:
3087 return self.get_timestamp() != prev_ni.timestamp
3088 except AttributeError:
3089 return 1
3090
3093
3096
3097
3098
3099 changed_since_last_build = decide_source
3100
3102 T = 0
3103 if T: Trace('is_up_to_date(%s):' % self)
3104 if not self.exists():
3105 if T: Trace(' not self.exists():')
3106
3107 r = self.rfile()
3108 if r != self:
3109
3110 if not self.changed(r):
3111 if T: Trace(' changed(%s):' % r)
3112
3113 if self._local:
3114
3115 e = LocalCopy(self, r, None)
3116 if isinstance(e, SCons.Errors.BuildError):
3117 raise
3118 self.store_info()
3119 if T: Trace(' 1\n')
3120 return 1
3121 self.changed()
3122 if T: Trace(' None\n')
3123 return None
3124 else:
3125 r = self.changed()
3126 if T: Trace(' self.exists(): %s\n' % r)
3127 return not r
3128
3129 memoizer_counters.append(SCons.Memoize.CountValue('rfile'))
3130
3132 try:
3133 return self._memo['rfile']
3134 except KeyError:
3135 pass
3136 result = self
3137 if not self.exists():
3138 norm_name = _my_normcase(self.name)
3139 for dir in self.dir.get_all_rdirs():
3140 try: node = dir.entries[norm_name]
3141 except KeyError: node = dir.file_on_disk(self.name)
3142 if node and node.exists() and \
3143 (isinstance(node, File) or isinstance(node, Entry) \
3144 or not node.is_derived()):
3145 result = node
3146
3147
3148
3149
3150
3151
3152
3153
3154
3155
3156
3157
3158 result.attributes = self.attributes
3159 break
3160 self._memo['rfile'] = result
3161 return result
3162
3164 return str(self.rfile())
3165
3167 """
3168 Fetch a Node's content signature for purposes of computing
3169 another Node's cachesig.
3170
3171 This is a wrapper around the normal get_csig() method that handles
3172 the somewhat obscure case of using CacheDir with the -n option.
3173 Any files that don't exist would normally be "built" by fetching
3174 them from the cache, but the normal get_csig() method will try
3175 to open up the local file, which doesn't exist because the -n
3176 option meant we didn't actually pull the file from cachedir.
3177 But since the file *does* actually exist in the cachedir, we
3178 can use its contents for the csig.
3179 """
3180 try:
3181 return self.cachedir_csig
3182 except AttributeError:
3183 pass
3184
3185 cachedir, cachefile = self.get_build_env().get_CacheDir().cachepath(self)
3186 if not self.exists() and cachefile and os.path.exists(cachefile):
3187 self.cachedir_csig = SCons.Util.MD5filesignature(cachefile, \
3188 SCons.Node.FS.File.md5_chunksize * 1024)
3189 else:
3190 self.cachedir_csig = self.get_csig()
3191 return self.cachedir_csig
3192
3193 - def get_contents_sig(self):
3194 """
3195 A helper method for get_cachedir_bsig.
3196
3197 It computes and returns the signature for this
3198 node's contents.
3199 """
3200
3201 try:
3202 return self.contentsig
3203 except AttributeError:
3204 pass
3205
3206 executor = self.get_executor()
3207
3208 result = self.contentsig = SCons.Util.MD5signature(executor.get_contents())
3209 return result
3210
3212 """
3213 Return the signature for a cached file, including
3214 its children.
3215
3216 It adds the path of the cached file to the cache signature,
3217 because multiple targets built by the same action will all
3218 have the same build signature, and we have to differentiate
3219 them somehow.
3220 """
3221 try:
3222 return self.cachesig
3223 except AttributeError:
3224 pass
3225
3226
3227 children = self.children()
3228 sigs = [n.get_cachedir_csig() for n in children]
3229
3230 sigs.append(self.get_contents_sig())
3231
3232 sigs.append(self.path)
3233
3234 result = self.cachesig = SCons.Util.MD5collect(sigs)
3235 return result
3236
3237 default_fs = None
3238
3244
3246 """
3247 """
3248 if SCons.Memoize.use_memoizer:
3249 __metaclass__ = SCons.Memoize.Memoized_Metaclass
3250
3251 memoizer_counters = []
3252
3255
3257 """
3258 A helper method for find_file() that looks up a directory for
3259 a file we're trying to find. This only creates the Dir Node if
3260 it exists on-disk, since if the directory doesn't exist we know
3261 we won't find any files in it... :-)
3262
3263 It would be more compact to just use this as a nested function
3264 with a default keyword argument (see the commented-out version
3265 below), but that doesn't work unless you have nested scopes,
3266 so we define it here just so this work under Python 1.5.2.
3267 """
3268 if fd is None:
3269 fd = self.default_filedir
3270 dir, name = os.path.split(fd)
3271 drive, d = _my_splitdrive(dir)
3272 if not name and d[:1] in ('/', OS_SEP):
3273
3274 return p.fs.get_root(drive)
3275 if dir:
3276 p = self.filedir_lookup(p, dir)
3277 if not p:
3278 return None
3279 norm_name = _my_normcase(name)
3280 try:
3281 node = p.entries[norm_name]
3282 except KeyError:
3283 return p.dir_on_disk(name)
3284 if isinstance(node, Dir):
3285 return node
3286 if isinstance(node, Entry):
3287 node.must_be_same(Dir)
3288 return node
3289 return None
3290
3292 return (filename, paths)
3293
3294 memoizer_counters.append(SCons.Memoize.CountDict('find_file', _find_file_key))
3295
3296 - def find_file(self, filename, paths, verbose=None):
3297 """
3298 find_file(str, [Dir()]) -> [nodes]
3299
3300 filename - a filename to find
3301 paths - a list of directory path *nodes* to search in. Can be
3302 represented as a list, a tuple, or a callable that is
3303 called with no arguments and returns the list or tuple.
3304
3305 returns - the node created from the found file.
3306
3307 Find a node corresponding to either a derived file or a file
3308 that exists already.
3309
3310 Only the first file found is returned, and none is returned
3311 if no file is found.
3312 """
3313 memo_key = self._find_file_key(filename, paths)
3314 try:
3315 memo_dict = self._memo['find_file']
3316 except KeyError:
3317 memo_dict = {}
3318 self._memo['find_file'] = memo_dict
3319 else:
3320 try:
3321 return memo_dict[memo_key]
3322 except KeyError:
3323 pass
3324
3325 if verbose and not callable(verbose):
3326 if not SCons.Util.is_String(verbose):
3327 verbose = "find_file"
3328 _verbose = u' %s: ' % verbose
3329 verbose = lambda s: sys.stdout.write(_verbose + s)
3330
3331 filedir, filename = os.path.split(filename)
3332 if filedir:
3333
3334
3335
3336
3337
3338
3339
3340
3341
3342
3343
3344
3345
3346
3347
3348
3349
3350
3351
3352
3353
3354
3355
3356
3357
3358
3359
3360
3361
3362
3363 self.default_filedir = filedir
3364 paths = [_f for _f in map(self.filedir_lookup, paths) if _f]
3365
3366 result = None
3367 for dir in paths:
3368 if verbose:
3369 verbose("looking for '%s' in '%s' ...\n" % (filename, dir))
3370 node, d = dir.srcdir_find_file(filename)
3371 if node:
3372 if verbose:
3373 verbose("... FOUND '%s' in '%s'\n" % (filename, d))
3374 result = node
3375 break
3376
3377 memo_dict[memo_key] = result
3378
3379 return result
3380
3381 find_file = FileFinder().find_file
3382
3383
3385 """
3386 Invalidate the memoized values of all Nodes (files or directories)
3387 that are associated with the given entries. Has been added to
3388 clear the cache of nodes affected by a direct execution of an
3389 action (e.g. Delete/Copy/Chmod). Existing Node caches become
3390 inconsistent if the action is run through Execute(). The argument
3391 `targets` can be a single Node object or filename, or a sequence
3392 of Nodes/filenames.
3393 """
3394 from traceback import extract_stack
3395
3396
3397
3398
3399
3400
3401 for f in extract_stack():
3402 if f[2] == 'Execute' and f[0][-14:] == 'Environment.py':
3403 break
3404 else:
3405
3406 return
3407
3408 if not SCons.Util.is_List(targets):
3409 targets = [targets]
3410
3411 for entry in targets:
3412
3413
3414 try:
3415 entry.clear_memoized_values()
3416 except AttributeError:
3417
3418
3419
3420 node = get_default_fs().Entry(entry)
3421 if node:
3422 node.clear_memoized_values()
3423
3424
3425
3426
3427
3428
3429