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 from __future__ import print_function
35
36 __revision__ = "src/engine/SCons/Node/FS.py a56bbd8c09fb219ab8a9673330ffcd55279219d0 2019-03-26 23:16:31 bdeegan"
37
38 import fnmatch
39 import os
40 import re
41 import shutil
42 import stat
43 import sys
44 import time
45 import codecs
46 from itertools import chain
47
48 import SCons.Action
49 import SCons.Debug
50 from SCons.Debug import logInstanceCreation
51 import SCons.Errors
52 import SCons.Memoize
53 import SCons.Node
54 import SCons.Node.Alias
55 import SCons.Subst
56 import SCons.Util
57 import SCons.Warnings
58
59 from SCons.Debug import Trace
60 from . import DeciderNeedsNode
61
62 print_duplicate = 0
63
64 MD5_TIMESTAMP_DEBUG = False
68 raise NotImplementedError
69
77
78 _sconsign_map = {0 : sconsign_none,
79 1 : sconsign_dir}
83
84 -class EntryProxyAttributeError(AttributeError):
85 """
86 An AttributeError subclass for recording and displaying the name
87 of the underlying Entry involved in an AttributeError exception.
88 """
89 - def __init__(self, entry_proxy, attribute):
90 AttributeError.__init__(self)
91 self.entry_proxy = entry_proxy
92 self.attribute = attribute
94 entry = self.entry_proxy.get()
95 fmt = "%s instance %s has no attribute %s"
96 return fmt % (entry.__class__.__name__,
97 repr(entry.name),
98 repr(self.attribute))
99
100
101
102 default_max_drift = 2*24*60*60
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122 Save_Strings = None
127
128
129
130
131
132
133
134
135 do_splitdrive = None
136 _my_splitdrive =None
139 global do_splitdrive
140 global has_unc
141 drive, path = os.path.splitdrive('X:/foo')
142
143
144 has_unc = (hasattr(os.path, 'splitunc')
145 or os.path.splitdrive(r'\\split\drive\test')[0] == r'\\split\drive')
146
147 do_splitdrive = not not drive or has_unc
148
149 global _my_splitdrive
150 if has_unc:
151 def splitdrive(p):
152 if p[1:2] == ':':
153 return p[:2], p[2:]
154 if p[0:2] == '//':
155
156
157 return '//', p[1:]
158 return '', p
159 else:
160 def splitdrive(p):
161 if p[1:2] == ':':
162 return p[:2], p[2:]
163 return '', p
164 _my_splitdrive = splitdrive
165
166
167
168 global OS_SEP
169 global UNC_PREFIX
170 global os_sep_is_slash
171
172 OS_SEP = os.sep
173 UNC_PREFIX = OS_SEP + OS_SEP
174 os_sep_is_slash = OS_SEP == '/'
175
176 initialize_do_splitdrive()
177
178
179 needs_normpath_check = re.compile(
180 r'''
181 # We need to renormalize the path if it contains any consecutive
182 # '/' characters.
183 .*// |
184
185 # We need to renormalize the path if it contains a '..' directory.
186 # Note that we check for all the following cases:
187 #
188 # a) The path is a single '..'
189 # b) The path starts with '..'. E.g. '../' or '../moredirs'
190 # but we not match '..abc/'.
191 # c) The path ends with '..'. E.g. '/..' or 'dirs/..'
192 # d) The path contains a '..' in the middle.
193 # E.g. dirs/../moredirs
194
195 (.*/)?\.\.(?:/|$) |
196
197 # We need to renormalize the path if it contains a '.'
198 # directory, but NOT if it is a single '.' '/' characters. We
199 # do not want to match a single '.' because this case is checked
200 # for explicitly since this is common enough case.
201 #
202 # Note that we check for all the following cases:
203 #
204 # a) We don't match a single '.'
205 # b) We match if the path starts with '.'. E.g. './' or
206 # './moredirs' but we not match '.abc/'.
207 # c) We match if the path ends with '.'. E.g. '/.' or
208 # 'dirs/.'
209 # d) We match if the path contains a '.' in the middle.
210 # E.g. dirs/./moredirs
211
212 \./|.*/\.(?:/|$)
213
214 ''',
215 re.VERBOSE
216 )
217 needs_normpath_match = needs_normpath_check.match
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238 if hasattr(os, 'link') and sys.platform != 'win32':
251 else:
252 _hardlink_func = None
253
254 if hasattr(os, 'symlink') and sys.platform != 'win32':
257 else:
258 _softlink_func = None
264
265
266 Valid_Duplicates = ['hard-soft-copy', 'soft-hard-copy',
267 'hard-copy', 'soft-copy', 'copy']
268
269 Link_Funcs = []
293
295 """
296 Relative paths cause problems with symbolic links, so
297 we use absolute paths, which may be a problem for people
298 who want to move their soft-linked src-trees around. Those
299 people should use the 'hard-copy' mode, softlinks cannot be
300 used for that; at least I have no idea how ...
301 """
302 src = source[0].get_abspath()
303 dest = target[0].get_abspath()
304 dir, file = os.path.split(dest)
305 if dir and not target[0].fs.isdir(dir):
306 os.makedirs(dir)
307 if not Link_Funcs:
308
309 set_duplicate('hard-soft-copy')
310 fs = source[0].fs
311
312 for func in Link_Funcs:
313 try:
314 func(fs, src, dest)
315 break
316 except (IOError, OSError):
317
318
319
320
321
322
323 if func == Link_Funcs[-1]:
324
325 raise
326 return 0
327
328 Link = SCons.Action.Action(LinkFunc, None)
330 return 'Local copy of %s from %s' % (target[0], source[0])
331
332 LocalCopy = SCons.Action.Action(LinkFunc, LocalString)
338
339 Unlink = SCons.Action.Action(UnlinkFunc, None)
351
352 Mkdir = SCons.Action.Action(MkdirFunc, None, presub=None)
353
354 MkdirBuilder = None
370
373
374 _null = _Null()
375
376
377 _is_cygwin = sys.platform == "cygwin"
378 if os.path.normcase("TeSt") == os.path.normpath("TeSt") and not _is_cygwin:
381 else:
384
389 self.type = type
390 self.do = do
391 self.ignore = ignore
392 self.func = do
394 return self.func(*args, **kw)
395 - def set(self, list):
396 if self.type in list:
397 self.func = self.do
398 else:
399 self.func = self.ignore
400
402 result = predicate()
403 try:
404
405
406
407
408
409
410 if node._memo['stat'] is None:
411 del node._memo['stat']
412 except (AttributeError, KeyError):
413 pass
414 if result:
415 raise TypeError(errorfmt % node.get_abspath())
416
419
420
421
422 diskcheck_match = DiskChecker('match', do_diskcheck_match, ignore_diskcheck_match)
423
424 diskcheckers = [
425 diskcheck_match,
426 ]
431
434
435
436
437 -class EntryProxy(SCons.Util.Proxy):
438
439 __str__ = SCons.Util.Delegate('__str__')
440
441
442
443
444 __hash__ = SCons.Util.Delegate('__hash__')
445
446 - def __get_abspath(self):
447 entry = self.get()
448 return SCons.Subst.SpecialAttrWrapper(entry.get_abspath(),
449 entry.name + "_abspath")
450
451 - def __get_filebase(self):
455
456 - def __get_suffix(self):
460
461 - def __get_file(self):
464
465 - def __get_base_path(self):
466 """Return the file's directory and file name, with the
467 suffix stripped."""
468 entry = self.get()
469 return SCons.Subst.SpecialAttrWrapper(SCons.Util.splitext(entry.get_path())[0],
470 entry.name + "_base")
471
473 """Return the path with / as the path separator,
474 regardless of platform."""
475 if os_sep_is_slash:
476 return self
477 else:
478 entry = self.get()
479 r = entry.get_path().replace(OS_SEP, '/')
480 return SCons.Subst.SpecialAttrWrapper(r, entry.name + "_posix")
481
483 """Return the path with \ as the path separator,
484 regardless of platform."""
485 if OS_SEP == '\\':
486 return self
487 else:
488 entry = self.get()
489 r = entry.get_path().replace(OS_SEP, '\\')
490 return SCons.Subst.SpecialAttrWrapper(r, entry.name + "_windows")
491
492 - def __get_srcnode(self):
493 return EntryProxy(self.get().srcnode())
494
495 - def __get_srcdir(self):
496 """Returns the directory containing the source node linked to this
497 node via VariantDir(), or the directory of this node if not linked."""
498 return EntryProxy(self.get().srcnode().dir)
499
500 - def __get_rsrcnode(self):
501 return EntryProxy(self.get().srcnode().rfile())
502
503 - def __get_rsrcdir(self):
504 """Returns the directory containing the source node linked to this
505 node via VariantDir(), or the directory of this node if not linked."""
506 return EntryProxy(self.get().srcnode().rfile().dir)
507
508 - def __get_dir(self):
509 return EntryProxy(self.get().dir)
510
511 dictSpecialAttrs = { "base" : __get_base_path,
512 "posix" : __get_posix_path,
513 "windows" : __get_windows_path,
514 "win32" : __get_windows_path,
515 "srcpath" : __get_srcnode,
516 "srcdir" : __get_srcdir,
517 "dir" : __get_dir,
518 "abspath" : __get_abspath,
519 "filebase" : __get_filebase,
520 "suffix" : __get_suffix,
521 "file" : __get_file,
522 "rsrcpath" : __get_rsrcnode,
523 "rsrcdir" : __get_rsrcdir,
524 }
525
526 - def __getattr__(self, name):
527
528
529 try:
530 attr_function = self.dictSpecialAttrs[name]
531 except KeyError:
532 try:
533 attr = SCons.Util.Proxy.__getattr__(self, name)
534 except AttributeError as e:
535
536
537
538 raise EntryProxyAttributeError(self, name)
539 return attr
540 else:
541 return attr_function(self)
542
543
544 -class Base(SCons.Node.Node):
545 """A generic class for file system entries. This class is for
546 when we don't know yet whether the entry being looked up is a file
547 or a directory. Instances of this class can morph into either
548 Dir or File objects by a later, more precise lookup.
549
550 Note: this class does not define __cmp__ and __hash__ for
551 efficiency reasons. SCons does a lot of comparing of
552 Node.FS.{Base,Entry,File,Dir} objects, so those operations must be
553 as fast as possible, which means we want to use Python's built-in
554 object identity comparisons.
555 """
556
557 __slots__ = ['name',
558 'fs',
559 '_abspath',
560 '_labspath',
561 '_path',
562 '_tpath',
563 '_path_elements',
564 'dir',
565 'cwd',
566 'duplicate',
567 '_local',
568 'sbuilder',
569 '_proxy',
570 '_func_sconsign']
571
572 - def __init__(self, name, directory, fs):
607
609 return '"' + self.__str__() + '"'
610
612 """
613 This node, which already existed, is being looked up as the
614 specified klass. Raise an exception if it isn't.
615 """
616 if isinstance(self, klass) or klass is Entry:
617 return
618 raise TypeError("Tried to lookup %s '%s' as a %s." %\
619 (self.__class__.__name__, self.get_internal_path(), klass.__name__))
620
623
626
629
631 """ Together with the node_bwcomp dict defined below,
632 this method provides a simple backward compatibility
633 layer for the Node attributes 'abspath', 'labspath',
634 'path', 'tpath', 'suffix' and 'path_elements'. These Node
635 attributes used to be directly available in v2.3 and earlier, but
636 have been replaced by getter methods that initialize the
637 single variables lazily when required, in order to save memory.
638 The redirection to the getters lets older Tools and
639 SConstruct continue to work without any additional changes,
640 fully transparent to the user.
641 Note, that __getattr__ is only called as fallback when the
642 requested attribute can't be found, so there should be no
643 speed performance penalty involved for standard builds.
644 """
645 if attr in node_bwcomp:
646 return node_bwcomp[attr](self)
647
648 raise AttributeError("%r object has no attribute %r" %
649 (self.__class__, attr))
650
658
660 """ less than operator used by sorting on py3"""
661 return str(self) < str(other)
662
663 @SCons.Memoize.CountMethodCall
672
697
698 rstr = __str__
699
700 @SCons.Memoize.CountMethodCall
713
716
719
721 st = self.stat()
722 if st:
723 return st[stat.ST_MTIME]
724 else:
725 return None
726
728 st = self.stat()
729 if st:
730 return st[stat.ST_SIZE]
731 else:
732 return None
733
735 st = self.stat()
736 return st is not None and stat.S_ISDIR(st[stat.ST_MODE])
737
739 st = self.stat()
740 return st is not None and stat.S_ISREG(st[stat.ST_MODE])
741
742 if hasattr(os, 'symlink'):
747 else:
750
756
759
771
773 """Return path relative to the current working directory of the
774 Node.FS.Base object that owns us."""
775 if not dir:
776 dir = self.fs.getcwd()
777 if self == dir:
778 return '.'
779 path_elems = self.get_path_elements()
780 pathname = ''
781 try: i = path_elems.index(dir)
782 except ValueError:
783 for p in path_elems[:-1]:
784 pathname += p.dirname
785 else:
786 for p in path_elems[i+1:-1]:
787 pathname += p.dirname
788 return pathname + path_elems[-1].name
789
795
797 """Fetch the source code builder for this node.
798
799 If there isn't one, we cache the source code builder specified
800 for the directory (which in turn will cache the value from its
801 parent directory, and so on up to the file system root).
802 """
803 try:
804 scb = self.sbuilder
805 except AttributeError:
806 scb = self.dir.src_builder()
807 self.sbuilder = scb
808 return scb
809
813
817
823
829
832
834
835
836
837 return self.name
838
840 try:
841 return self._proxy
842 except AttributeError:
843 ret = EntryProxy(self)
844 self._proxy = ret
845 return ret
846
848 """
849
850 Generates a target entry that corresponds to this entry (usually
851 a source file) with the specified prefix and suffix.
852
853 Note that this method can be overridden dynamically for generated
854 files that need different behavior. See Tool/swig.py for
855 an example.
856 """
857 return SCons.Node._target_from_source_map[self._func_target_from_source](self, prefix, suffix, splitext)
858
861
862 @SCons.Memoize.CountDictCall(_Rfindalldirs_key)
864 """
865 Return all of the directories for a given path list, including
866 corresponding "backing" directories in any repositories.
867
868 The Node lookups are relative to this Node (typically a
869 directory), so memoizing result saves cycles from looking
870 up the same path for each target in a given directory.
871 """
872 try:
873 memo_dict = self._memo['Rfindalldirs']
874 except KeyError:
875 memo_dict = {}
876 self._memo['Rfindalldirs'] = memo_dict
877 else:
878 try:
879 return memo_dict[pathlist]
880 except KeyError:
881 pass
882
883 create_dir_relative_to_self = self.Dir
884 result = []
885 for path in pathlist:
886 if isinstance(path, SCons.Node.Node):
887 result.append(path)
888 else:
889 dir = create_dir_relative_to_self(path)
890 result.extend(dir.get_all_rdirs())
891
892 memo_dict[pathlist] = result
893
894 return result
895
896 - def RDirs(self, pathlist):
897 """Search for a list of directories in the Repository list."""
898 cwd = self.cwd or self.fs._cwd
899 return cwd.Rfindalldirs(pathlist)
900
901 @SCons.Memoize.CountMethodCall
903 try:
904 return self._memo['rentry']
905 except KeyError:
906 pass
907 result = self
908 if not self.exists():
909 norm_name = _my_normcase(self.name)
910 for dir in self.dir.get_all_rdirs():
911 try:
912 node = dir.entries[norm_name]
913 except KeyError:
914 if dir.entry_exists_on_disk(self.name):
915 result = dir.Entry(self.name)
916 break
917 self._memo['rentry'] = result
918 return result
919
920 - def _glob1(self, pattern, ondisk=True, source=False, strings=False):
922
923
924
925
926
927 node_bwcomp = {'abspath' : Base.get_abspath,
928 'labspath' : Base.get_labspath,
929 'path' : Base.get_internal_path,
930 'tpath' : Base.get_tpath,
931 'path_elements' : Base.get_path_elements,
932 'suffix' : Base.get_suffix}
933
934 -class Entry(Base):
935 """This is the class for generic Node.FS entries--that is, things
936 that could be a File or a Dir, but we're just not sure yet.
937 Consequently, the methods in this class really exist just to
938 transform their associated object into the right class when the
939 time comes, and then call the same-named method in the transformed
940 class."""
941
942 __slots__ = ['scanner_paths',
943 'cachedir_csig',
944 'cachesig',
945 'repositories',
946 'srcdir',
947 'entries',
948 'searched',
949 '_sconsign',
950 'variant_dirs',
951 'root',
952 'dirname',
953 'on_disk_entries',
954 'released_target_info',
955 'contentsig']
956
957 - def __init__(self, name, directory, fs):
958 Base.__init__(self, name, directory, fs)
959 self._func_exists = 3
960 self._func_get_contents = 1
961
962 - def diskcheck_match(self):
964
965 - def disambiguate(self, must_exist=None):
966 """
967 """
968 if self.isdir():
969 self.__class__ = Dir
970 self._morph()
971 elif self.isfile():
972 self.__class__ = File
973 self._morph()
974 self.clear()
975 else:
976
977
978
979
980
981
982
983
984
985 srcdir = self.dir.srcnode()
986 if srcdir != self.dir and \
987 srcdir.entry_exists_on_disk(self.name) and \
988 self.srcnode().isdir():
989 self.__class__ = Dir
990 self._morph()
991 elif must_exist:
992 msg = "No such file or directory: '%s'" % self.get_abspath()
993 raise SCons.Errors.UserError(msg)
994 else:
995 self.__class__ = File
996 self._morph()
997 self.clear()
998 return self
999
1001 """We're a generic Entry, but the caller is actually looking for
1002 a File at this point, so morph into one."""
1003 self.__class__ = File
1004 self._morph()
1005 self.clear()
1006 return File.rfile(self)
1007
1008 - def scanner_key(self):
1009 return self.get_suffix()
1010
1011 - def get_contents(self):
1012 """Fetch the contents of the entry. Returns the exact binary
1013 contents of the file."""
1014 return SCons.Node._get_contents_map[self._func_get_contents](self)
1015
1017 """Fetch the decoded text contents of a Unicode encoded Entry.
1018
1019 Since this should return the text contents from the file
1020 system, we check to see into what sort of subclass we should
1021 morph this Entry."""
1022 try:
1023 self = self.disambiguate(must_exist=1)
1024 except SCons.Errors.UserError:
1025
1026
1027
1028
1029
1030 return ''
1031 else:
1032 return self.get_text_contents()
1033
1034 - def must_be_same(self, klass):
1035 """Called to make sure a Node is a Dir. Since we're an
1036 Entry, we can morph into one."""
1037 if self.__class__ is not klass:
1038 self.__class__ = klass
1039 self._morph()
1040 self.clear()
1041
1042
1043
1044
1045
1046
1047
1048
1049
1050
1051
1052
1055
1056 - def rel_path(self, other):
1057 d = self.disambiguate()
1058 if d.__class__ is Entry:
1059 raise Exception("rel_path() could not disambiguate File/Dir")
1060 return d.rel_path(other)
1061
1062 - def new_ninfo(self):
1063 return self.disambiguate().new_ninfo()
1064
1065 - def _glob1(self, pattern, ondisk=True, source=False, strings=False):
1066 return self.disambiguate()._glob1(pattern, ondisk, source, strings)
1067
1068 - def get_subst_proxy(self):
1070
1071
1072
1073 _classEntry = Entry
1077 """
1078 This class implements an abstraction layer for operations involving
1079 a local file system. Essentially, this wraps any function in
1080 the os, os.path or shutil modules that we use to actually go do
1081 anything with or to the local file system.
1082
1083 Note that there's a very good chance we'll refactor this part of
1084 the architecture in some way as we really implement the interface(s)
1085 for remote file system Nodes. For example, the right architecture
1086 might be to have this be a subclass instead of a base class.
1087 Nevertheless, we're using this as a first step in that direction.
1088
1089 We're not using chdir() yet because the calling subclass method
1090 needs to use os.chdir() directly to avoid recursion. Will we
1091 really need this one?
1092 """
1093
1094
1095 - def chmod(self, path, mode):
1097 - def copy(self, src, dst):
1098 return shutil.copy(src, dst)
1099 - def copy2(self, src, dst):
1100 return shutil.copy2(src, dst)
1111 - def link(self, src, dst):
1112 return os.link(src, dst)
1122 return os.rename(old, new)
1123 - def stat(self, path):
1127 - def open(self, path):
1131
1132 if hasattr(os, 'symlink'):
1135 else:
1138
1139 if hasattr(os, 'readlink'):
1142 else:
1145
1146
1147 -class FS(LocalFS):
1148
1150 """Initialize the Node.FS subsystem.
1151
1152 The supplied path is the top of the source tree, where we
1153 expect to find the top-level build file. If no path is
1154 supplied, the current directory is the default.
1155
1156 The path argument must be a valid absolute path.
1157 """
1158 if SCons.Debug.track_instances: logInstanceCreation(self, 'Node.FS')
1159
1160 self._memo = {}
1161
1162 self.Root = {}
1163 self.SConstruct_dir = None
1164 self.max_drift = default_max_drift
1165
1166 self.Top = None
1167 if path is None:
1168 self.pathTop = os.getcwd()
1169 else:
1170 self.pathTop = path
1171 self.defaultDrive = _my_normcase(_my_splitdrive(self.pathTop)[0])
1172
1173 self.Top = self.Dir(self.pathTop)
1174 self.Top._path = '.'
1175 self.Top._tpath = '.'
1176 self._cwd = self.Top
1177
1178 DirNodeInfo.fs = self
1179 FileNodeInfo.fs = self
1180
1182 self.SConstruct_dir = dir
1183
1185 return self.max_drift
1186
1188 self.max_drift = max_drift
1189
1191 if hasattr(self, "_cwd"):
1192 return self._cwd
1193 else:
1194 return "<no cwd>"
1195
1196 - def chdir(self, dir, change_os_dir=0):
1197 """Change the current working directory for lookups.
1198 If change_os_dir is true, we will also change the "real" cwd
1199 to match.
1200 """
1201 curr=self._cwd
1202 try:
1203 if dir is not None:
1204 self._cwd = dir
1205 if change_os_dir:
1206 os.chdir(dir.get_abspath())
1207 except OSError:
1208 self._cwd = curr
1209 raise
1210
1212 """
1213 Returns the root directory for the specified drive, creating
1214 it if necessary.
1215 """
1216 drive = _my_normcase(drive)
1217 try:
1218 return self.Root[drive]
1219 except KeyError:
1220 root = RootDir(drive, self)
1221 self.Root[drive] = root
1222 if not drive:
1223 self.Root[self.defaultDrive] = root
1224 elif drive == self.defaultDrive:
1225 self.Root[''] = root
1226 return root
1227
1228 - def _lookup(self, p, directory, fsclass, create=1):
1229 """
1230 The generic entry point for Node lookup with user-supplied data.
1231
1232 This translates arbitrary input into a canonical Node.FS object
1233 of the specified fsclass. The general approach for strings is
1234 to turn it into a fully normalized absolute path and then call
1235 the root directory's lookup_abs() method for the heavy lifting.
1236
1237 If the path name begins with '#', it is unconditionally
1238 interpreted relative to the top-level directory of this FS. '#'
1239 is treated as a synonym for the top-level SConstruct directory,
1240 much like '~' is treated as a synonym for the user's home
1241 directory in a UNIX shell. So both '#foo' and '#/foo' refer
1242 to the 'foo' subdirectory underneath the top-level SConstruct
1243 directory.
1244
1245 If the path name is relative, then the path is looked up relative
1246 to the specified directory, or the current directory (self._cwd,
1247 typically the SConscript directory) if the specified directory
1248 is None.
1249 """
1250 if isinstance(p, Base):
1251
1252
1253 p.must_be_same(fsclass)
1254 return p
1255
1256 p = str(p)
1257
1258 if not os_sep_is_slash:
1259 p = p.replace(OS_SEP, '/')
1260
1261 if p[0:1] == '#':
1262
1263
1264
1265 p = p[1:]
1266 directory = self.Top
1267
1268
1269
1270
1271
1272
1273
1274
1275
1276
1277
1278 if do_splitdrive:
1279 drive, p = _my_splitdrive(p)
1280 if drive:
1281 root = self.get_root(drive)
1282 else:
1283 root = directory.root
1284 else:
1285 root = directory.root
1286
1287
1288
1289 p = p.strip('/')
1290
1291 needs_normpath = needs_normpath_match(p)
1292
1293
1294 if p in ('', '.'):
1295 p = directory.get_labspath()
1296 else:
1297 p = directory.get_labspath() + '/' + p
1298 else:
1299 if do_splitdrive:
1300 drive, p = _my_splitdrive(p)
1301 if drive and not p:
1302
1303
1304
1305 p = '/'
1306 else:
1307 drive = ''
1308
1309
1310
1311 if p != '/':
1312 p = p.rstrip('/')
1313
1314 needs_normpath = needs_normpath_match(p)
1315
1316 if p[0:1] == '/':
1317
1318 root = self.get_root(drive)
1319 else:
1320
1321
1322
1323
1324 if directory:
1325 if not isinstance(directory, Dir):
1326 directory = self.Dir(directory)
1327 else:
1328 directory = self._cwd
1329
1330 if p in ('', '.'):
1331 p = directory.get_labspath()
1332 else:
1333 p = directory.get_labspath() + '/' + p
1334
1335 if drive:
1336 root = self.get_root(drive)
1337 else:
1338 root = directory.root
1339
1340 if needs_normpath is not None:
1341
1342
1343
1344
1345
1346
1347
1348
1349
1350 ins = p.split('/')[1:]
1351 outs = []
1352 for d in ins:
1353 if d == '..':
1354 try:
1355 outs.pop()
1356 except IndexError:
1357 pass
1358 elif d not in ('', '.'):
1359 outs.append(d)
1360 p = '/' + '/'.join(outs)
1361
1362 return root._lookup_abs(p, fsclass, create)
1363
1364 - def Entry(self, name, directory = None, create = 1):
1365 """Look up or create a generic Entry node with the specified name.
1366 If the name is a relative path (begins with ./, ../, or a file
1367 name), then it is looked up relative to the supplied directory
1368 node, or to the top level directory of the FS (supplied at
1369 construction time) if no directory is supplied.
1370 """
1371 return self._lookup(name, directory, Entry, create)
1372
1373 - def File(self, name, directory = None, create = 1):
1374 """Look up or create a File node with the specified name. If
1375 the name is a relative path (begins with ./, ../, or a file name),
1376 then it is looked up relative to the supplied directory node,
1377 or to the top level directory of the FS (supplied at construction
1378 time) if no directory is supplied.
1379
1380 This method will raise TypeError if a directory is found at the
1381 specified path.
1382 """
1383 return self._lookup(name, directory, File, create)
1384
1385 - def Dir(self, name, directory = None, create = True):
1386 """Look up or create a Dir node with the specified name. If
1387 the name is a relative path (begins with ./, ../, or a file name),
1388 then it is looked up relative to the supplied directory node,
1389 or to the top level directory of the FS (supplied at construction
1390 time) if no directory is supplied.
1391
1392 This method will raise TypeError if a normal file is found at the
1393 specified path.
1394 """
1395 return self._lookup(name, directory, Dir, create)
1396
1397 - def VariantDir(self, variant_dir, src_dir, duplicate=1):
1398 """Link the supplied variant directory to the source directory
1399 for purposes of building files."""
1400
1401 if not isinstance(src_dir, SCons.Node.Node):
1402 src_dir = self.Dir(src_dir)
1403 if not isinstance(variant_dir, SCons.Node.Node):
1404 variant_dir = self.Dir(variant_dir)
1405 if src_dir.is_under(variant_dir):
1406 raise SCons.Errors.UserError("Source directory cannot be under variant directory.")
1407 if variant_dir.srcdir:
1408 if variant_dir.srcdir == src_dir:
1409 return
1410 raise SCons.Errors.UserError("'%s' already has a source directory: '%s'."%(variant_dir, variant_dir.srcdir))
1411 variant_dir.link(src_dir, duplicate)
1412
1419
1421 """Locate the directory of a given python module name
1422
1423 For example scons might resolve to
1424 Windows: C:\Python27\Lib\site-packages\scons-2.5.1
1425 Linux: /usr/lib/scons
1426
1427 This can be useful when we want to determine a toolpath based on a python module name"""
1428
1429 dirpath = ''
1430 if sys.version_info[0] < 3 or (sys.version_info[0] == 3 and sys.version_info[1] in (0,1,2,3,4)):
1431
1432 import imp
1433 splitname = modulename.split('.')
1434 srchpths = sys.path
1435 for item in splitname:
1436 file, path, desc = imp.find_module(item, srchpths)
1437 if file is not None:
1438 path = os.path.dirname(path)
1439 srchpths = [path]
1440 dirpath = path
1441 else:
1442
1443 import importlib.util
1444 modspec = importlib.util.find_spec(modulename)
1445 dirpath = os.path.dirname(modspec.origin)
1446 return self._lookup(dirpath, None, Dir, True)
1447
1448
1450 """Create targets in corresponding variant directories
1451
1452 Climb the directory tree, and look up path names
1453 relative to any linked variant directories we find.
1454
1455 Even though this loops and walks up the tree, we don't memoize
1456 the return value because this is really only used to process
1457 the command-line targets.
1458 """
1459 targets = []
1460 message = None
1461 fmt = "building associated VariantDir targets: %s"
1462 start_dir = dir
1463 while dir:
1464 for bd in dir.variant_dirs:
1465 if start_dir.is_under(bd):
1466
1467 return [orig], fmt % str(orig)
1468 p = os.path.join(bd._path, *tail)
1469 targets.append(self.Entry(p))
1470 tail = [dir.name] + tail
1471 dir = dir.up()
1472 if targets:
1473 message = fmt % ' '.join(map(str, targets))
1474 return targets, message
1475
1476 - def Glob(self, pathname, ondisk=True, source=True, strings=False, exclude=None, cwd=None):
1477 """
1478 Globs
1479
1480 This is mainly a shim layer
1481 """
1482 if cwd is None:
1483 cwd = self.getcwd()
1484 return cwd.glob(pathname, ondisk, source, strings, exclude)
1485
1503
1507
1508 glob_magic_check = re.compile('[*?[]')
1512
1514 """A class for directories in a file system.
1515 """
1516
1517 __slots__ = ['scanner_paths',
1518 'cachedir_csig',
1519 'cachesig',
1520 'repositories',
1521 'srcdir',
1522 'entries',
1523 'searched',
1524 '_sconsign',
1525 'variant_dirs',
1526 'root',
1527 'dirname',
1528 'on_disk_entries',
1529 'released_target_info',
1530 'contentsig']
1531
1532 NodeInfo = DirNodeInfo
1533 BuildInfo = DirBuildInfo
1534
1535 - def __init__(self, name, directory, fs):
1539
1605
1609
1611 """Called when we change the repository(ies) for a directory.
1612 This clears any cached information that is invalidated by changing
1613 the repository."""
1614
1615 for node in list(self.entries.values()):
1616 if node != self.dir:
1617 if node != self and isinstance(node, Dir):
1618 node.__clearRepositoryCache(duplicate)
1619 else:
1620 node.clear()
1621 try:
1622 del node._srcreps
1623 except AttributeError:
1624 pass
1625 if duplicate is not None:
1626 node.duplicate=duplicate
1627
1631
1632 - def Entry(self, name):
1633 """
1634 Looks up or creates an entry node named 'name' relative to
1635 this directory.
1636 """
1637 return self.fs.Entry(name, self)
1638
1639 - def Dir(self, name, create=True):
1640 """
1641 Looks up or creates a directory node named 'name' relative to
1642 this directory.
1643 """
1644 return self.fs.Dir(name, self, create)
1645
1646 - def File(self, name):
1647 """
1648 Looks up or creates a file node named 'name' relative to
1649 this directory.
1650 """
1651 return self.fs.File(name, self)
1652
1653 - def link(self, srcdir, duplicate):
1660
1667
1668 @SCons.Memoize.CountMethodCall
1690
1696
1699
1702
1703 @SCons.Memoize.CountDictCall(_rel_path_key)
1705 """Return a path to "other" relative to this directory.
1706 """
1707
1708
1709
1710
1711
1712
1713
1714
1715
1716
1717
1718 try:
1719 memo_dict = self._memo['rel_path']
1720 except KeyError:
1721 memo_dict = {}
1722 self._memo['rel_path'] = memo_dict
1723 else:
1724 try:
1725 return memo_dict[other]
1726 except KeyError:
1727 pass
1728
1729 if self is other:
1730 result = '.'
1731
1732 elif not other in self._path_elements:
1733 try:
1734 other_dir = other.get_dir()
1735 except AttributeError:
1736 result = str(other)
1737 else:
1738 if other_dir is None:
1739 result = other.name
1740 else:
1741 dir_rel_path = self.rel_path(other_dir)
1742 if dir_rel_path == '.':
1743 result = other.name
1744 else:
1745 result = dir_rel_path + OS_SEP + other.name
1746 else:
1747 i = self._path_elements.index(other) + 1
1748
1749 path_elems = ['..'] * (len(self._path_elements) - i) \
1750 + [n.name for n in other._path_elements[i:]]
1751
1752 result = OS_SEP.join(path_elems)
1753
1754 memo_dict[other] = result
1755
1756 return result
1757
1761
1765
1767 """Return this directory's implicit dependencies.
1768
1769 We don't bother caching the results because the scan typically
1770 shouldn't be requested more than once (as opposed to scanning
1771 .h file contents, which can be requested as many times as the
1772 files is #included by other files).
1773 """
1774 if not scanner:
1775 return []
1776
1777
1778
1779
1780
1781
1782
1783
1784 self.clear()
1785 return scanner(self, env, path)
1786
1787
1788
1789
1790
1793
1799
1800
1801
1802
1803
1805 """Create this directory, silently and without worrying about
1806 whether the builder is the default or not."""
1807 listDirs = []
1808 parent = self
1809 while parent:
1810 if parent.exists():
1811 break
1812 listDirs.append(parent)
1813 p = parent.up()
1814 if p is None:
1815
1816
1817 raise SCons.Errors.StopError(parent._path)
1818 parent = p
1819 listDirs.reverse()
1820 for dirnode in listDirs:
1821 try:
1822
1823
1824
1825
1826 SCons.Node.Node.build(dirnode)
1827 dirnode.get_executor().nullify()
1828
1829
1830
1831
1832 dirnode.clear()
1833 except OSError:
1834 pass
1835
1839
1841 """Return any corresponding targets in a variant directory.
1842 """
1843 return self.fs.variant_dir_target_climb(self, self, [])
1844
1846 """A directory does not get scanned."""
1847 return None
1848
1850 """We already emit things in text, so just return the binary
1851 version."""
1852 return self.get_contents()
1853
1854 - def get_contents(self):
1855 """Return content signatures and names of all our children
1856 separated by new-lines. Ensure that the nodes are sorted."""
1857 return SCons.Node._get_contents_map[self._func_get_contents](self)
1858
1860 """Compute the content signature for Directory nodes. In
1861 general, this is not needed and the content signature is not
1862 stored in the DirNodeInfo. However, if get_contents on a Dir
1863 node is called which has a child directory, the child
1864 directory should return the hash of its contents."""
1865 contents = self.get_contents()
1866 return SCons.Util.MD5signature(contents)
1867
1870
1881
1892
1896
1898 """Dir has a special need for srcnode()...if we
1899 have a srcdir attribute set, then that *is* our srcnode."""
1900 if self.srcdir:
1901 return self.srcdir
1902 return Base.srcnode(self)
1903
1905 """Return the latest timestamp from among our children"""
1906 stamp = 0
1907 for kid in self.children():
1908 if kid.get_timestamp() > stamp:
1909 stamp = kid.get_timestamp()
1910 return stamp
1911
1913 """Get the absolute path of the file."""
1914 return self._abspath
1915
1917 """Get the absolute path of the file."""
1918 return self._labspath
1919
1922
1925
1928
1929 - def entry_abspath(self, name):
1930 return self._abspath + OS_SEP + name
1931
1932 - def entry_labspath(self, name):
1933 return self._labspath + '/' + name
1934
1935 - def entry_path(self, name):
1936 return self._path + OS_SEP + name
1937
1938 - def entry_tpath(self, name):
1939 return self._tpath + OS_SEP + name
1940
1941 - def entry_exists_on_disk(self, name):
1942 """ Searches through the file/dir entries of the current
1943 directory, and returns True if a physical entry with the given
1944 name could be found.
1945
1946 @see rentry_exists_on_disk
1947 """
1948 try:
1949 d = self.on_disk_entries
1950 except AttributeError:
1951 d = {}
1952 try:
1953 entries = os.listdir(self._abspath)
1954 except OSError:
1955 pass
1956 else:
1957 for entry in map(_my_normcase, entries):
1958 d[entry] = True
1959 self.on_disk_entries = d
1960 if sys.platform == 'win32' or sys.platform == 'cygwin':
1961 name = _my_normcase(name)
1962 result = d.get(name)
1963 if result is None:
1964
1965
1966 result = os.path.exists(self._abspath + OS_SEP + name)
1967 d[name] = result
1968 return result
1969 else:
1970 return name in d
1971
1972 - def rentry_exists_on_disk(self, name):
1973 """ Searches through the file/dir entries of the current
1974 *and* all its remote directories (repos), and returns
1975 True if a physical entry with the given name could be found.
1976 The local directory (self) gets searched first, so
1977 repositories take a lower precedence regarding the
1978 searching order.
1979
1980 @see entry_exists_on_disk
1981 """
1982
1983 rentry_exists = self.entry_exists_on_disk(name)
1984 if not rentry_exists:
1985
1986 norm_name = _my_normcase(name)
1987 for rdir in self.get_all_rdirs():
1988 try:
1989 node = rdir.entries[norm_name]
1990 if node:
1991 rentry_exists = True
1992 break
1993 except KeyError:
1994 if rdir.entry_exists_on_disk(name):
1995 rentry_exists = True
1996 break
1997 return rentry_exists
1998
1999 @SCons.Memoize.CountMethodCall
2019
2036
2039
2040 @SCons.Memoize.CountDictCall(_srcdir_find_file_key)
2042 try:
2043 memo_dict = self._memo['srcdir_find_file']
2044 except KeyError:
2045 memo_dict = {}
2046 self._memo['srcdir_find_file'] = memo_dict
2047 else:
2048 try:
2049 return memo_dict[filename]
2050 except KeyError:
2051 pass
2052
2053 def func(node):
2054 if (isinstance(node, File) or isinstance(node, Entry)) and \
2055 (node.is_derived() or node.exists()):
2056 return node
2057 return None
2058
2059 norm_name = _my_normcase(filename)
2060
2061 for rdir in self.get_all_rdirs():
2062 try: node = rdir.entries[norm_name]
2063 except KeyError: node = rdir.file_on_disk(filename)
2064 else: node = func(node)
2065 if node:
2066 result = (node, self)
2067 memo_dict[filename] = result
2068 return result
2069
2070 for srcdir in self.srcdir_list():
2071 for rdir in srcdir.get_all_rdirs():
2072 try: node = rdir.entries[norm_name]
2073 except KeyError: node = rdir.file_on_disk(filename)
2074 else: node = func(node)
2075 if node:
2076 result = (File(filename, self, self.fs), srcdir)
2077 memo_dict[filename] = result
2078 return result
2079
2080 result = (None, None)
2081 memo_dict[filename] = result
2082 return result
2083
2092
2101
2102 - def walk(self, func, arg):
2103 """
2104 Walk this directory tree by calling the specified function
2105 for each directory in the tree.
2106
2107 This behaves like the os.path.walk() function, but for in-memory
2108 Node.FS.Dir objects. The function takes the same arguments as
2109 the functions passed to os.path.walk():
2110
2111 func(arg, dirname, fnames)
2112
2113 Except that "dirname" will actually be the directory *Node*,
2114 not the string. The '.' and '..' entries are excluded from
2115 fnames. The fnames list may be modified in-place to filter the
2116 subdirectories visited or otherwise impose a specific order.
2117 The "arg" argument is always passed to func() and may be used
2118 in any way (or ignored, passing None is common).
2119 """
2120 entries = self.entries
2121 names = list(entries.keys())
2122 names.remove('.')
2123 names.remove('..')
2124 func(arg, self, names)
2125 for dirname in [n for n in names if isinstance(entries[n], Dir)]:
2126 entries[dirname].walk(func, arg)
2127
2128 - def glob(self, pathname, ondisk=True, source=False, strings=False, exclude=None):
2129 """
2130 Returns a list of Nodes (or strings) matching a specified
2131 pathname pattern.
2132
2133 Pathname patterns follow UNIX shell semantics: * matches
2134 any-length strings of any characters, ? matches any character,
2135 and [] can enclose lists or ranges of characters. Matches do
2136 not span directory separators.
2137
2138 The matches take into account Repositories, returning local
2139 Nodes if a corresponding entry exists in a Repository (either
2140 an in-memory Node or something on disk).
2141
2142 By defafult, the glob() function matches entries that exist
2143 on-disk, in addition to in-memory Nodes. Setting the "ondisk"
2144 argument to False (or some other non-true value) causes the glob()
2145 function to only match in-memory Nodes. The default behavior is
2146 to return both the on-disk and in-memory Nodes.
2147
2148 The "source" argument, when true, specifies that corresponding
2149 source Nodes must be returned if you're globbing in a build
2150 directory (initialized with VariantDir()). The default behavior
2151 is to return Nodes local to the VariantDir().
2152
2153 The "strings" argument, when true, returns the matches as strings,
2154 not Nodes. The strings are path names relative to this directory.
2155
2156 The "exclude" argument, if not None, must be a pattern or a list
2157 of patterns following the same UNIX shell semantics.
2158 Elements matching a least one pattern of this list will be excluded
2159 from the result.
2160
2161 The underlying algorithm is adapted from the glob.glob() function
2162 in the Python library (but heavily modified), and uses fnmatch()
2163 under the covers.
2164 """
2165 dirname, basename = os.path.split(pathname)
2166 if not dirname:
2167 result = self._glob1(basename, ondisk, source, strings)
2168 else:
2169 if has_glob_magic(dirname):
2170 list = self.glob(dirname, ondisk, source, False, exclude)
2171 else:
2172 list = [self.Dir(dirname, create=True)]
2173 result = []
2174 for dir in list:
2175 r = dir._glob1(basename, ondisk, source, strings)
2176 if strings:
2177 r = [os.path.join(str(dir), x) for x in r]
2178 result.extend(r)
2179 if exclude:
2180 excludes = []
2181 excludeList = SCons.Util.flatten(exclude)
2182 for x in excludeList:
2183 r = self.glob(x, ondisk, source, strings)
2184 excludes.extend(r)
2185 result = [x for x in result if not any(fnmatch.fnmatch(str(x), str(e)) for e in SCons.Util.flatten(excludes))]
2186 return sorted(result, key=lambda a: str(a))
2187
2188 - def _glob1(self, pattern, ondisk=True, source=False, strings=False):
2189 """
2190 Globs for and returns a list of entry names matching a single
2191 pattern in this directory.
2192
2193 This searches any repositories and source directories for
2194 corresponding entries and returns a Node (or string) relative
2195 to the current directory if an entry is found anywhere.
2196
2197 TODO: handle pattern with no wildcard
2198 """
2199 search_dir_list = self.get_all_rdirs()
2200 for srcdir in self.srcdir_list():
2201 search_dir_list.extend(srcdir.get_all_rdirs())
2202
2203 selfEntry = self.Entry
2204 names = []
2205 for dir in search_dir_list:
2206
2207
2208
2209 node_names = [ v.name for k, v in dir.entries.items()
2210 if k not in ('.', '..') ]
2211 names.extend(node_names)
2212 if not strings:
2213
2214
2215 for name in node_names: selfEntry(name)
2216 if ondisk:
2217 try:
2218 disk_names = os.listdir(dir._abspath)
2219 except os.error:
2220 continue
2221 names.extend(disk_names)
2222 if not strings:
2223
2224
2225
2226
2227
2228
2229
2230
2231 if pattern[0] != '.':
2232 disk_names = [x for x in disk_names if x[0] != '.']
2233 disk_names = fnmatch.filter(disk_names, pattern)
2234 dirEntry = dir.Entry
2235 for name in disk_names:
2236
2237
2238 name = './' + name
2239 node = dirEntry(name).disambiguate()
2240 n = selfEntry(name)
2241 if n.__class__ != node.__class__:
2242 n.__class__ = node.__class__
2243 n._morph()
2244
2245 names = set(names)
2246 if pattern[0] != '.':
2247 names = [x for x in names if x[0] != '.']
2248 names = fnmatch.filter(names, pattern)
2249
2250 if strings:
2251 return names
2252
2253 return [self.entries[_my_normcase(n)] for n in names]
2254
2256 """A class for the root directory of a file system.
2257
2258 This is the same as a Dir class, except that the path separator
2259 ('/' or '\\') is actually part of the name, so we don't need to
2260 add a separator when creating the path names of entries within
2261 this directory.
2262 """
2263
2264 __slots__ = ['_lookupDict']
2265
2319
2361
2362
2367
2369 """
2370 Fast (?) lookup of a *normalized* absolute path.
2371
2372 This method is intended for use by internal lookups with
2373 already-normalized path data. For general-purpose lookups,
2374 use the FS.Entry(), FS.Dir() or FS.File() methods.
2375
2376 The caller is responsible for making sure we're passed a
2377 normalized absolute path; we merely let Python's dictionary look
2378 up and return the One True Node.FS object for the path.
2379
2380 If a Node for the specified "p" doesn't already exist, and
2381 "create" is specified, the Node may be created after recursive
2382 invocation to find or create the parent directory or directories.
2383 """
2384 k = _my_normcase(p)
2385 try:
2386 result = self._lookupDict[k]
2387 except KeyError:
2388 if not create:
2389 msg = "No such file or directory: '%s' in '%s' (and create is False)" % (p, str(self))
2390 raise SCons.Errors.UserError(msg)
2391
2392
2393 dir_name, file_name = p.rsplit('/',1)
2394 dir_node = self._lookup_abs(dir_name, Dir)
2395 result = klass(file_name, dir_node, self.fs)
2396
2397
2398
2399 result.diskcheck_match()
2400
2401 self._lookupDict[k] = result
2402 dir_node.entries[_my_normcase(file_name)] = result
2403 dir_node.implicit = None
2404 else:
2405
2406
2407 result.must_be_same(klass)
2408 return result
2409
2412
2413 - def entry_abspath(self, name):
2414 return self._abspath + name
2415
2416 - def entry_labspath(self, name):
2418
2419 - def entry_path(self, name):
2420 return self._path + name
2421
2422 - def entry_tpath(self, name):
2423 return self._tpath + name
2424
2426 if self is dir:
2427 return 1
2428 else:
2429 return 0
2430
2433
2436
2439
2442 __slots__ = ('csig', 'timestamp', 'size')
2443 current_version_id = 2
2444
2445 field_list = ['csig', 'timestamp', 'size']
2446
2447
2448 fs = None
2449
2460
2462 """
2463 Return all fields that shall be pickled. Walk the slots in the class
2464 hierarchy and add those to the state dictionary. If a '__dict__' slot is
2465 available, copy all entries to the dictionary. Also include the version
2466 id, which is fixed for all instances of a class.
2467 """
2468 state = getattr(self, '__dict__', {}).copy()
2469 for obj in type(self).mro():
2470 for name in getattr(obj,'__slots__',()):
2471 if hasattr(self, name):
2472 state[name] = getattr(self, name)
2473
2474 state['_version_id'] = self.current_version_id
2475 try:
2476 del state['__weakref__']
2477 except KeyError:
2478 pass
2479
2480 return state
2481
2483 """
2484 Restore the attributes from a pickled state.
2485 """
2486
2487 del state['_version_id']
2488 for key, value in state.items():
2489 if key not in ('__weakref__',):
2490 setattr(self, key, value)
2491
2494
2496 return not self.__eq__(other)
2497
2500 """
2501 This is info loaded from sconsign.
2502
2503 Attributes unique to FileBuildInfo:
2504 dependency_map : Caches file->csig mapping
2505 for all dependencies. Currently this is only used when using
2506 MD5-timestamp decider.
2507 It's used to ensure that we copy the correct
2508 csig from previous build to be written to .sconsign when current build
2509 is done. Previously the matching of csig to file was strictly by order
2510 they appeared in bdepends, bsources, or bimplicit, and so a change in order
2511 or count of any of these could yield writing wrong csig, and then false positive
2512 rebuilds
2513 """
2514 __slots__ = ('dependency_map')
2515 current_version_id = 2
2516
2527
2529 """
2530 Converts this FileBuildInfo object for writing to a .sconsign file
2531
2532 This replaces each Node in our various dependency lists with its
2533 usual string representation: relative to the top-level SConstruct
2534 directory, or an absolute path if it's outside.
2535 """
2536 if os_sep_is_slash:
2537 node_to_str = str
2538 else:
2539 def node_to_str(n):
2540 try:
2541 s = n.get_internal_path()
2542 except AttributeError:
2543 s = str(n)
2544 else:
2545 s = s.replace(OS_SEP, '/')
2546 return s
2547 for attr in ['bsources', 'bdepends', 'bimplicit']:
2548 try:
2549 val = getattr(self, attr)
2550 except AttributeError:
2551 pass
2552 else:
2553 setattr(self, attr, list(map(node_to_str, val)))
2554
2556 """
2557 Converts a newly-read FileBuildInfo object for in-SCons use
2558
2559 For normal up-to-date checking, we don't have any conversion to
2560 perform--but we're leaving this method here to make that clear.
2561 """
2562 pass
2563
2565 """
2566 Prepares a FileBuildInfo object for explaining what changed
2567
2568 The bsources, bdepends and bimplicit lists have all been
2569 stored on disk as paths relative to the top-level SConstruct
2570 directory. Convert the strings to actual Nodes (for use by the
2571 --debug=explain code and --implicit-cache).
2572 """
2573 attrs = [
2574 ('bsources', 'bsourcesigs'),
2575 ('bdepends', 'bdependsigs'),
2576 ('bimplicit', 'bimplicitsigs'),
2577 ]
2578 for (nattr, sattr) in attrs:
2579 try:
2580 strings = getattr(self, nattr)
2581 nodeinfos = getattr(self, sattr)
2582 except AttributeError:
2583 continue
2584 if strings is None or nodeinfos is None:
2585 continue
2586 nodes = []
2587 for s, ni in zip(strings, nodeinfos):
2588 if not isinstance(s, SCons.Node.Node):
2589 s = ni.str_to_node(s)
2590 nodes.append(s)
2591 setattr(self, nattr, nodes)
2592
2604
2605
2606 -class File(Base):
2607 """A class for files in a file system.
2608 """
2609
2610 __slots__ = ['scanner_paths',
2611 'cachedir_csig',
2612 'cachesig',
2613 'repositories',
2614 'srcdir',
2615 'entries',
2616 'searched',
2617 '_sconsign',
2618 'variant_dirs',
2619 'root',
2620 'dirname',
2621 'on_disk_entries',
2622 'released_target_info',
2623 'contentsig']
2624
2625 NodeInfo = FileNodeInfo
2626 BuildInfo = FileBuildInfo
2627
2628 md5_chunksize = 64
2629
2633
2634 - def __init__(self, name, directory, fs):
2638
2639 - def Entry(self, name):
2640 """Create an entry node named 'name' relative to
2641 the directory of this file."""
2642 return self.dir.Entry(name)
2643
2644 - def Dir(self, name, create=True):
2645 """Create a directory node named 'name' relative to
2646 the directory of this file."""
2647 return self.dir.Dir(name, create=create)
2648
2649 - def Dirs(self, pathlist):
2650 """Create a list of directories relative to the SConscript
2651 directory of this file."""
2652 return [self.Dir(p) for p in pathlist]
2653
2654 - def File(self, name):
2655 """Create a file node named 'name' relative to
2656 the directory of this file."""
2657 return self.dir.File(name)
2658
2687
2690
2691 - def get_contents(self):
2693
2695 """
2696 This attempts to figure out what the encoding of the text is
2697 based upon the BOM bytes, and then decodes the contents so that
2698 it's a valid python string.
2699 """
2700 contents = self.get_contents()
2701
2702
2703
2704
2705
2706
2707 if contents[:len(codecs.BOM_UTF8)] == codecs.BOM_UTF8:
2708 return contents[len(codecs.BOM_UTF8):].decode('utf-8')
2709 if contents[:len(codecs.BOM_UTF16_LE)] == codecs.BOM_UTF16_LE:
2710 return contents[len(codecs.BOM_UTF16_LE):].decode('utf-16-le')
2711 if contents[:len(codecs.BOM_UTF16_BE)] == codecs.BOM_UTF16_BE:
2712 return contents[len(codecs.BOM_UTF16_BE):].decode('utf-16-be')
2713 try:
2714 return contents.decode('utf-8')
2715 except UnicodeDecodeError as e:
2716 try:
2717 return contents.decode('latin-1')
2718 except UnicodeDecodeError as e:
2719 return contents.decode('utf-8', error='backslashreplace')
2720
2721
2722 - def get_content_hash(self):
2723 """
2724 Compute and return the MD5 hash for this file.
2725 """
2726 if not self.rexists():
2727 return SCons.Util.MD5signature('')
2728 fname = self.rfile().get_abspath()
2729 try:
2730 cs = SCons.Util.MD5filesignature(fname,
2731 chunksize=SCons.Node.FS.File.md5_chunksize*1024)
2732 except EnvironmentError as e:
2733 if not e.filename:
2734 e.filename = fname
2735 raise
2736 return cs
2737
2738 @SCons.Memoize.CountMethodCall
2740 try:
2741 return self._memo['get_size']
2742 except KeyError:
2743 pass
2744
2745 if self.rexists():
2746 size = self.rfile().getsize()
2747 else:
2748 size = 0
2749
2750 self._memo['get_size'] = size
2751
2752 return size
2753
2754 @SCons.Memoize.CountMethodCall
2769
2770 convert_copy_attrs = [
2771 'bsources',
2772 'bimplicit',
2773 'bdepends',
2774 'bact',
2775 'bactsig',
2776 'ninfo',
2777 ]
2778
2779
2780 convert_sig_attrs = [
2781 'bsourcesigs',
2782 'bimplicitsigs',
2783 'bdependsigs',
2784 ]
2785
2786 - def convert_old_entry(self, old_entry):
2787
2788
2789
2790
2791
2792
2793
2794
2795
2796
2797
2798
2799
2800
2801
2802
2803
2804
2805
2806
2807
2808
2809
2810
2811
2812
2813
2814
2815
2816
2817
2818
2819
2820
2821
2822
2823
2824
2825
2826
2827
2828
2829
2830
2831
2832
2833
2834
2835
2836
2837
2838
2839
2840
2841
2842
2843
2844
2845
2846
2847
2848
2849
2850
2851
2852
2853
2854 import SCons.SConsign
2855 new_entry = SCons.SConsign.SConsignEntry()
2856 new_entry.binfo = self.new_binfo()
2857 binfo = new_entry.binfo
2858 for attr in self.convert_copy_attrs:
2859 try:
2860 value = getattr(old_entry, attr)
2861 except AttributeError:
2862 continue
2863 setattr(binfo, attr, value)
2864 delattr(old_entry, attr)
2865 for attr in self.convert_sig_attrs:
2866 try:
2867 sig_list = getattr(old_entry, attr)
2868 except AttributeError:
2869 continue
2870 value = []
2871 for sig in sig_list:
2872 ninfo = self.new_ninfo()
2873 if len(sig) == 32:
2874 ninfo.csig = sig
2875 else:
2876 ninfo.timestamp = sig
2877 value.append(ninfo)
2878 setattr(binfo, attr, value)
2879 delattr(old_entry, attr)
2880 return new_entry
2881
2882 @SCons.Memoize.CountMethodCall
2909
2915
2918
2920 return (id(env), id(scanner), path)
2921
2922 @SCons.Memoize.CountDictCall(_get_found_includes_key)
2924 """Return the included implicit dependencies in this file.
2925 Cache results so we only scan the file once per path
2926 regardless of how many times this information is requested.
2927 """
2928 memo_key = (id(env), id(scanner), path)
2929 try:
2930 memo_dict = self._memo['get_found_includes']
2931 except KeyError:
2932 memo_dict = {}
2933 self._memo['get_found_includes'] = memo_dict
2934 else:
2935 try:
2936 return memo_dict[memo_key]
2937 except KeyError:
2938 pass
2939
2940 if scanner:
2941 result = [n.disambiguate() for n in scanner(self, env, path)]
2942 else:
2943 result = []
2944
2945 memo_dict[memo_key] = result
2946
2947 return result
2948
2953
2969
2971 """Try to retrieve the node's content from a cache
2972
2973 This method is called from multiple threads in a parallel build,
2974 so only do thread safe stuff here. Do thread unsafe stuff in
2975 built().
2976
2977 Returns true if the node was successfully retrieved.
2978 """
2979 if self.nocache:
2980 return None
2981 if not self.is_derived():
2982 return None
2983 return self.get_build_env().get_CacheDir().retrieve(self)
2984
3007
3009 """Called just after this node has been marked
3010 up-to-date or was built completely.
3011
3012 This is where we try to release as many target node infos
3013 as possible for clean builds and update runs, in order
3014 to minimize the overall memory consumption.
3015
3016 We'd like to remove a lot more attributes like self.sources
3017 and self.sources_set, but they might get used
3018 in a next build step. For example, during configuration
3019 the source files for a built E{*}.o file are used to figure out
3020 which linker to use for the resulting Program (gcc vs. g++)!
3021 That's why we check for the 'keep_targetinfo' attribute,
3022 config Nodes and the Interactive mode just don't allow
3023 an early release of most variables.
3024
3025 In the same manner, we can't simply remove the self.attributes
3026 here. The smart linking relies on the shared flag, and some
3027 parts of the java Tool use it to transport information
3028 about nodes...
3029
3030 @see: built() and Node.release_target_info()
3031 """
3032 if (self.released_target_info or SCons.Node.interactive):
3033 return
3034
3035 if not hasattr(self.attributes, 'keep_targetinfo'):
3036
3037
3038 self.changed(allowcache=True)
3039 self.get_contents_sig()
3040 self.get_build_env()
3041
3042 self.executor = None
3043 self._memo.pop('rfile', None)
3044 self.prerequisites = None
3045
3046 if not len(self.ignore_set):
3047 self.ignore_set = None
3048 if not len(self.implicit_set):
3049 self.implicit_set = None
3050 if not len(self.depends_set):
3051 self.depends_set = None
3052 if not len(self.ignore):
3053 self.ignore = None
3054 if not len(self.depends):
3055 self.depends = None
3056
3057
3058 self.released_target_info = True
3059
3061 if self.rexists():
3062 return None
3063 scb = self.dir.src_builder()
3064 if scb is _null:
3065 scb = None
3066 if scb is not None:
3067 try:
3068 b = self.builder
3069 except AttributeError:
3070 b = None
3071 if b is None:
3072 self.builder_set(scb)
3073 return scb
3074
3076 """Return whether this Node has a source builder or not.
3077
3078 If this Node doesn't have an explicit source code builder, this
3079 is where we figure out, on the fly, if there's a transparent
3080 source code builder for it.
3081
3082 Note that if we found a source builder, we also set the
3083 self.builder attribute, so that all of the methods that actually
3084 *build* this file don't have to do anything different.
3085 """
3086 try:
3087 scb = self.sbuilder
3088 except AttributeError:
3089 scb = self.sbuilder = self.find_src_builder()
3090 return scb is not None
3091
3098
3106
3107
3108
3109
3110
3114
3128
3129
3130
3131
3132
3139
3154
3155 @SCons.Memoize.CountMethodCall
3164
3165
3166
3167
3168
3170 """
3171 Returns the content signature currently stored for this node
3172 if it's been unmodified longer than the max_drift value, or the
3173 max_drift value is 0. Returns None otherwise.
3174 """
3175 old = self.get_stored_info()
3176 mtime = self.get_timestamp()
3177
3178 max_drift = self.fs.max_drift
3179 if max_drift > 0:
3180 if (time.time() - mtime) > max_drift:
3181 try:
3182 n = old.ninfo
3183 if n.timestamp and n.csig and n.timestamp == mtime:
3184 return n.csig
3185 except AttributeError:
3186 pass
3187 elif max_drift == 0:
3188 try:
3189 return old.ninfo.csig
3190 except AttributeError:
3191 pass
3192
3193 return None
3194
3231
3232
3233
3234
3235
3239
3263
3264 - def changed(self, node=None, allowcache=False):
3265 """
3266 Returns if the node is up-to-date with respect to the BuildInfo
3267 stored last time it was built.
3268
3269 For File nodes this is basically a wrapper around Node.changed(),
3270 but we allow the return value to get cached after the reference
3271 to the Executor got released in release_target_info().
3272
3273 @see: Node.changed()
3274 """
3275 if node is None:
3276 try:
3277 return self._memo['changed']
3278 except KeyError:
3279 pass
3280
3281 has_changed = SCons.Node.Node.changed(self, node)
3282 if allowcache:
3283 self._memo['changed'] = has_changed
3284 return has_changed
3285
3286 - def changed_content(self, target, prev_ni):
3287 cur_csig = self.get_csig()
3288 try:
3289 return cur_csig != prev_ni.csig
3290 except AttributeError:
3291 return 1
3292
3295
3296
3297
3298 __dmap_cache = {}
3299 __dmap_sig_cache = {}
3300
3301
3326
3328 """
3329 Return a list of corresponding csigs from previous
3330 build in order of the node/files in children.
3331
3332 Args:
3333 self - self
3334 dmap - Dictionary of file -> csig
3335
3336 Returns:
3337 List of csigs for provided list of children
3338 """
3339 prev = []
3340
3341
3342 if len(dmap) == 0:
3343 if MD5_TIMESTAMP_DEBUG: print("Nothing dmap shortcutting")
3344 return None
3345
3346 if MD5_TIMESTAMP_DEBUG: print("len(dmap):%d"%len(dmap))
3347
3348 c_str = str(self)
3349 if MD5_TIMESTAMP_DEBUG: print("Checking :%s"%c_str)
3350 df = dmap.get(c_str, None)
3351 if df:
3352 return df
3353
3354 if os.altsep:
3355 c_str = c_str.replace(os.sep, os.altsep)
3356 df = dmap.get(c_str, None)
3357 if MD5_TIMESTAMP_DEBUG: print("-->%s"%df)
3358 if df:
3359 return df
3360
3361 if not df:
3362 try:
3363
3364 c_str = self.get_path()
3365 df = dmap.get(c_str, None)
3366 if MD5_TIMESTAMP_DEBUG: print("-->%s"%df)
3367 if df:
3368 return df
3369
3370 if os.altsep:
3371 c_str = c_str.replace(os.sep, os.altsep)
3372 df = dmap.get(c_str, None)
3373 if MD5_TIMESTAMP_DEBUG: print("-->%s"%df)
3374 if df:
3375 return df
3376
3377 except AttributeError as e:
3378 raise FileBuildInfoFileToCsigMappingError("No mapping from file name to content signature for :%s"%c_str)
3379
3380 return df
3381
3382 - def changed_timestamp_then_content(self, target, prev_ni, node=None):
3383 """
3384 Used when decider for file is Timestamp-MD5
3385
3386 NOTE: If the timestamp hasn't changed this will skip md5'ing the
3387 file and just copy the prev_ni provided. If the prev_ni
3388 is wrong. It will propagate it.
3389 See: https://github.com/SCons/scons/issues/2980
3390
3391 Args:
3392 self - dependency
3393 target - target
3394 prev_ni - The NodeInfo object loaded from previous builds .sconsign
3395 node - Node instance. This is the only changed* function which requires
3396 node to function. So if we detect that it's not passed.
3397 we throw DeciderNeedsNode, and caller should handle this and pass node.
3398
3399 Returns:
3400 Boolean - Indicates if node(File) has changed.
3401 """
3402 if node is None:
3403
3404 raise DeciderNeedsNode(self.changed_timestamp_then_content)
3405
3406
3407 bi = node.get_stored_info().binfo
3408 rebuilt = False
3409 try:
3410 dependency_map = bi.dependency_map
3411 except AttributeError as e:
3412 dependency_map = self._build_dependency_map(bi)
3413 rebuilt = True
3414
3415 if len(dependency_map) == 0:
3416
3417
3418
3419 if MD5_TIMESTAMP_DEBUG: print("Skipping checks len(dmap)=0")
3420
3421
3422
3423 self.get_csig()
3424 return True
3425
3426 new_prev_ni = self._get_previous_signatures(dependency_map)
3427 new = self.changed_timestamp_match(target, new_prev_ni)
3428
3429 if MD5_TIMESTAMP_DEBUG:
3430 old = self.changed_timestamp_match(target, prev_ni)
3431
3432 if old != new:
3433 print("Mismatch self.changed_timestamp_match(%s, prev_ni) old:%s new:%s"%(str(target), old, new))
3434 new_prev_ni = self._get_previous_signatures(dependency_map)
3435
3436
3437 if not new:
3438 try:
3439
3440 self.get_ninfo().csig = new_prev_ni.csig
3441 except AttributeError:
3442 pass
3443 return False
3444 return self.changed_content(target, new_prev_ni)
3445
3451
3453 """
3454 Return True if the timestamps don't match or if there is no previous timestamp
3455 :param target:
3456 :param prev_ni: Information about the node from the previous build
3457 :return:
3458 """
3459 try:
3460 return self.get_timestamp() != prev_ni.timestamp
3461 except AttributeError:
3462 return 1
3463
3493
3494 @SCons.Memoize.CountMethodCall
3529
3531 """
3532 For this node, find if there exists a corresponding file in one or more repositories
3533 :return: list of corresponding files in repositories
3534 """
3535 retvals = []
3536
3537 norm_name = _my_normcase(self.name)
3538 for repo_dir in self.dir.get_all_rdirs():
3539 try:
3540 node = repo_dir.entries[norm_name]
3541 except KeyError:
3542 node = repo_dir.file_on_disk(self.name)
3543
3544 if node and node.exists() and \
3545 (isinstance(node, File) or isinstance(node, Entry) \
3546 or not node.is_derived()):
3547 retvals.append(node)
3548
3549 return retvals
3550
3551
3553 return str(self.rfile())
3554
3556 """
3557 Fetch a Node's content signature for purposes of computing
3558 another Node's cachesig.
3559
3560 This is a wrapper around the normal get_csig() method that handles
3561 the somewhat obscure case of using CacheDir with the -n option.
3562 Any files that don't exist would normally be "built" by fetching
3563 them from the cache, but the normal get_csig() method will try
3564 to open up the local file, which doesn't exist because the -n
3565 option meant we didn't actually pull the file from cachedir.
3566 But since the file *does* actually exist in the cachedir, we
3567 can use its contents for the csig.
3568 """
3569 try:
3570 return self.cachedir_csig
3571 except AttributeError:
3572 pass
3573
3574 cachedir, cachefile = self.get_build_env().get_CacheDir().cachepath(self)
3575 if not self.exists() and cachefile and os.path.exists(cachefile):
3576 self.cachedir_csig = SCons.Util.MD5filesignature(cachefile, \
3577 SCons.Node.FS.File.md5_chunksize * 1024)
3578 else:
3579 self.cachedir_csig = self.get_csig()
3580 return self.cachedir_csig
3581
3582 - def get_contents_sig(self):
3583 """
3584 A helper method for get_cachedir_bsig.
3585
3586 It computes and returns the signature for this
3587 node's contents.
3588 """
3589
3590 try:
3591 return self.contentsig
3592 except AttributeError:
3593 pass
3594
3595 executor = self.get_executor()
3596
3597 result = self.contentsig = SCons.Util.MD5signature(executor.get_contents())
3598 return result
3599
3601 """
3602 Return the signature for a cached file, including
3603 its children.
3604
3605 It adds the path of the cached file to the cache signature,
3606 because multiple targets built by the same action will all
3607 have the same build signature, and we have to differentiate
3608 them somehow.
3609
3610 Signature should normally be string of hex digits.
3611 """
3612 try:
3613 return self.cachesig
3614 except AttributeError:
3615 pass
3616
3617
3618 children = self.children()
3619 sigs = [n.get_cachedir_csig() for n in children]
3620
3621
3622 sigs.append(self.get_contents_sig())
3623
3624
3625 sigs.append(self.get_internal_path())
3626
3627
3628 result = self.cachesig = SCons.Util.MD5collect(sigs)
3629 return result
3630
3631 default_fs = None
3638
3640 """
3641 """
3642
3645
3647 """
3648 A helper method for find_file() that looks up a directory for
3649 a file we're trying to find. This only creates the Dir Node if
3650 it exists on-disk, since if the directory doesn't exist we know
3651 we won't find any files in it... :-)
3652
3653 It would be more compact to just use this as a nested function
3654 with a default keyword argument (see the commented-out version
3655 below), but that doesn't work unless you have nested scopes,
3656 so we define it here just so this work under Python 1.5.2.
3657 """
3658 if fd is None:
3659 fd = self.default_filedir
3660 dir, name = os.path.split(fd)
3661 drive, d = _my_splitdrive(dir)
3662 if not name and d[:1] in ('/', OS_SEP):
3663
3664 return p.fs.get_root(drive)
3665 if dir:
3666 p = self.filedir_lookup(p, dir)
3667 if not p:
3668 return None
3669 norm_name = _my_normcase(name)
3670 try:
3671 node = p.entries[norm_name]
3672 except KeyError:
3673 return p.dir_on_disk(name)
3674 if isinstance(node, Dir):
3675 return node
3676 if isinstance(node, Entry):
3677 node.must_be_same(Dir)
3678 return node
3679 return None
3680
3682 return (filename, paths)
3683
3684 @SCons.Memoize.CountDictCall(_find_file_key)
3685 - def find_file(self, filename, paths, verbose=None):
3686 """
3687 Find a node corresponding to either a derived file or a file that exists already.
3688
3689 Only the first file found is returned, and none is returned if no file is found.
3690
3691 filename: A filename to find
3692 paths: A list of directory path *nodes* to search in. Can be represented as a list, a tuple, or a callable that is called with no arguments and returns the list or tuple.
3693
3694 returns The node created from the found file.
3695
3696 """
3697 memo_key = self._find_file_key(filename, paths)
3698 try:
3699 memo_dict = self._memo['find_file']
3700 except KeyError:
3701 memo_dict = {}
3702 self._memo['find_file'] = memo_dict
3703 else:
3704 try:
3705 return memo_dict[memo_key]
3706 except KeyError:
3707 pass
3708
3709 if verbose and not callable(verbose):
3710 if not SCons.Util.is_String(verbose):
3711 verbose = "find_file"
3712 _verbose = u' %s: ' % verbose
3713 verbose = lambda s: sys.stdout.write(_verbose + s)
3714
3715 filedir, filename = os.path.split(filename)
3716 if filedir:
3717 self.default_filedir = filedir
3718 paths = [_f for _f in map(self.filedir_lookup, paths) if _f]
3719
3720 result = None
3721 for dir in paths:
3722 if verbose:
3723 verbose("looking for '%s' in '%s' ...\n" % (filename, dir))
3724 node, d = dir.srcdir_find_file(filename)
3725 if node:
3726 if verbose:
3727 verbose("... FOUND '%s' in '%s'\n" % (filename, d))
3728 result = node
3729 break
3730
3731 memo_dict[memo_key] = result
3732
3733 return result
3734
3735 find_file = FileFinder().find_file
3739 """
3740 Invalidate the memoized values of all Nodes (files or directories)
3741 that are associated with the given entries. Has been added to
3742 clear the cache of nodes affected by a direct execution of an
3743 action (e.g. Delete/Copy/Chmod). Existing Node caches become
3744 inconsistent if the action is run through Execute(). The argument
3745 `targets` can be a single Node object or filename, or a sequence
3746 of Nodes/filenames.
3747 """
3748 from traceback import extract_stack
3749
3750
3751
3752
3753
3754
3755 for f in extract_stack():
3756 if f[2] == 'Execute' and f[0][-14:] == 'Environment.py':
3757 break
3758 else:
3759
3760 return
3761
3762 if not SCons.Util.is_List(targets):
3763 targets = [targets]
3764
3765 for entry in targets:
3766
3767
3768 try:
3769 entry.clear_memoized_values()
3770 except AttributeError:
3771
3772
3773
3774 node = get_default_fs().Entry(entry)
3775 if node:
3776 node.clear_memoized_values()
3777
3778
3779
3780
3781
3782
3783