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