Clean up and simplify logic. TODO: scons -c isn't removing the .di files. Mainly...
[scons.git] / SCons / Defaults.py
bloba680cb45b7d92f906bc3b5d222bc3b4796ff2690
1 # MIT License
3 # Copyright The SCons Foundation
5 # Permission is hereby granted, free of charge, to any person obtaining
6 # a copy of this software and associated documentation files (the
7 # "Software"), to deal in the Software without restriction, including
8 # without limitation the rights to use, copy, modify, merge, publish,
9 # distribute, sublicense, and/or sell copies of the Software, and to
10 # permit persons to whom the Software is furnished to do so, subject to
11 # the following conditions:
13 # The above copyright notice and this permission notice shall be included
14 # in all copies or substantial portions of the Software.
16 # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY
17 # KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE
18 # WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
19 # NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
20 # LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
21 # OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
22 # WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
25 """Builders and other things for the local site.
27 Here's where we'll duplicate the functionality of autoconf until we
28 move it into the installation procedure or use something like qmconf.
30 The code that reads the registry to find MSVC components was borrowed
31 from distutils.msvccompiler.
32 """
34 import os
35 import shutil
36 import stat
37 import sys
38 import time
39 from typing import List
41 import SCons.Action
42 import SCons.Builder
43 import SCons.CacheDir
44 import SCons.Environment
45 import SCons.Errors
46 import SCons.PathList
47 import SCons.Scanner.Dir
48 import SCons.Subst
49 import SCons.Tool
50 from SCons.Util import is_List, is_String, is_Sequence, is_Tuple, is_Dict, flatten
52 # A placeholder for a default Environment (for fetching source files
53 # from source code management systems and the like). This must be
54 # initialized later, after the top-level directory is set by the calling
55 # interface.
56 _default_env = None
59 # Lazily instantiate the default environment so the overhead of creating
60 # it doesn't apply when it's not needed.
61 def _fetch_DefaultEnvironment(*args, **kw):
62 """Returns the already-created default construction environment."""
63 global _default_env
64 return _default_env
67 def DefaultEnvironment(*args, **kw):
68 """
69 Initial public entry point for creating the default construction
70 Environment.
72 After creating the environment, we overwrite our name
73 (DefaultEnvironment) with the _fetch_DefaultEnvironment() function,
74 which more efficiently returns the initialized default construction
75 environment without checking for its existence.
77 (This function still exists with its _default_check because someone
78 else (*cough* Script/__init__.py *cough*) may keep a reference
79 to this function. So we can't use the fully functional idiom of
80 having the name originally be a something that *only* creates the
81 construction environment and then overwrites the name.)
82 """
83 global _default_env
84 if not _default_env:
85 _default_env = SCons.Environment.Environment(*args, **kw)
86 _default_env.Decider('content')
87 global DefaultEnvironment
88 DefaultEnvironment = _fetch_DefaultEnvironment
89 _default_env._CacheDir_path = None
90 return _default_env
93 # Emitters for setting the shared attribute on object files,
94 # and an action for checking that all of the source files
95 # going into a shared library are, in fact, shared.
96 def StaticObjectEmitter(target, source, env):
97 for tgt in target:
98 tgt.attributes.shared = None
99 return target, source
102 def SharedObjectEmitter(target, source, env):
103 for tgt in target:
104 tgt.attributes.shared = 1
105 return target, source
108 def SharedFlagChecker(source, target, env):
109 same = env.subst('$STATIC_AND_SHARED_OBJECTS_ARE_THE_SAME')
110 if same == '0' or same == '' or same == 'False':
111 for src in source:
112 try:
113 shared = src.attributes.shared
114 except AttributeError:
115 shared = None
116 if not shared:
117 raise SCons.Errors.UserError(
118 "Source file: %s is static and is not compatible with shared target: %s" % (src, target[0]))
121 SharedCheck = SCons.Action.Action(SharedFlagChecker, None)
123 # Some people were using these variable name before we made
124 # SourceFileScanner part of the public interface. Don't break their
125 # SConscript files until we've given them some fair warning and a
126 # transition period.
127 CScan = SCons.Tool.CScanner
128 DScan = SCons.Tool.DScanner
129 LaTeXScan = SCons.Tool.LaTeXScanner
130 ObjSourceScan = SCons.Tool.SourceFileScanner
131 ProgScan = SCons.Tool.ProgramScanner
133 # These aren't really tool scanners, so they don't quite belong with
134 # the rest of those in Tool/__init__.py, but I'm not sure where else
135 # they should go. Leave them here for now.
137 DirScanner = SCons.Scanner.Dir.DirScanner()
138 DirEntryScanner = SCons.Scanner.Dir.DirEntryScanner()
140 # Actions for common languages.
141 CAction = SCons.Action.Action("$CCCOM", "$CCCOMSTR")
142 ShCAction = SCons.Action.Action("$SHCCCOM", "$SHCCCOMSTR")
143 CXXAction = SCons.Action.Action("$CXXCOM", "$CXXCOMSTR")
144 ShCXXAction = SCons.Action.Action("$SHCXXCOM", "$SHCXXCOMSTR")
146 DAction = SCons.Action.Action("$DCOM", "$DCOMSTR")
147 ShDAction = SCons.Action.Action("$SHDCOM", "$SHDCOMSTR")
149 ASAction = SCons.Action.Action("$ASCOM", "$ASCOMSTR")
150 ASPPAction = SCons.Action.Action("$ASPPCOM", "$ASPPCOMSTR")
152 LinkAction = SCons.Action.Action("$LINKCOM", "$LINKCOMSTR")
153 ShLinkAction = SCons.Action.Action("$SHLINKCOM", "$SHLINKCOMSTR")
155 LdModuleLinkAction = SCons.Action.Action("$LDMODULECOM", "$LDMODULECOMSTR")
157 # Common tasks that we allow users to perform in platform-independent
158 # ways by creating ActionFactory instances.
159 ActionFactory = SCons.Action.ActionFactory
162 def get_paths_str(dest) -> str:
163 """Generates a string from *dest* for use in a strfunction.
165 If *dest* is a list, manually converts each elem to a string.
167 def quote(arg) -> str:
168 return f'"{arg}"'
170 if is_List(dest):
171 elem_strs = [quote(d) for d in dest]
172 return f'[{", ".join(elem_strs)}]'
173 else:
174 return quote(dest)
177 permission_dic = {
178 'u': {
179 'r': stat.S_IRUSR,
180 'w': stat.S_IWUSR,
181 'x': stat.S_IXUSR
183 'g': {
184 'r': stat.S_IRGRP,
185 'w': stat.S_IWGRP,
186 'x': stat.S_IXGRP
188 'o': {
189 'r': stat.S_IROTH,
190 'w': stat.S_IWOTH,
191 'x': stat.S_IXOTH
196 def chmod_func(dest, mode) -> None:
197 """Implementation of the Chmod action function.
199 *mode* can be either an integer (normally expressed in octal mode,
200 as in 0o755) or a string following the syntax of the POSIX chmod
201 command (for example "ugo+w"). The latter must be converted, since
202 the underlying Python only takes the numeric form.
204 from string import digits
205 SCons.Node.FS.invalidate_node_memos(dest)
206 if not is_List(dest):
207 dest = [dest]
208 if is_String(mode) and 0 not in [i in digits for i in mode]:
209 mode = int(mode, 8)
210 if not is_String(mode):
211 for element in dest:
212 os.chmod(str(element), mode)
213 else:
214 mode = str(mode)
215 for operation in mode.split(","):
216 if "=" in operation:
217 operator = "="
218 elif "+" in operation:
219 operator = "+"
220 elif "-" in operation:
221 operator = "-"
222 else:
223 raise SyntaxError("Could not find +, - or =")
224 operation_list = operation.split(operator)
225 if len(operation_list) != 2:
226 raise SyntaxError("More than one operator found")
227 user = operation_list[0].strip().replace("a", "ugo")
228 permission = operation_list[1].strip()
229 new_perm = 0
230 for u in user:
231 for p in permission:
232 try:
233 new_perm = new_perm | permission_dic[u][p]
234 except KeyError:
235 raise SyntaxError("Unrecognized user or permission format")
236 for element in dest:
237 curr_perm = os.stat(str(element)).st_mode
238 if operator == "=":
239 os.chmod(str(element), new_perm)
240 elif operator == "+":
241 os.chmod(str(element), curr_perm | new_perm)
242 elif operator == "-":
243 os.chmod(str(element), curr_perm & ~new_perm)
246 def chmod_strfunc(dest, mode) -> str:
247 """strfunction for the Chmod action function."""
248 if not is_String(mode):
249 return f'Chmod({get_paths_str(dest)}, {mode:#o})'
250 else:
251 return f'Chmod({get_paths_str(dest)}, "{mode}")'
255 Chmod = ActionFactory(chmod_func, chmod_strfunc)
259 def copy_func(dest, src, symlinks: bool=True) -> int:
260 """Implementation of the Copy action function.
262 Copies *src* to *dest*. If *src* is a list, *dest* must be
263 a directory, or not exist (will be created).
265 Since Python :mod:`shutil` methods, which know nothing about
266 SCons Nodes, will be called to perform the actual copying,
267 args are converted to strings first.
269 If *symlinks* evaluates true, then a symbolic link will be
270 shallow copied and recreated as a symbolic link; otherwise, copying
271 a symbolic link will be equivalent to copying the symbolic link's
272 final target regardless of symbolic link depth.
275 dest = str(dest)
276 src = [str(n) for n in src] if is_List(src) else str(src)
278 SCons.Node.FS.invalidate_node_memos(dest)
279 if is_List(src):
280 # this fails only if dest exists and is not a dir
281 try:
282 os.makedirs(dest, exist_ok=True)
283 except FileExistsError:
284 raise SCons.Errors.BuildError(
285 errstr=(
286 'Error: Copy() called with a list of sources, '
287 'which requires target to be a directory, '
288 f'but "{dest}" is not a directory.'
291 for file in src:
292 shutil.copy2(file, dest)
293 return 0
295 elif os.path.islink(src):
296 if symlinks:
297 os.symlink(os.readlink(src), dest)
298 return 0
300 return copy_func(dest, os.path.realpath(src))
302 elif os.path.isfile(src):
303 shutil.copy2(src, dest)
304 return 0
306 else:
307 shutil.copytree(src, dest, symlinks)
308 return 0
311 def copy_strfunc(dest, src, symlinks: bool=True) -> str:
312 """strfunction for the Copy action function."""
313 return f'Copy({get_paths_str(dest)}, {get_paths_str(src)})'
316 Copy = ActionFactory(copy_func, copy_strfunc)
319 def delete_func(dest, must_exist: bool=False) -> None:
320 """Implementation of the Delete action function.
322 Lets the Python :func:`os.unlink` raise an error if *dest* does not exist,
323 unless *must_exist* evaluates false (the default).
325 SCons.Node.FS.invalidate_node_memos(dest)
326 if not is_List(dest):
327 dest = [dest]
328 for entry in dest:
329 entry = str(entry)
330 # os.path.exists returns False with broken links that exist
331 entry_exists = os.path.exists(entry) or os.path.islink(entry)
332 if not entry_exists and not must_exist:
333 continue
334 # os.path.isdir returns True when entry is a link to a dir
335 if os.path.isdir(entry) and not os.path.islink(entry):
336 shutil.rmtree(entry, True)
337 continue
338 os.unlink(entry)
341 def delete_strfunc(dest, must_exist: bool=False) -> str:
342 """strfunction for the Delete action function."""
343 return f'Delete({get_paths_str(dest)})'
346 Delete = ActionFactory(delete_func, delete_strfunc)
349 def mkdir_func(dest) -> None:
350 """Implementation of the Mkdir action function."""
351 SCons.Node.FS.invalidate_node_memos(dest)
352 if not is_List(dest):
353 dest = [dest]
354 for entry in dest:
355 os.makedirs(str(entry), exist_ok=True)
358 Mkdir = ActionFactory(mkdir_func, lambda _dir: f'Mkdir({get_paths_str(_dir)})')
361 def move_func(dest, src) -> None:
362 """Implementation of the Move action function."""
363 SCons.Node.FS.invalidate_node_memos(dest)
364 SCons.Node.FS.invalidate_node_memos(src)
365 shutil.move(src, dest)
368 Move = ActionFactory(
369 move_func, lambda dest, src: f'Move("{dest}", "{src}")', convert=str
373 def touch_func(dest) -> None:
374 """Implementation of the Touch action function."""
375 SCons.Node.FS.invalidate_node_memos(dest)
376 if not is_List(dest):
377 dest = [dest]
378 for file in dest:
379 file = str(file)
380 mtime = int(time.time())
381 if os.path.exists(file):
382 atime = os.path.getatime(file)
383 else:
384 with open(file, 'w'):
385 atime = mtime
386 os.utime(file, (atime, mtime))
389 Touch = ActionFactory(touch_func, lambda file: f'Touch({get_paths_str(file)})')
392 # Internal utility functions
394 # pylint: disable-msg=too-many-arguments
395 def _concat(prefix, items_iter, suffix, env, f=lambda x: x, target=None, source=None, affect_signature: bool=True):
397 Creates a new list from 'items_iter' by first interpolating each element
398 in the list using the 'env' dictionary and then calling f on the
399 list, and finally calling _concat_ixes to concatenate 'prefix' and
400 'suffix' onto each element of the list.
403 if not items_iter:
404 return items_iter
406 l = f(SCons.PathList.PathList(items_iter).subst_path(env, target, source))
407 if l is not None:
408 items_iter = l
410 if not affect_signature:
411 value = ['$(']
412 else:
413 value = []
414 value += _concat_ixes(prefix, items_iter, suffix, env)
416 if not affect_signature:
417 value += ["$)"]
419 return value
420 # pylint: enable-msg=too-many-arguments
423 def _concat_ixes(prefix, items_iter, suffix, env):
425 Creates a new list from 'items_iter' by concatenating the 'prefix' and
426 'suffix' arguments onto each element of the list. A trailing space
427 on 'prefix' or leading space on 'suffix' will cause them to be put
428 into separate list elements rather than being concatenated.
431 result = []
433 # ensure that prefix and suffix are strings
434 prefix = str(env.subst(prefix, SCons.Subst.SUBST_RAW))
435 suffix = str(env.subst(suffix, SCons.Subst.SUBST_RAW))
437 for x in flatten(items_iter):
438 if isinstance(x, SCons.Node.FS.File):
439 result.append(x)
440 continue
441 x = str(x)
442 if x:
444 if prefix:
445 if prefix[-1] == ' ':
446 result.append(prefix[:-1])
447 elif x[:len(prefix)] != prefix:
448 x = prefix + x
450 result.append(x)
452 if suffix:
453 if suffix[0] == ' ':
454 result.append(suffix[1:])
455 elif x[-len(suffix):] != suffix:
456 result[-1] = result[-1] + suffix
458 return result
461 def _stripixes(prefix, itms, suffix, stripprefixes, stripsuffixes, env, c=None):
463 This is a wrapper around _concat()/_concat_ixes() that checks for
464 the existence of prefixes or suffixes on list items and strips them
465 where it finds them. This is used by tools (like the GNU linker)
466 that need to turn something like 'libfoo.a' into '-lfoo'.
469 if not itms:
470 return itms
472 if not callable(c):
473 env_c = env['_concat']
474 if env_c != _concat and callable(env_c):
475 # There's a custom _concat() method in the construction
476 # environment, and we've allowed people to set that in
477 # the past (see test/custom-concat.py), so preserve the
478 # backwards compatibility.
479 c = env_c
480 else:
481 c = _concat_ixes
483 stripprefixes = list(map(env.subst, flatten(stripprefixes)))
484 stripsuffixes = list(map(env.subst, flatten(stripsuffixes)))
486 stripped = []
487 for l in SCons.PathList.PathList(itms).subst_path(env, None, None):
488 if isinstance(l, SCons.Node.FS.File):
489 stripped.append(l)
490 continue
492 if not is_String(l):
493 l = str(l)
495 for stripprefix in stripprefixes:
496 lsp = len(stripprefix)
497 if l[:lsp] == stripprefix:
498 l = l[lsp:]
499 # Do not strip more than one prefix
500 break
502 for stripsuffix in stripsuffixes:
503 lss = len(stripsuffix)
504 if l[-lss:] == stripsuffix:
505 l = l[:-lss]
506 # Do not strip more than one suffix
507 break
509 stripped.append(l)
511 return c(prefix, stripped, suffix, env)
514 def processDefines(defs) -> List[str]:
515 """Return list of strings for preprocessor defines from *defs*.
517 Resolves the different forms ``CPPDEFINES`` can be assembled in:
518 if the Append/Prepend routines are used beyond a initial setting it
519 will be a deque, but if written to only once (Environment initializer,
520 or direct write) it can be a multitude of types.
522 Any prefix/suffix is handled elsewhere (usually :func:`_concat_ixes`).
524 .. versionchanged:: 4.5.0
525 Bare tuples are now treated the same as tuple-in-sequence, assumed
526 to describe a valued macro. Bare strings are now split on space.
527 A dictionary is no longer sorted before handling.
529 dlist = []
530 if is_List(defs):
531 for define in defs:
532 if define is None:
533 continue
534 elif is_Sequence(define):
535 if len(define) > 2:
536 raise SCons.Errors.UserError(
537 f"Invalid tuple in CPPDEFINES: {define!r}, "
538 "must be a tuple with only two elements"
540 name, *value = define
541 if value and value[0] is not None:
542 # TODO: do we need to quote value if it contains space?
543 dlist.append(f"{name}={value[0]}")
544 else:
545 dlist.append(str(define[0]))
546 elif is_Dict(define):
547 for macro, value in define.items():
548 if value is not None:
549 # TODO: do we need to quote value if it contains space?
550 dlist.append(f"{macro}={value}")
551 else:
552 dlist.append(str(macro))
553 elif is_String(define):
554 dlist.append(str(define))
555 else:
556 raise SCons.Errors.UserError(
557 f"CPPDEFINES entry {define!r} is not a tuple, list, "
558 "dict, string or None."
560 elif is_Tuple(defs):
561 if len(defs) > 2:
562 raise SCons.Errors.UserError(
563 f"Invalid tuple in CPPDEFINES: {defs!r}, "
564 "must be a tuple with only two elements"
566 name, *value = defs
567 if value and value[0] is not None:
568 # TODO: do we need to quote value if it contains space?
569 dlist.append(f"{name}={value[0]}")
570 else:
571 dlist.append(str(define[0]))
572 elif is_Dict(defs):
573 for macro, value in defs.items():
574 if value is None:
575 dlist.append(str(macro))
576 else:
577 dlist.append(f"{macro}={value}")
578 elif is_String(defs):
579 return defs.split()
580 else:
581 dlist.append(str(defs))
583 return dlist
586 def _defines(prefix, defs, suffix, env, target=None, source=None, c=_concat_ixes):
587 """A wrapper around :func:`_concat_ixes` that turns a list or string
588 into a list of C preprocessor command-line definitions.
590 return c(prefix, env.subst_list(processDefines(defs), target=target, source=source), suffix, env)
593 class NullCmdGenerator:
594 """This is a callable class that can be used in place of other
595 command generators if you don't want them to do anything.
597 The __call__ method for this class simply returns the thing
598 you instantiated it with.
600 Example usage:
601 env["DO_NOTHING"] = NullCmdGenerator
602 env["LINKCOM"] = "${DO_NOTHING('$LINK $SOURCES $TARGET')}"
605 def __init__(self, cmd) -> None:
606 self.cmd = cmd
608 def __call__(self, target, source, env, for_signature=None):
609 return self.cmd
612 class Variable_Method_Caller:
613 """A class for finding a construction variable on the stack and
614 calling one of its methods.
616 We use this to support "construction variables" in our string
617 eval()s that actually stand in for methods--specifically, use
618 of "RDirs" in call to _concat that should actually execute the
619 "TARGET.RDirs" method. (We used to support this by creating a little
620 "build dictionary" that mapped RDirs to the method, but this got in
621 the way of Memoizing construction environments, because we had to
622 create new environment objects to hold the variables.)
625 def __init__(self, variable, method) -> None:
626 self.variable = variable
627 self.method = method
629 def __call__(self, *args, **kw):
630 try:
631 1 // 0
632 except ZeroDivisionError:
633 # Don't start iterating with the current stack-frame to
634 # prevent creating reference cycles (f_back is safe).
635 frame = sys.exc_info()[2].tb_frame.f_back
636 variable = self.variable
637 while frame:
638 if variable in frame.f_locals:
639 v = frame.f_locals[variable]
640 if v:
641 method = getattr(v, self.method)
642 return method(*args, **kw)
643 frame = frame.f_back
644 return None
647 def __libversionflags(env, version_var, flags_var):
649 if version_var is not empty, returns env[flags_var], otherwise returns None
650 :param env:
651 :param version_var:
652 :param flags_var:
653 :return:
655 try:
656 if env.subst('$' + version_var):
657 return env[flags_var]
658 except KeyError:
659 pass
660 return None
663 def __lib_either_version_flag(env, version_var1, version_var2, flags_var):
665 if $version_var1 or $version_var2 is not empty, returns env[flags_var], otherwise returns None
666 :param env:
667 :param version_var1:
668 :param version_var2:
669 :param flags_var:
670 :return:
672 try:
673 if env.subst('$' + version_var1) or env.subst('$' + version_var2):
674 return env[flags_var]
675 except KeyError:
676 pass
677 return None
683 ConstructionEnvironment = {
684 'BUILDERS': {},
685 'SCANNERS': [SCons.Tool.SourceFileScanner],
686 'CONFIGUREDIR': '#/.sconf_temp',
687 'CONFIGURELOG': '#/config.log',
688 'CPPSUFFIXES': SCons.Tool.CSuffixes,
689 'DSUFFIXES': SCons.Tool.DSuffixes,
690 'ENV': {},
691 'IDLSUFFIXES': SCons.Tool.IDLSuffixes,
692 '_concat': _concat,
693 '_defines': _defines,
694 '_stripixes': _stripixes,
695 '_LIBFLAGS': '${_concat(LIBLINKPREFIX, LIBS, LIBLINKSUFFIX, __env__)}',
697 '_LIBDIRFLAGS': '${_concat(LIBDIRPREFIX, LIBPATH, LIBDIRSUFFIX, __env__, RDirs, TARGET, SOURCE, affect_signature=False)}',
698 '_CPPINCFLAGS': '${_concat(INCPREFIX, CPPPATH, INCSUFFIX, __env__, RDirs, TARGET, SOURCE, affect_signature=False)}',
700 '_CPPDEFFLAGS': '${_defines(CPPDEFPREFIX, CPPDEFINES, CPPDEFSUFFIX, __env__, TARGET, SOURCE)}',
702 '__libversionflags': __libversionflags,
703 '__SHLIBVERSIONFLAGS': '${__libversionflags(__env__,"SHLIBVERSION","_SHLIBVERSIONFLAGS")}',
704 '__LDMODULEVERSIONFLAGS': '${__libversionflags(__env__,"LDMODULEVERSION","_LDMODULEVERSIONFLAGS")}',
705 '__DSHLIBVERSIONFLAGS': '${__libversionflags(__env__,"DSHLIBVERSION","_DSHLIBVERSIONFLAGS")}',
706 '__lib_either_version_flag': __lib_either_version_flag,
708 'TEMPFILE': NullCmdGenerator,
709 'TEMPFILEARGJOIN': ' ',
710 'TEMPFILEARGESCFUNC': SCons.Subst.quote_spaces,
711 'Dir': Variable_Method_Caller('TARGET', 'Dir'),
712 'Dirs': Variable_Method_Caller('TARGET', 'Dirs'),
713 'File': Variable_Method_Caller('TARGET', 'File'),
714 'RDirs': Variable_Method_Caller('TARGET', 'RDirs'),
717 # Local Variables:
718 # tab-width:4
719 # indent-tabs-mode:nil
720 # End:
721 # vim: set expandtab tabstop=4 shiftwidth=4: