Upgrade to SCons v4.9.1

This commit is contained in:
Artem Pavlenko 2025-04-05 09:52:46 +01:00
parent 2525147a68
commit 659bf720d3
1537 changed files with 1484 additions and 849 deletions

2
scons/scons-LICENSE vendored
View File

@ -5,7 +5,7 @@
MIT License
Copyright (c) 2001 - 2024 The SCons Foundation
Copyright (c) 2001 - 2025 The SCons Foundation
Permission is hereby granted, free of charge, to any person obtaining
a copy of this software and associated documentation files (the

View File

@ -32,15 +32,15 @@ The files are split into directories named by the first few
digits of the signature. The prefix length used for directory
names can be changed by this script.
"""
__revision__ = "scripts/scons-configure-cache.py 08661ed4c552323ef3a7f0ff1af38868cbabb05e Tue, 03 Sep 2024 17:46:32 -0700 bdbaddog"
__revision__ = "scripts/scons-configure-cache.py 39a12f34d532ab2493e78a7b73aeab2250852790 Thu, 27 Mar 2025 11:44:24 -0700 bdbaddog"
__version__ = "4.8.1"
__version__ = "4.9.1"
__build__ = "08661ed4c552323ef3a7f0ff1af38868cbabb05e"
__build__ = "39a12f34d532ab2493e78a7b73aeab2250852790"
__buildsys__ = "M1Dog2021"
__date__ = "Tue, 03 Sep 2024 17:46:32 -0700"
__date__ = "Thu, 27 Mar 2025 11:44:24 -0700"
__developer__ = "bdbaddog"
@ -49,9 +49,9 @@ import os
import sys
# python compatibility check
if sys.version_info < (3, 6, 0):
if sys.version_info < (3, 7, 0):
msg = "scons: *** SCons version %s does not run under Python version %s.\n\
Python >= 3.6.0 is required.\n"
Python >= 3.7.0 is required.\n"
sys.stderr.write(msg % (__version__, sys.version.split()[0]))
sys.exit(1)

View File

@ -1,33 +0,0 @@
# SPDX-License-Identifier: MIT
#
# Copyright The SCons Foundation
"""Various SCons type aliases.
For representing complex types across the entire repo without risking
circular dependencies, we take advantage of TYPE_CHECKING to import
modules in an tool-only environment. This allows us to introduce
hinting that resolves as expected in IDEs without clashing at runtime.
For consistency, it's recommended to ALWAYS use these aliases in a
type-hinting context, even if the type is actually expected to be
resolved in a given file.
"""
from typing import Union, TYPE_CHECKING
if TYPE_CHECKING:
import SCons.Executor
# Because we don't have access to TypeAlias until 3.10, we have to utilize
# 'Union' for all aliases. As it expects at least two entries, anything that
# is only represented with a single type needs to list itself twice.
ExecutorType = Union["SCons.Executor.Executor", "SCons.Executor.Executor"]
# Local Variables:
# tab-width:4
# indent-tabs-mode:nil
# End:
# vim: set expandtab tabstop=4 shiftwidth=4:

View File

@ -1,9 +0,0 @@
__version__="4.8.1"
__copyright__="Copyright (c) 2001 - 2024 The SCons Foundation"
__developer__="bdbaddog"
__date__="Tue, 03 Sep 2024 17:46:32 -0700"
__buildsys__="M1Dog2021"
__revision__="08661ed4c552323ef3a7f0ff1af38868cbabb05e"
__build__="08661ed4c552323ef3a7f0ff1af38868cbabb05e"
# make sure compatibility is always in place
import SCons.compat # noqa

View File

@ -100,6 +100,8 @@ way for wrapping up the functions.
"""
from __future__ import annotations
import inspect
import os
import pickle
@ -109,7 +111,7 @@ import sys
from abc import ABC, abstractmethod
from collections import OrderedDict
from subprocess import DEVNULL, PIPE
from typing import List, Optional, Tuple
from typing import TYPE_CHECKING
import SCons.Debug
import SCons.Errors
@ -120,7 +122,9 @@ import SCons.Util
from SCons.Debug import logInstanceCreation
from SCons.Subst import SUBST_CMD, SUBST_RAW, SUBST_SIG
from SCons.Util import is_String, is_List
from SCons.Util.sctyping import ExecutorType
if TYPE_CHECKING:
from SCons.Executor import Executor
class _null:
pass
@ -481,9 +485,7 @@ def _do_create_action(act, kw):
return None
# TODO: from __future__ import annotations once we get to Python 3.7 base,
# to avoid quoting the defined-later classname
def _do_create_list_action(act, kw) -> "ListAction":
def _do_create_list_action(act, kw) -> ListAction:
"""A factory for list actions.
Convert the input list *act* into Actions and then wrap them in a
@ -529,7 +531,7 @@ class ActionBase(ABC):
show=_null,
execute=_null,
chdir=_null,
executor: Optional[ExecutorType] = None,
executor: Executor | None = None,
):
raise NotImplementedError
@ -541,15 +543,15 @@ class ActionBase(ABC):
batch_key = no_batch_key
def genstring(self, target, source, env, executor: Optional[ExecutorType] = None) -> str:
def genstring(self, target, source, env, executor: Executor | None = None) -> str:
return str(self)
@abstractmethod
def get_presig(self, target, source, env, executor: Optional[ExecutorType] = None):
def get_presig(self, target, source, env, executor: Executor | None = None):
raise NotImplementedError
@abstractmethod
def get_implicit_deps(self, target, source, env, executor: Optional[ExecutorType] = None):
def get_implicit_deps(self, target, source, env, executor: Executor | None = None):
raise NotImplementedError
def get_contents(self, target, source, env):
@ -601,10 +603,10 @@ class ActionBase(ABC):
self.presub_env = None # don't need this any more
return lines
def get_varlist(self, target, source, env, executor: Optional[ExecutorType] = None):
def get_varlist(self, target, source, env, executor: Executor | None = None):
return self.varlist
def get_targets(self, env, executor: Optional[ExecutorType]):
def get_targets(self, env, executor: Executor | None):
"""
Returns the type of targets ($TARGETS, $CHANGED_TARGETS) used
by this action.
@ -658,7 +660,7 @@ class _ActionAction(ActionBase):
show=_null,
execute=_null,
chdir=_null,
executor: Optional[ExecutorType] = None):
executor: Executor | None = None):
if not is_List(target):
target = [target]
if not is_List(source):
@ -742,10 +744,10 @@ class _ActionAction(ActionBase):
# an ABC like parent ActionBase, but things reach in and use it. It's
# not just unittests or we could fix it up with a concrete subclass there.
def get_presig(self, target, source, env, executor: Optional[ExecutorType] = None):
def get_presig(self, target, source, env, executor: Executor | None = None):
raise NotImplementedError
def get_implicit_deps(self, target, source, env, executor: Optional[ExecutorType] = None):
def get_implicit_deps(self, target, source, env, executor: Executor | None = None):
raise NotImplementedError
@ -891,15 +893,6 @@ def scons_subproc_run(scons_env, *args, **kwargs) -> subprocess.CompletedProcess
del kwargs['error']
kwargs['check'] = check
# TODO: Python version-compat stuff: remap/remove too-new args if needed
if 'text' in kwargs and sys.version_info < (3, 7):
kwargs['universal_newlines'] = kwargs.pop('text')
if 'capture_output' in kwargs and sys.version_info < (3, 7):
capture_output = kwargs.pop('capture_output')
if capture_output:
kwargs['stdout'] = kwargs['stderr'] = PIPE
# Most SCons tools/tests expect not to fail on things like missing files.
# check=True (or error="raise") means we're okay to take an exception;
# else we catch the likely exception and construct a dummy
@ -1010,7 +1003,7 @@ class CommandAction(_ActionAction):
return str(self.cmd_list)
def process(self, target, source, env, executor=None, overrides: Optional[dict] = None) -> Tuple[List, bool, bool]:
def process(self, target, source, env, executor: Executor | None = None, overrides: dict | None = None) -> tuple[list, bool, bool]:
if executor:
result = env.subst_list(self.cmd_list, SUBST_CMD, executor=executor, overrides=overrides)
else:
@ -1031,7 +1024,7 @@ class CommandAction(_ActionAction):
pass
return result, ignore, silent
def strfunction(self, target, source, env, executor: Optional[ExecutorType] = None, overrides: Optional[dict] = None) -> str:
def strfunction(self, target, source, env, executor: Executor | None = None, overrides: dict | None = None) -> str:
if self.cmdstr is None:
return None
if self.cmdstr is not _null:
@ -1046,7 +1039,7 @@ class CommandAction(_ActionAction):
return ''
return _string_from_cmd_list(cmd_list[0])
def execute(self, target, source, env, executor: Optional[ExecutorType] = None):
def execute(self, target, source, env, executor: Executor | None = None):
"""Execute a command action.
This will handle lists of commands as well as individual commands,
@ -1108,7 +1101,7 @@ class CommandAction(_ActionAction):
command=cmd_line)
return 0
def get_presig(self, target, source, env, executor: Optional[ExecutorType] = None):
def get_presig(self, target, source, env, executor: Executor | None = None):
"""Return the signature contents of this action's command line.
This strips $(-$) and everything in between the string,
@ -1123,7 +1116,7 @@ class CommandAction(_ActionAction):
return env.subst_target_source(cmd, SUBST_SIG, executor=executor)
return env.subst_target_source(cmd, SUBST_SIG, target, source)
def get_implicit_deps(self, target, source, env, executor: Optional[ExecutorType] = None):
def get_implicit_deps(self, target, source, env, executor: Executor | None = None):
"""Return the implicit dependencies of this action's command line."""
icd = env.get('IMPLICIT_COMMAND_DEPENDENCIES', True)
if is_String(icd) and icd[:1] == '$':
@ -1145,7 +1138,7 @@ class CommandAction(_ActionAction):
# lightweight dependency scanning.
return self._get_implicit_deps_lightweight(target, source, env, executor)
def _get_implicit_deps_lightweight(self, target, source, env, executor: Optional[ExecutorType]):
def _get_implicit_deps_lightweight(self, target, source, env, executor: Executor | None):
"""
Lightweight dependency scanning involves only scanning the first entry
in an action string, even if it contains &&.
@ -1166,7 +1159,7 @@ class CommandAction(_ActionAction):
res.append(env.fs.File(d))
return res
def _get_implicit_deps_heavyweight(self, target, source, env, executor: Optional[ExecutorType],
def _get_implicit_deps_heavyweight(self, target, source, env, executor: Executor | None,
icd_int):
"""
Heavyweight dependency scanning involves scanning more than just the
@ -1234,7 +1227,7 @@ class CommandGeneratorAction(ActionBase):
self.varlist = kw.get('varlist', ())
self.targets = kw.get('targets', '$TARGETS')
def _generate(self, target, source, env, for_signature, executor: Optional[ExecutorType] = None):
def _generate(self, target, source, env, for_signature, executor: Executor | None = None):
# ensure that target is a list, to make it easier to write
# generator functions:
if not is_List(target):
@ -1265,11 +1258,11 @@ class CommandGeneratorAction(ActionBase):
def batch_key(self, env, target, source):
return self._generate(target, source, env, 1).batch_key(env, target, source)
def genstring(self, target, source, env, executor: Optional[ExecutorType] = None) -> str:
def genstring(self, target, source, env, executor: Executor | None = None) -> str:
return self._generate(target, source, env, 1, executor).genstring(target, source, env)
def __call__(self, target, source, env, exitstatfunc=_null, presub=_null,
show=_null, execute=_null, chdir=_null, executor: Optional[ExecutorType] = None):
show=_null, execute=_null, chdir=_null, executor: Executor | None = None):
act = self._generate(target, source, env, 0, executor)
if act is None:
raise SCons.Errors.UserError(
@ -1281,7 +1274,7 @@ class CommandGeneratorAction(ActionBase):
target, source, env, exitstatfunc, presub, show, execute, chdir, executor
)
def get_presig(self, target, source, env, executor: Optional[ExecutorType] = None):
def get_presig(self, target, source, env, executor: Executor | None = None):
"""Return the signature contents of this action's command line.
This strips $(-$) and everything in between the string,
@ -1289,13 +1282,13 @@ class CommandGeneratorAction(ActionBase):
"""
return self._generate(target, source, env, 1, executor).get_presig(target, source, env)
def get_implicit_deps(self, target, source, env, executor: Optional[ExecutorType] = None):
def get_implicit_deps(self, target, source, env, executor: Executor | None = None):
return self._generate(target, source, env, 1, executor).get_implicit_deps(target, source, env)
def get_varlist(self, target, source, env, executor: Optional[ExecutorType] = None):
def get_varlist(self, target, source, env, executor: Executor | None = None):
return self._generate(target, source, env, 1, executor).get_varlist(target, source, env, executor)
def get_targets(self, env, executor: Optional[ExecutorType]):
def get_targets(self, env, executor: Executor | None):
return self._generate(None, None, env, 1, executor).get_targets(env, executor)
@ -1341,22 +1334,22 @@ class LazyAction(CommandGeneratorAction, CommandAction):
raise SCons.Errors.UserError("$%s value %s cannot be used to create an Action." % (self.var, repr(c)))
return gen_cmd
def _generate(self, target, source, env, for_signature, executor: Optional[ExecutorType] = None):
def _generate(self, target, source, env, for_signature, executor: Executor | None = None):
return self._generate_cache(env)
def __call__(self, target, source, env, *args, **kw):
c = self.get_parent_class(env)
return c.__call__(self, target, source, env, *args, **kw)
def get_presig(self, target, source, env, executor: Optional[ExecutorType] = None):
def get_presig(self, target, source, env, executor: Executor | None = None):
c = self.get_parent_class(env)
return c.get_presig(self, target, source, env)
def get_implicit_deps(self, target, source, env, executor: Optional[ExecutorType] = None):
def get_implicit_deps(self, target, source, env, executor: Executor | None = None):
c = self.get_parent_class(env)
return c.get_implicit_deps(self, target, source, env)
def get_varlist(self, target, source, env, executor: Optional[ExecutorType] = None):
def get_varlist(self, target, source, env, executor: Executor | None = None):
c = self.get_parent_class(env)
return c.get_varlist(self, target, source, env, executor)
@ -1389,7 +1382,7 @@ class FunctionAction(_ActionAction):
except AttributeError:
return "unknown_python_function"
def strfunction(self, target, source, env, executor: Optional[ExecutorType] = None):
def strfunction(self, target, source, env, executor: Executor | None = None):
if self.cmdstr is None:
return None
if self.cmdstr is not _null:
@ -1430,7 +1423,7 @@ class FunctionAction(_ActionAction):
return str(self.execfunction)
return "%s(target, source, env)" % name
def execute(self, target, source, env, executor: Optional[ExecutorType] = None):
def execute(self, target, source, env, executor: Executor | None = None):
exc_info = (None,None,None)
try:
if executor:
@ -1461,14 +1454,14 @@ class FunctionAction(_ActionAction):
# more information about this issue.
del exc_info
def get_presig(self, target, source, env, executor: Optional[ExecutorType] = None):
def get_presig(self, target, source, env, executor: Executor | None = None):
"""Return the signature contents of this callable action."""
try:
return self.gc(target, source, env)
except AttributeError:
return self.funccontents
def get_implicit_deps(self, target, source, env, executor: Optional[ExecutorType] = None):
def get_implicit_deps(self, target, source, env, executor: Executor | None = None):
return []
class ListAction(ActionBase):
@ -1485,7 +1478,7 @@ class ListAction(ActionBase):
self.varlist = ()
self.targets = '$TARGETS'
def genstring(self, target, source, env, executor: Optional[ExecutorType] = None) -> str:
def genstring(self, target, source, env, executor: Executor | None = None) -> str:
return '\n'.join([a.genstring(target, source, env) for a in self.list])
def __str__(self) -> str:
@ -1495,7 +1488,7 @@ class ListAction(ActionBase):
return SCons.Util.flatten_sequence(
[a.presub_lines(env) for a in self.list])
def get_presig(self, target, source, env, executor: Optional[ExecutorType] = None):
def get_presig(self, target, source, env, executor: Executor | None = None):
"""Return the signature contents of this action list.
Simple concatenation of the signatures of the elements.
@ -1503,7 +1496,7 @@ class ListAction(ActionBase):
return b"".join([bytes(x.get_contents(target, source, env)) for x in self.list])
def __call__(self, target, source, env, exitstatfunc=_null, presub=_null,
show=_null, execute=_null, chdir=_null, executor: Optional[ExecutorType] = None):
show=_null, execute=_null, chdir=_null, executor: Executor | None = None):
if executor:
target = executor.get_all_targets()
source = executor.get_all_sources()
@ -1514,13 +1507,13 @@ class ListAction(ActionBase):
return stat
return 0
def get_implicit_deps(self, target, source, env, executor: Optional[ExecutorType] = None):
def get_implicit_deps(self, target, source, env, executor: Executor | None = None):
result = []
for act in self.list:
result.extend(act.get_implicit_deps(target, source, env))
return result
def get_varlist(self, target, source, env, executor: Optional[ExecutorType] = None):
def get_varlist(self, target, source, env, executor: Executor | None = None):
result = OrderedDict()
for act in self.list:
for var in act.get_varlist(target, source, env, executor):
@ -1586,7 +1579,7 @@ class ActionCaller:
kw[key] = self.subst(self.kw[key], target, source, env)
return kw
def __call__(self, target, source, env, executor: Optional[ExecutorType] = None):
def __call__(self, target, source, env, executor: Executor | None = None):
args = self.subst_args(target, source, env)
kw = self.subst_kw(target, source, env)
return self.parent.actfunc(*args, **kw)

View File

@ -99,10 +99,11 @@ There are the following methods for internal use within this module:
"""
from __future__ import annotations
import os
from collections import UserDict, UserList
from contextlib import suppress
from typing import Optional
import SCons.Action
import SCons.Debug
@ -112,7 +113,7 @@ import SCons.Util
import SCons.Warnings
from SCons.Debug import logInstanceCreation
from SCons.Errors import InternalError, UserError
from SCons.Util.sctyping import ExecutorType
from SCons.Executor import Executor
class _Null:
pass
@ -591,7 +592,7 @@ class BuilderBase:
# build this particular list of targets from this particular list of
# sources.
executor: Optional[ExecutorType] = None
executor: Executor | None = None
key = None
if self.multi:

View File

@ -27,8 +27,10 @@
import atexit
import json
import os
import shutil
import stat
import sys
import tempfile
import uuid
import SCons.Action
@ -36,6 +38,12 @@ import SCons.Errors
import SCons.Warnings
import SCons.Util
CACHE_PREFIX_LEN = 2 # first two characters used as subdirectory name
CACHE_TAG = (
b"Signature: 8a477f597d28d172789f06886806bc55\n"
b"# SCons cache directory - see https://bford.info/cachedir/\n"
)
cache_enabled = True
cache_debug = False
cache_force = False
@ -64,23 +72,22 @@ def CacheRetrieveFunc(target, source, env) -> int:
except OSError:
pass
st = fs.stat(cachefile)
fs.chmod(t.get_internal_path(), stat.S_IMODE(st[stat.ST_MODE]) | stat.S_IWRITE)
fs.chmod(t.get_internal_path(), stat.S_IMODE(st.st_mode) | stat.S_IWRITE)
return 0
def CacheRetrieveString(target, source, env) -> None:
def CacheRetrieveString(target, source, env) -> str:
t = target[0]
fs = t.fs
cd = env.get_CacheDir()
cachedir, cachefile = cd.cachepath(t)
if t.fs.exists(cachefile):
return "Retrieved `%s' from cache" % t.get_internal_path()
return None
return ""
CacheRetrieve = SCons.Action.Action(CacheRetrieveFunc, CacheRetrieveString)
CacheRetrieveSilent = SCons.Action.Action(CacheRetrieveFunc, None)
def CachePushFunc(target, source, env):
def CachePushFunc(target, source, env) -> None:
if cache_readonly:
return
@ -134,8 +141,7 @@ CachePush = SCons.Action.Action(CachePushFunc, None)
class CacheDir:
def __init__(self, path) -> None:
"""
Initialize a CacheDir object.
"""Initialize a CacheDir object.
The cache configuration is stored in the object. It
is read from the config file in the supplied path if
@ -147,53 +153,120 @@ class CacheDir:
self.path = path
self.current_cache_debug = None
self.debugFP = None
self.config = dict()
if path is None:
return
self.config = {}
if path is not None:
self._readconfig(path)
self._readconfig(path)
def _add_config(self, path: str) -> None:
"""Create the cache config file in *path*.
def _readconfig(self, path):
"""
Read the cache config.
If directory or config file do not exist, create. Take advantage
of Py3 capability in os.makedirs() and in file open(): just try
the operation and handle failure appropriately.
Omit the check for old cache format, assume that's old enough
there will be none of those left to worry about.
:param path: path to the cache directory
Locking isn't necessary in the normal case - when the cachedir is
being created - because it's written to a unique directory first,
before the directory is renamed. But it is legal to call CacheDir
with an existing directory, which may be missing the config file,
and in that case we do need locking. Simpler to always lock.
"""
config_file = os.path.join(path, 'config')
# TODO: this breaks the "unserializable config object" test which
# does some crazy stuff, so for now don't use setdefault. It does
# seem like it would be better to preserve an exisiting value.
# self.config.setdefault('prefix_len', CACHE_PREFIX_LEN)
self.config['prefix_len'] = CACHE_PREFIX_LEN
with SCons.Util.FileLock(config_file, timeout=5, writer=True), open(
config_file, "x"
) as config:
try:
json.dump(self.config, config)
except Exception:
msg = "Failed to write cache configuration for " + path
raise SCons.Errors.SConsEnvironmentError(msg)
# Add the tag file "carelessly" - the contents are not used by SCons
# so we don't care about the chance of concurrent writes.
try:
# still use a try block even with exist_ok, might have other fails
os.makedirs(path, exist_ok=True)
except OSError:
msg = "Failed to create cache directory " + path
raise SCons.Errors.SConsEnvironmentError(msg)
tagfile = os.path.join(path, "CACHEDIR.TAG")
with open(tagfile, 'xb') as cachedir_tag:
cachedir_tag.write(CACHE_TAG)
except FileExistsError:
pass
def _mkdir_atomic(self, path: str) -> bool:
"""Create cache directory at *path*.
Uses directory renaming to avoid races. If we are actually
creating the dir, populate it with the metadata files at the
same time as that's the safest way. But it's not illegal to point
CacheDir at an existing directory that wasn't a cache previously,
so we may have to do that elsewhere, too.
Returns:
``True`` if it we created the dir, ``False`` if already existed,
Raises:
SConsEnvironmentError: if we tried and failed to create the cache.
"""
directory = os.path.abspath(path)
if os.path.exists(directory):
return False
try:
with SCons.Util.FileLock(config_file, timeout=5, writer=True), open(
config_file, "x"
) as config:
self.config['prefix_len'] = 2
# TODO: Python 3.7. See comment below.
# tempdir = tempfile.TemporaryDirectory(dir=os.path.dirname(directory))
tempdir = tempfile.mkdtemp(dir=os.path.dirname(directory))
except OSError as e:
msg = "Failed to create cache directory " + path
raise SCons.Errors.SConsEnvironmentError(msg) from e
# TODO: Python 3.7: the context manager raises exception on cleanup
# if the temporary was moved successfully (File Not Found).
# Fixed in 3.8+. In the replacement below we manually clean up if
# the move failed as mkdtemp() does not. TemporaryDirectory's
# cleanup is more sophisitcated so prefer when we can use it.
# self._add_config(tempdir.name)
# with tempdir:
# try:
# os.replace(tempdir.name, directory)
# return True
# except OSError as e:
# # did someone else get there first?
# if os.path.isdir(directory):
# return False # context manager cleans up
# msg = "Failed to create cache directory " + path
# raise SCons.Errors.SConsEnvironmentError(msg) from e
self._add_config(tempdir)
try:
os.replace(tempdir, directory)
return True
except OSError as e:
# did someone else get there first? attempt cleanup.
if os.path.isdir(directory):
try:
json.dump(self.config, config)
except Exception:
msg = "Failed to write cache configuration for " + path
raise SCons.Errors.SConsEnvironmentError(msg)
except FileExistsError:
try:
with SCons.Util.FileLock(config_file, timeout=5, writer=False), open(
config_file
) as config:
self.config = json.load(config)
except (ValueError, json.decoder.JSONDecodeError):
msg = "Failed to read cache configuration for " + path
raise SCons.Errors.SConsEnvironmentError(msg)
shutil.rmtree(tempdir)
except Exception: # we tried, don't worry about it
pass
return False
msg = "Failed to create cache directory " + path
raise SCons.Errors.SConsEnvironmentError(msg) from e
def _readconfig(self, path: str) -> None:
"""Read the cache config from *path*.
If directory or config file do not exist, create and populate.
"""
config_file = os.path.join(path, 'config')
created = self._mkdir_atomic(path)
if not created and not os.path.isfile(config_file):
# Could have been passed an empty directory
self._add_config(path)
try:
with SCons.Util.FileLock(config_file, timeout=5, writer=False), open(
config_file
) as config:
self.config = json.load(config)
except (ValueError, json.decoder.JSONDecodeError):
msg = "Failed to read cache configuration for " + path
raise SCons.Errors.SConsEnvironmentError(msg)
def CacheDebug(self, fmt, target, cachefile) -> None:
if cache_debug != self.current_cache_debug:
@ -252,7 +325,7 @@ class CacheDir:
def is_readonly(self) -> bool:
return cache_readonly
def get_cachedir_csig(self, node):
def get_cachedir_csig(self, node) -> str:
cachedir, cachefile = self.cachepath(node)
if cachefile and os.path.exists(cachefile):
return SCons.Util.hash_file_signature(cachefile, SCons.Node.FS.File.hash_chunksize)

View File

@ -31,12 +31,14 @@ The code that reads the registry to find MSVC components was borrowed
from distutils.msvccompiler.
"""
from __future__ import annotations
import os
import shutil
import stat
import sys
import time
from typing import List, Callable
from typing import Callable
import SCons.Action
import SCons.Builder
@ -467,8 +469,8 @@ def _stripixes(
prefix: str,
items,
suffix: str,
stripprefixes: List[str],
stripsuffixes: List[str],
stripprefixes: list[str],
stripsuffixes: list[str],
env,
literal_prefix: str = "",
c: Callable[[list], list] = None,
@ -547,7 +549,7 @@ def _stripixes(
return c(prefix, stripped, suffix, env)
def processDefines(defs) -> List[str]:
def processDefines(defs) -> list[str]:
"""Return list of strings for preprocessor defines from *defs*.
Resolves the different forms ``CPPDEFINES`` can be assembled in:

View File

@ -30,6 +30,8 @@ Keyword arguments supplied when the construction Environment is created
are construction variables used to initialize the Environment.
"""
from __future__ import annotations
import copy
import os
import sys
@ -37,7 +39,7 @@ import re
import shlex
from collections import UserDict, UserList, deque
from subprocess import PIPE, DEVNULL
from typing import Callable, Collection, Optional, Sequence, Union
from typing import TYPE_CHECKING, Callable, Collection, Sequence
import SCons.Action
import SCons.Builder
@ -76,7 +78,9 @@ from SCons.Util import (
to_String_for_subst,
uniquer_hashables,
)
from SCons.Util.sctyping import ExecutorType
if TYPE_CHECKING:
from SCons.Executor import Executor
class _Null:
pass
@ -534,6 +538,11 @@ class SubstitutionEnvironment:
Environment.Base to create their own flavors of construction
environment, we'll save that for a future refactoring when this
class actually becomes useful.)
Special note: methods here and in actual child classes might be called
via proxy from an :class:`OverrideEnvironment`, which isn't in the
class inheritance chain. Take care that methods called with a *self*
that's really an ``OverrideEnvironment`` don't make bad assumptions.
"""
def __init__(self, **kw) -> None:
@ -567,6 +576,20 @@ class SubstitutionEnvironment:
self._special_set_keys = list(self._special_set.keys())
def __eq__(self, other):
"""Compare two environments.
This is used by checks in Builder to determine if duplicate
targets have environments that would cause the same result.
The more reliable way (respecting the admonition to avoid poking
at :attr:`_dict` directly) would be to use ``Dictionary`` so this
is sure to work even if one or both are are instances of
:class:`OverrideEnvironment`. However an actual
``SubstitutionEnvironment`` doesn't have a ``Dictionary`` method
That causes problems for unit tests written to excercise
``SubsitutionEnvironment`` directly, although nobody else seems
to ever instantiate one. We count on :class:`OverrideEnvironment`
to fake the :attr:`_dict` to make things work.
"""
return self._dict == other._dict
def __delitem__(self, key) -> None:
@ -679,7 +702,7 @@ class SubstitutionEnvironment:
def lvars(self):
return {}
def subst(self, string, raw: int=0, target=None, source=None, conv=None, executor: Optional[ExecutorType] = None, overrides: Optional[dict] = None):
def subst(self, string, raw: int=0, target=None, source=None, conv=None, executor: Executor | None = None, overrides: dict | None = None):
"""Recursively interpolates construction variables from the
Environment into the specified string, returning the expanded
result. Construction variables are specified by a $ prefix
@ -705,7 +728,7 @@ class SubstitutionEnvironment:
nkw[k] = v
return nkw
def subst_list(self, string, raw: int=0, target=None, source=None, conv=None, executor: Optional[ExecutorType] = None, overrides: Optional[dict] = None):
def subst_list(self, string, raw: int=0, target=None, source=None, conv=None, executor: Executor | None = None, overrides: dict | None = None):
"""Calls through to SCons.Subst.scons_subst_list().
See the documentation for that function.
@ -811,16 +834,30 @@ class SubstitutionEnvironment:
self.added_methods = [dm for dm in self.added_methods if dm.method is not function]
def Override(self, overrides):
"""
Produce a modified environment whose variables are overridden by
the overrides dictionaries. "overrides" is a dictionary that
will override the variables of this environment.
"""Create an override environment from the current environment.
This function is much more efficient than Clone() or creating
a new Environment because it doesn't copy the construction
Produces a modified environment where the current variables are
overridden by any same-named variables from the *overrides* dict.
An override is much more efficient than doing :meth:`~Base.Clone`
or creating a new Environment because it doesn't copy the construction
environment dictionary, it just wraps the underlying construction
environment, and doesn't even create a wrapper object if there
are no overrides.
Using this method is preferred over directly instantiating an
:class:`OverrideEnvirionment` because extra checks are performed,
substitution takes place, and there is special handling for a
*parse_flags* keyword argument.
This method is not currently exposed as part of the public API,
but is invoked internally when things like builder calls have
keyword arguments, which are then passed as *overrides* here.
Some tools also call this explicitly.
Returns:
A proxy environment of type :class:`OverrideEnvironment`.
or the current environment if *overrides* is empty.
"""
if not overrides: return self
o = copy_non_reserved_keywords(overrides)
@ -868,7 +905,7 @@ class SubstitutionEnvironment:
'RPATH' : [],
}
def do_parse(arg: Union[str, Sequence]) -> None:
def do_parse(arg: str | Sequence) -> None:
if not arg:
return
@ -946,7 +983,7 @@ class SubstitutionEnvironment:
else:
mapping[append_next_arg_to].append(arg)
append_next_arg_to = None
elif not arg[0] in ['-', '+']:
elif arg[0] not in ['-', '+']:
mapping['LIBS'].append(self.fs.File(arg))
elif arg == '-dylib_file':
mapping['LINKFLAGS'].append(arg)
@ -1414,7 +1451,6 @@ class Base(SubstitutionEnvironment):
The variable is created if it is not already present.
"""
kw = copy_non_reserved_keywords(kw)
for key, val in kw.items():
if key == 'CPPDEFINES':
@ -1676,26 +1712,35 @@ class Base(SubstitutionEnvironment):
return None
def Dictionary(self, *args):
r"""Return construction variables from an environment.
def Dictionary(self, *args: str, as_dict: bool = False):
"""Return construction variables from an environment.
Args:
\*args (optional): variable names to look up
args (optional): construction variable names to select.
If omitted, all variables are selected and returned
as a dict.
as_dict: if true, and *args* is supplied, return the
variables and their values in a dict. If false
(the default), return a single value as a scalar,
or multiple values in a list.
Returns:
If `args` omitted, the dictionary of all construction variables.
If one arg, the corresponding value is returned.
If more than one arg, a list of values is returned.
A dictionary of construction variables, or a single value
or list of values.
Raises:
KeyError: if any of `args` is not in the construction environment.
KeyError: if any of *args* is not in the construction environment.
.. versionchanged:: 4.9.0
Added the *as_dict* keyword arg to specify always returning a dict.
"""
if not args:
return self._dict
dlist = [self._dict[x] for x in args]
if as_dict:
return {key: self._dict[key] for key in args}
dlist = [self._dict[key] for key in args]
if len(dlist) == 1:
dlist = dlist[0]
return dlist[0]
return dlist
@ -1718,18 +1763,15 @@ class Base(SubstitutionEnvironment):
Raises:
ValueError: *format* is not a recognized serialization format.
.. versionchanged:: NEXT_VERSION
.. versionchanged:: 4.9.0
*key* is no longer limited to a single construction variable name.
If *key* is supplied, a formatted dictionary is generated like the
no-arg case - previously a single *key* displayed just the value.
"""
if not key:
cvars = self.Dictionary()
elif len(key) == 1:
dkey = key[0]
cvars = {dkey: self[dkey]}
if len(key):
cvars = self.Dictionary(*key, as_dict=True)
else:
cvars = dict(zip(key, self.Dictionary(*key)))
cvars = self.Dictionary()
fmt = format.lower()
@ -1760,7 +1802,7 @@ class Base(SubstitutionEnvironment):
raise ValueError("Unsupported serialization format: %s." % fmt)
def FindIxes(self, paths: Sequence[str], prefix: str, suffix: str) -> Optional[str]:
def FindIxes(self, paths: Sequence[str], prefix: str, suffix: str) -> str | None:
"""Search *paths* for a path that has *prefix* and *suffix*.
Returns on first match.
@ -1857,7 +1899,6 @@ class Base(SubstitutionEnvironment):
The variable is created if it is not already present.
"""
kw = copy_non_reserved_keywords(kw)
for key, val in kw.items():
if key == 'CPPDEFINES':
@ -2042,7 +2083,7 @@ class Base(SubstitutionEnvironment):
return self.fs.Dir(self.subst(tp)).srcnode().get_abspath()
def Tool(
self, tool: Union[str, Callable], toolpath: Optional[Collection[str]] = None, **kwargs
self, tool: str | Callable, toolpath: Collection[str] | None = None, **kwargs
) -> Callable:
"""Find and run tool module *tool*.
@ -2207,6 +2248,16 @@ class Base(SubstitutionEnvironment):
self.get_CacheDir()
def Clean(self, targets, files) -> None:
"""Mark additional files for cleaning.
*files* will be removed if any of *targets* are selected
for cleaning - that is, the combination of target selection
and -c clean mode.
Args:
targets (files or nodes): targets to associate *files* with.
files (files or nodes): items to remove if *targets* are selected.
"""
global CleanTargets
tlist = self.arg2nodes(targets, self.fs.Entry)
flist = self.arg2nodes(files, self.fs.Entry)
@ -2293,8 +2344,8 @@ class Base(SubstitutionEnvironment):
return result
return self.fs.PyPackageDir(s)
def NoClean(self, *targets):
"""Tag target(s) so that it will not be cleaned by -c."""
def NoClean(self, *targets) -> list:
"""Tag *targets* to not be removed in clean mode."""
tlist = []
for t in targets:
tlist.extend(self.arg2nodes(t, self.fs.Entry))
@ -2537,45 +2588,75 @@ class Base(SubstitutionEnvironment):
class OverrideEnvironment(Base):
"""A proxy that overrides variables in a wrapped construction
environment by returning values from an overrides dictionary in
preference to values from the underlying subject environment.
"""A proxy that implements override environments.
This is a lightweight (I hope) proxy that passes through most use of
attributes to the underlying Environment.Base class, but has just
enough additional methods defined to act like a real construction
environment with overridden values. It can wrap either a Base
construction environment, or another OverrideEnvironment, which
can in turn nest arbitrary OverrideEnvironments...
Returns attributes/methods and construction variables from the
base environment *subject*, except that same-named construction
variables from *overrides* are returned on read access; assignment
to a construction variable creates an override entry - *subject* is
not modified. This is a much lighter weight approach for limited-use
setups than cloning an environment, for example to handle a builder
call with keyword arguments that make a temporary change to the
current environment::
Note that we do *not* call the underlying base class
(SubsitutionEnvironment) initialization, because we get most of those
from proxying the attributes of the subject construction environment.
But because we subclass SubstitutionEnvironment, this class also
has inherited arg2nodes() and subst*() methods; those methods can't
be proxied because they need *this* object's methods to fetch the
values from the overrides dictionary.
env.Program(target="foo", source=sources, DEBUG=True)
While the majority of methods are proxied from the underlying environment
class, enough plumbing is defined in this class for it to behave
like an ordinary Environment without the caller needing to know it is
"special" in some way. We don't call the initializer of the class
we're proxying, rather depend on it already being properly set up.
Deletion is handled specially, if a variable was explicitly deleted,
it should no longer appear to be in the env, but we also don't want to
modify the subject environment.
:class:`OverrideEnvironment` can nest arbitratily, *subject*
can be an existing instance. Although instances can be
instantiated directly, the expected use is to call the
:meth:`~SubstitutionEnvironment.Override` method as a factory.
Note Python does not give us a way to assure the subject environment
is not modified. Assigning to a variable creates a new entry in
the override, but moditying a variable will first fetch the one
from the subject, and if mutable, it will just be modified in place.
For example: ``over_env.Append(CPPDEFINES="-O")``, where ``CPPDEFINES``
is an existing list or :class:`~SCons.Util.CLVar`, will successfully
append to ``CPPDEFINES`` in the subject env. To avoid such leakage,
clients such as Scanners, Emitters and Action functions called by a
Builder using override syntax must take care if modifying an env
(which is not advised anyway) in case they were passed an
``OverrideEnvironment``.
"""
def __init__(self, subject, overrides=None) -> None:
def __init__(self, subject, overrides: dict | None = None) -> None:
if SCons.Debug.track_instances: logInstanceCreation(self, 'Environment.OverrideEnvironment')
overrides = {} if overrides is None else overrides
# set these directly via __dict__ to avoid trapping by __setattr__
self.__dict__['__subject'] = subject
if overrides is None:
self.__dict__['overrides'] = {}
else:
self.__dict__['overrides'] = overrides
self.__dict__['overrides'] = overrides
self.__dict__['__deleted'] = []
# Methods that make this class act like a proxy.
def __getattr__(self, name):
# Proxied environment methods don't know (nor should they have to) that
# they could be called with an OverrideEnvironment as 'self' and may
# access the _dict construction variable dict directly, so we need to
# pretend to have one, and not serve up the one from the subject, or it
# will miss the overridden values (and possibly modify the base). Use
# ourselves and hope the dict-like methods below are sufficient.
if name == '_dict':
return self
attr = getattr(self.__dict__['__subject'], name)
# Here we check if attr is one of the Wrapper classes. For
# example when a pseudo-builder is being called from an
# OverrideEnvironment.
#
# These wrappers when they're constructed capture the
# Environment they are being constructed with and so will not
# have access to overrided values. So we rebuild them with the
# OverrideEnvironment so they have access to overrided values.
# Check first if attr is one of the Wrapper classes, for example
# when a pseudo-builder is being called from an OverrideEnvironment.
# These wrappers, when they're constructed, capture the Environment
# they are being constructed with and so will not have access to
# overridden values. So we rebuild them with the OverrideEnvironment
# so they have access to overridden values.
if isinstance(attr, MethodWrapper):
return attr.clone(self)
else:
@ -2585,13 +2666,21 @@ class OverrideEnvironment(Base):
setattr(self.__dict__['__subject'], name, value)
# Methods that make this class act like a dictionary.
def __getitem__(self, key):
"""Return the visible value of *key*.
Backfills from the subject env if *key* doesn't have an entry in
the override, and is not explicity deleted.
"""
try:
return self.__dict__['overrides'][key]
except KeyError:
if key in self.__dict__['__deleted']:
raise
return self.__dict__['__subject'].__getitem__(key)
def __setitem__(self, key, value):
def __setitem__(self, key, value) -> None:
# This doesn't have the same performance equation as a "real"
# environment: in an override you're basically just writing
# new stuff; it's not a common case to be changing values already
@ -2599,58 +2688,94 @@ class OverrideEnvironment(Base):
if not key.isidentifier():
raise UserError(f"Illegal construction variable {key!r}")
self.__dict__['overrides'][key] = value
if key in self.__dict__['__deleted']:
# it's no longer "deleted" if we set it
self.__dict__['__deleted'].remove(key)
def __delitem__(self, key):
def __delitem__(self, key) -> None:
"""Delete *key* from override.
Makes *key* not visible in the override. Previously implemented
by deleting from ``overrides`` and from ``__subject``, which
keeps :meth:`__getitem__` from filling it back in next time.
However, that approach was a form of leak, as the subject
environment was modified. So instead we log that it's deleted
and use that to make decisions elsewhere.
"""
try:
del self.__dict__['overrides'][key]
except KeyError:
deleted = 0
deleted = False
else:
deleted = 1
try:
result = self.__dict__['__subject'].__delitem__(key)
except KeyError:
if not deleted:
raise
result = None
return result
deleted = True
if not deleted and key not in self.__dict__['__subject']:
raise KeyError(key)
self.__dict__['__deleted'].append(key)
def get(self, key, default=None):
"""Emulates the get() method of dictionaries."""
"""Emulates the ``get`` method of dictionaries.
Backfills from the subject environment if *key* is not in the override
and not deleted.
"""
try:
return self.__dict__['overrides'][key]
except KeyError:
if key in self.__dict__['__deleted']:
return default
return self.__dict__['__subject'].get(key, default)
def __contains__(self, key) -> bool:
"""Emulates the ``contains`` method of dictionaries.
Backfills from the subject environment if *key* is not in the override
and not deleted.
"""
if key in self.__dict__['overrides']:
return True
if key in self.__dict__['__deleted']:
return False
return key in self.__dict__['__subject']
def Dictionary(self, *args):
d = self.__dict__['__subject'].Dictionary().copy()
def Dictionary(self, *args, as_dict: bool = False):
"""Return construction variables from an environment.
Behavior is as described for :class:`SubstitutionEnvironment.Dictionary`
but understanda about the override.
Raises:
KeyError: if any of *args* is not in the construction environment.
.. versionchanged: 4.9.0
Added the *as_dict* keyword arg to always return a dict.
"""
d = {}
d.update(self.__dict__['__subject'])
d.update(self.__dict__['overrides'])
d = {k: v for k, v in d.items() if k not in self.__dict__['__deleted']}
if not args:
return d
dlist = [d[x] for x in args]
if as_dict:
return {key: d[key] for key in args}
dlist = [d[key] for key in args]
if len(dlist) == 1:
dlist = dlist[0]
return dlist[0]
return dlist
def items(self):
"""Emulates the items() method of dictionaries."""
"""Emulates the ``items`` method of dictionaries."""
return self.Dictionary().items()
def keys(self):
"""Emulates the keys() method of dictionaries."""
"""Emulates the ``keys`` method of dictionaries."""
return self.Dictionary().keys()
def values(self):
"""Emulates the values() method of dictionaries."""
"""Emulates the ``values`` method of dictionaries."""
return self.Dictionary().values()
def setdefault(self, key, default=None):
"""Emulates the setdefault() method of dictionaries."""
"""Emulates the ``setdefault`` method of dictionaries."""
try:
return self.__getitem__(key)
except KeyError:
@ -2658,6 +2783,7 @@ class OverrideEnvironment(Base):
return default
# Overridden private construction environment methods.
def _update(self, other) -> None:
self.__dict__['overrides'].update(other)
@ -2680,6 +2806,7 @@ class OverrideEnvironment(Base):
return lvars
# Overridden public construction environment methods.
def Replace(self, **kw) -> None:
kw = copy_non_reserved_keywords(kw)
self.__dict__['overrides'].update(semi_deepcopy(kw))

View File

@ -26,11 +26,15 @@
Used to handle internal and user errors in SCons.
"""
from __future__ import annotations
import shutil
from typing import Optional
from typing import TYPE_CHECKING
from SCons.Util.sctypes import to_String, is_String
from SCons.Util.sctyping import ExecutorType
if TYPE_CHECKING:
from SCons.Executor import Executor
# Note that not all Errors are defined here, some are at the point of use
@ -75,7 +79,7 @@ class BuildError(Exception):
def __init__(self,
node=None, errstr: str="Unknown error", status: int=2, exitstatus: int=2,
filename=None, executor: Optional[ExecutorType] = None, action=None, command=None,
filename=None, executor: Executor | None = None, action=None, command=None,
exc_info=(None, None, None)) -> None:
# py3: errstr should be string and not bytes.

View File

@ -23,8 +23,9 @@
"""Execute actions with specific lists of target and source Nodes."""
from __future__ import annotations
import collections
from typing import Dict
import SCons.Errors
import SCons.Memoize
@ -32,7 +33,6 @@ import SCons.Util
from SCons.compat import NoSlotsPyPy
import SCons.Debug
from SCons.Debug import logInstanceCreation
from SCons.Util.sctyping import ExecutorType
class Batch:
"""Remembers exact association between targets
@ -550,12 +550,12 @@ class Executor(metaclass=NoSlotsPyPy):
_batch_executors: Dict[str, ExecutorType] = {}
_batch_executors: dict[str, Executor] = {}
def GetBatchExecutor(key: str) -> ExecutorType:
def GetBatchExecutor(key: str) -> Executor:
return _batch_executors[key]
def AddBatchExecutor(key: str, executor: ExecutorType) -> None:
def AddBatchExecutor(key: str, executor: Executor) -> None:
assert key not in _batch_executors
_batch_executors[key] = executor

View File

@ -104,9 +104,15 @@ class Alias(SCons.Node.Node):
#
#
def build(self) -> None:
def build(self, **kw) -> None:
"""A "builder" for aliases."""
pass
if len(self.executor.post_actions) + len(self.executor.pre_actions) > 0:
# Only actually call Node's build() if there are any
# pre or post actions.
# Alias nodes will get 1 action and Alias.build()
# This fixes GH Issue #2281
return self.really_build(**kw)
def convert(self) -> None:
try: del self.builder

View File

@ -30,6 +30,8 @@ This holds a "default_fs" variable that should be initialized with an FS
that can be used by scripts or modules looking for the canonical default.
"""
from __future__ import annotations
import codecs
import fnmatch
import importlib.util
@ -40,7 +42,6 @@ import stat
import sys
import time
from itertools import chain
from typing import Optional
import SCons.Action
import SCons.Debug
@ -761,6 +762,8 @@ class Base(SCons.Node.Node):
st = self.stat()
if st:
# TODO: switch to st.st_mtime, however this changes granularity
# (ST_MTIME is an int for backwards compat, st_mtime is float)
return st[stat.ST_MTIME]
else:
return None
@ -1492,7 +1495,7 @@ class FS(LocalFS):
d = self.Dir(d)
self.Top.addRepository(d)
def PyPackageDir(self, modulename) -> Optional[Dir]:
def PyPackageDir(self, modulename) -> Dir | None:
r"""Locate the directory of Python module *modulename*.
For example 'SCons' might resolve to
@ -3190,7 +3193,7 @@ class File(Base):
# SIGNATURE SUBSYSTEM
#
def get_max_drift_csig(self) -> Optional[str]:
def get_max_drift_csig(self) -> str | None:
"""
Returns the content signature currently stored for this node
if it's been unmodified longer than the max_drift value, or the

View File

@ -40,18 +40,26 @@ be able to depend on any other type of "thing."
"""
from __future__ import annotations
import collections
import copy
from itertools import chain, zip_longest
from typing import Optional
from typing import Any, Callable, TYPE_CHECKING
import SCons.Debug
import SCons.Executor
import SCons.Memoize
from SCons.compat import NoSlotsPyPy
from SCons.Debug import logInstanceCreation, Trace
from SCons.Executor import Executor
from SCons.Util import hash_signature, is_List, UniqueList, render_tree
from SCons.Util.sctyping import ExecutorType
if TYPE_CHECKING:
from SCons.Builder import BuilderBase
from SCons.Environment import Base as Environment
from SCons.Scanner import ScannerBase
from SCons.SConsign import SConsignEntry
print_duplicate = 0
@ -101,7 +109,7 @@ def do_nothing_node(node) -> None: pass
Annotate = do_nothing_node
# global set for recording all processed SContruct/SConscript nodes
SConscriptNodes = set()
SConscriptNodes: set[Node] = set()
# Gets set to 'True' if we're running in interactive mode. Is
# currently used to release parts of a target's info during
@ -188,7 +196,7 @@ def get_contents_entry(node):
"""Fetch the contents of the entry. Returns the exact binary
contents of the file."""
try:
node = node.disambiguate(must_exist=1)
node = node.disambiguate(must_exist=True)
except SCons.Errors.UserError:
# There was nothing on disk with which to disambiguate
# this entry. Leave it as an Entry, but return a null
@ -355,7 +363,7 @@ class NodeInfoBase:
__slots__ = ('__weakref__',)
current_version_id = 2
def update(self, node) -> None:
def update(self, node: Node) -> None:
try:
field_list = self.field_list
except AttributeError:
@ -375,7 +383,7 @@ class NodeInfoBase:
def convert(self, node, val) -> None:
pass
def merge(self, other) -> None:
def merge(self, other: NodeInfoBase) -> None:
"""
Merge the fields of another object into this object. Already existing
information is overwritten by the other instance's data.
@ -385,7 +393,7 @@ class NodeInfoBase:
state = other.__getstate__()
self.__setstate__(state)
def format(self, field_list=None, names: int=0):
def format(self, field_list: list[str] | None = None, names: bool = False):
if field_list is None:
try:
field_list = self.field_list
@ -408,7 +416,7 @@ class NodeInfoBase:
fields.append(f)
return fields
def __getstate__(self):
def __getstate__(self) -> dict[str, Any]:
"""
Return all fields that shall be pickled. Walk the slots in the class
hierarchy and add those to the state dictionary. If a '__dict__' slot is
@ -428,7 +436,7 @@ class NodeInfoBase:
pass
return state
def __setstate__(self, state) -> None:
def __setstate__(self, state: dict[str, Any]) -> None:
"""
Restore the attributes from a pickled state. The version is discarded.
"""
@ -457,12 +465,12 @@ class BuildInfoBase:
def __init__(self) -> None:
# Create an object attribute from the class attribute so it ends up
# in the pickled data in the .sconsign file.
self.bsourcesigs = []
self.bdependsigs = []
self.bimplicitsigs = []
self.bactsig = None
self.bsourcesigs: list[BuildInfoBase] = []
self.bdependsigs: list[BuildInfoBase] = []
self.bimplicitsigs: list[BuildInfoBase] = []
self.bactsig: str | None = None
def merge(self, other) -> None:
def merge(self, other: BuildInfoBase) -> None:
"""
Merge the fields of another object into this object. Already existing
information is overwritten by the other instance's data.
@ -472,7 +480,7 @@ class BuildInfoBase:
state = other.__getstate__()
self.__setstate__(state)
def __getstate__(self):
def __getstate__(self) -> dict[str, Any]:
"""
Return all fields that shall be pickled. Walk the slots in the class
hierarchy and add those to the state dictionary. If a '__dict__' slot is
@ -492,7 +500,7 @@ class BuildInfoBase:
pass
return state
def __setstate__(self, state) -> None:
def __setstate__(self, state: dict[str, Any]) -> None:
"""
Restore the attributes from a pickled state.
"""
@ -570,42 +578,42 @@ class Node(metaclass=NoSlotsPyPy):
# this way, instead of wrapping up each list+dictionary pair in
# a class. (Of course, we could always still do that in the
# future if we had a good reason to...).
self.sources = [] # source files used to build node
self.sources_set = set()
self.sources: list[Node] = [] # source files used to build node
self.sources_set: set[Node] = set()
self._specific_sources = False
self.depends = [] # explicit dependencies (from Depends)
self.depends_set = set()
self.ignore = [] # dependencies to ignore
self.ignore_set = set()
self.prerequisites = None
self.implicit = None # implicit (scanned) dependencies (None means not scanned yet)
self.waiting_parents = set()
self.waiting_s_e = set()
self.depends: list[Node] = [] # explicit dependencies (from Depends)
self.depends_set: set[Node] = set()
self.ignore: list[Node] = [] # dependencies to ignore
self.ignore_set: set[Node] = set()
self.prerequisites: UniqueList | None = None
self.implicit: list[Node] | None = None # implicit (scanned) dependencies (None means not scanned yet)
self.waiting_parents: set[Node] = set()
self.waiting_s_e: set[Node] = set()
self.ref_count = 0
self.wkids = None # Kids yet to walk, when it's an array
self.wkids: list[Node] | None = None # Kids yet to walk, when it's an array
self.env = None
self.env: Environment | None = None
self.state = no_state
self.precious = None
self.precious = False
self.pseudo = False
self.noclean = 0
self.nocache = 0
self.cached = 0 # is this node pulled from cache?
self.always_build = None
self.includes = None
self.noclean = False
self.nocache = False
self.cached = False # is this node pulled from cache?
self.always_build = False
self.includes: list[str] | None = None
self.attributes = self.Attrs() # Generic place to stick information about the Node.
self.side_effect = 0 # true iff this node is a side effect
self.side_effects = [] # the side effects of building this target
self.linked = 0 # is this node linked to the variant directory?
self.changed_since_last_build = 0
self.store_info = 0
self._tags = None
self._func_is_derived = 1
self._func_exists = 1
self._func_rexists = 1
self._func_get_contents = 0
self._func_target_from_source = 0
self.ninfo = None
self.side_effect = False # true iff this node is a side effect
self.side_effects: list[Node] = [] # the side effects of building this target
self.linked = False # is this node linked to the variant directory?
self.changed_since_last_build = 0 # Index for "_decider_map".
self.store_info = 0 # Index for "store_info_map".
self._tags: dict[str, Any] | None = None
self._func_is_derived = 1 # Index for "_is_derived_map".
self._func_exists = 1 # Index for "_exists_map"
self._func_rexists = 1 # Index for "_rexists_map"
self._func_get_contents = 0 # Index for "_get_contents_map"
self._func_target_from_source = 0 # Index for "_target_from_source_map"
self.ninfo: NodeInfoBase | None = None
self.clear_memoized_values()
@ -614,14 +622,14 @@ class Node(metaclass=NoSlotsPyPy):
# what line in what file created the node, for example).
Annotate(self)
def disambiguate(self, must_exist=None):
def disambiguate(self, must_exist: bool = False):
return self
def get_suffix(self) -> str:
return ''
@SCons.Memoize.CountMethodCall
def get_build_env(self):
def get_build_env(self) -> Environment:
"""Fetch the appropriate Environment to build this node.
"""
try:
@ -632,15 +640,15 @@ class Node(metaclass=NoSlotsPyPy):
self._memo['get_build_env'] = result
return result
def get_build_scanner_path(self, scanner):
def get_build_scanner_path(self, scanner: ScannerBase):
"""Fetch the appropriate scanner path for this node."""
return self.get_executor().get_build_scanner_path(scanner)
def set_executor(self, executor: ExecutorType) -> None:
def set_executor(self, executor: Executor) -> None:
"""Set the action executor for this node."""
self.executor = executor
def get_executor(self, create: int=1) -> ExecutorType:
def get_executor(self, create: bool = True) -> Executor:
"""Fetch the action executor for this node. Create one if
there isn't already one, and requested to do so."""
try:
@ -651,7 +659,7 @@ class Node(metaclass=NoSlotsPyPy):
try:
act = self.builder.action
except AttributeError:
executor = SCons.Executor.Null(targets=[self]) # type: ignore
executor = SCons.Executor.Null(targets=[self]) # type: ignore[assignment]
else:
executor = SCons.Executor.Executor(act,
self.env or self.builder.env,
@ -664,7 +672,7 @@ class Node(metaclass=NoSlotsPyPy):
def executor_cleanup(self) -> None:
"""Let the executor clean up any cached information."""
try:
executor = self.get_executor(create=None)
executor = self.get_executor(create=False)
except AttributeError:
pass
else:
@ -681,7 +689,7 @@ class Node(metaclass=NoSlotsPyPy):
def push_to_cache(self) -> bool:
"""Try to push a node into a cache
"""
pass
return False
def retrieve_from_cache(self) -> bool:
"""Try to retrieve the node's content from a cache
@ -708,7 +716,7 @@ class Node(metaclass=NoSlotsPyPy):
"""
pass
def prepare(self):
def prepare(self) -> None:
"""Prepare for this Node to be built.
This is called after the Taskmaster has decided that the Node
@ -741,7 +749,7 @@ class Node(metaclass=NoSlotsPyPy):
raise SCons.Errors.StopError(msg % (i, self))
self.binfo = self.get_binfo()
def build(self, **kw):
def build(self, **kw) -> None:
"""Actually build the node.
This is called by the Taskmaster after it's decided that the
@ -826,10 +834,10 @@ class Node(metaclass=NoSlotsPyPy):
"""
pass
def add_to_waiting_s_e(self, node) -> None:
def add_to_waiting_s_e(self, node: Node) -> None:
self.waiting_s_e.add(node)
def add_to_waiting_parents(self, node) -> int:
def add_to_waiting_parents(self, node: Node) -> int:
"""
Returns the number of nodes added to our waiting parents list:
1 if we add a unique waiting parent, 0 if not. (Note that the
@ -866,13 +874,13 @@ class Node(metaclass=NoSlotsPyPy):
delattr(self, attr)
except AttributeError:
pass
self.cached = 0
self.cached = False
self.includes = None
def clear_memoized_values(self) -> None:
self._memo = {}
def builder_set(self, builder) -> None:
def builder_set(self, builder: BuilderBase | None) -> None:
self.builder = builder
try:
del self.executor
@ -898,7 +906,7 @@ class Node(metaclass=NoSlotsPyPy):
b = self.builder = None
return b is not None
def set_explicit(self, is_explicit) -> None:
def set_explicit(self, is_explicit: bool) -> None:
self.is_explicit = is_explicit
def has_explicit_builder(self) -> bool:
@ -914,7 +922,7 @@ class Node(metaclass=NoSlotsPyPy):
self.is_explicit = False
return False
def get_builder(self, default_builder=None):
def get_builder(self, default_builder: BuilderBase | None = None) -> BuilderBase | None:
"""Return the set builder, or a specified default value"""
try:
return self.builder
@ -947,7 +955,7 @@ class Node(metaclass=NoSlotsPyPy):
return False
return True
def check_attributes(self, name):
def check_attributes(self, name: str) -> Any | None:
""" Simple API to check if the node.attributes for name has been set"""
return getattr(getattr(self, "attributes", None), name, None)
@ -957,7 +965,7 @@ class Node(metaclass=NoSlotsPyPy):
"""
return [], None
def get_found_includes(self, env, scanner, path):
def get_found_includes(self, env: Environment, scanner: ScannerBase | None, path) -> list[Node]:
"""Return the scanned include lines (implicit dependencies)
found in this node.
@ -967,7 +975,7 @@ class Node(metaclass=NoSlotsPyPy):
"""
return []
def get_implicit_deps(self, env, initial_scanner, path_func, kw = {}):
def get_implicit_deps(self, env: Environment, initial_scanner: ScannerBase | None, path_func, kw = {}) -> list[Node]:
"""Return a list of implicit dependencies for this node.
This method exists to handle recursive invocation of the scanner
@ -1002,7 +1010,7 @@ class Node(metaclass=NoSlotsPyPy):
return dependencies
def _get_scanner(self, env, initial_scanner, root_node_scanner, kw):
def _get_scanner(self, env: Environment, initial_scanner: ScannerBase | None, root_node_scanner: ScannerBase | None, kw: dict[str, Any] | None) -> ScannerBase | None:
if initial_scanner:
# handle explicit scanner case
scanner = initial_scanner.select(self)
@ -1019,13 +1027,13 @@ class Node(metaclass=NoSlotsPyPy):
return scanner
def get_env_scanner(self, env, kw={}):
def get_env_scanner(self, env: Environment, kw: dict[str, Any] | None = {}) -> ScannerBase | None:
return env.get_scanner(self.scanner_key())
def get_target_scanner(self):
def get_target_scanner(self) -> ScannerBase | None:
return self.builder.target_scanner
def get_source_scanner(self, node):
def get_source_scanner(self, node: Node) -> ScannerBase | None:
"""Fetch the source scanner for the specified node
NOTE: "self" is the target being built, "node" is
@ -1051,10 +1059,10 @@ class Node(metaclass=NoSlotsPyPy):
scanner = scanner.select(node)
return scanner
def add_to_implicit(self, deps) -> None:
def add_to_implicit(self, deps: list[Node]) -> None:
if not hasattr(self, 'implicit') or self.implicit is None:
self.implicit = []
self.implicit_set = set()
self.implicit_set: set[Node] = set()
self._children_reset()
self._add_child(self.implicit, self.implicit_set, deps)
@ -1113,10 +1121,10 @@ class Node(metaclass=NoSlotsPyPy):
if scanner:
executor.scan_targets(scanner)
def scanner_key(self):
def scanner_key(self) -> str | None:
return None
def select_scanner(self, scanner):
def select_scanner(self, scanner: ScannerBase) -> ScannerBase | None:
"""Selects a scanner for this Node.
This is a separate method so it can be overridden by Node
@ -1126,7 +1134,7 @@ class Node(metaclass=NoSlotsPyPy):
"""
return scanner.select(self)
def env_set(self, env, safe: bool=False) -> None:
def env_set(self, env: Environment, safe: bool = False) -> None:
if safe and self.env:
return
self.env = env
@ -1138,21 +1146,21 @@ class Node(metaclass=NoSlotsPyPy):
NodeInfo = NodeInfoBase
BuildInfo = BuildInfoBase
def new_ninfo(self):
def new_ninfo(self) -> NodeInfoBase:
ninfo = self.NodeInfo()
return ninfo
def get_ninfo(self):
def get_ninfo(self) -> NodeInfoBase:
if self.ninfo is not None:
return self.ninfo
self.ninfo = self.new_ninfo()
return self.ninfo
def new_binfo(self):
def new_binfo(self) -> BuildInfoBase:
binfo = self.BuildInfo()
return binfo
def get_binfo(self):
def get_binfo(self) -> BuildInfoBase:
"""
Fetch a node's build information.
@ -1211,7 +1219,7 @@ class Node(metaclass=NoSlotsPyPy):
except AttributeError:
pass
def get_csig(self):
def get_csig(self) -> str:
try:
return self.ninfo.csig
except AttributeError:
@ -1219,13 +1227,13 @@ class Node(metaclass=NoSlotsPyPy):
ninfo.csig = hash_signature(self.get_contents())
return self.ninfo.csig
def get_cachedir_csig(self):
def get_cachedir_csig(self) -> str:
return self.get_csig()
def get_stored_info(self):
def get_stored_info(self) -> SConsignEntry | None:
return None
def get_stored_implicit(self):
def get_stored_implicit(self) -> list[Node] | None:
"""Fetch the stored implicit dependencies"""
return None
@ -1233,7 +1241,7 @@ class Node(metaclass=NoSlotsPyPy):
#
#
def set_precious(self, precious: int = 1) -> None:
def set_precious(self, precious: bool = True) -> None:
"""Set the Node's precious value."""
self.precious = precious
@ -1241,19 +1249,15 @@ class Node(metaclass=NoSlotsPyPy):
"""Set the Node's pseudo value."""
self.pseudo = pseudo
def set_noclean(self, noclean: int = 1) -> None:
def set_noclean(self, noclean: bool = True) -> None:
"""Set the Node's noclean value."""
# Make sure noclean is an integer so the --debug=stree
# output in Util.py can use it as an index.
self.noclean = noclean and 1 or 0
self.noclean = noclean
def set_nocache(self, nocache: int = 1) -> None:
def set_nocache(self, nocache: bool = True) -> None:
"""Set the Node's nocache value."""
# Make sure nocache is an integer so the --debug=stree
# output in Util.py can use it as an index.
self.nocache = nocache and 1 or 0
self.nocache = nocache
def set_always_build(self, always_build: int = 1) -> None:
def set_always_build(self, always_build: bool = True) -> None:
"""Set the Node's always_build value."""
self.always_build = always_build
@ -1261,12 +1265,12 @@ class Node(metaclass=NoSlotsPyPy):
"""Reports whether node exists."""
return _exists_map[self._func_exists](self)
def rexists(self):
def rexists(self) -> bool:
"""Does this node exist locally or in a repository?"""
# There are no repositories by default:
return _rexists_map[self._func_rexists](self)
def get_contents(self):
def get_contents(self) -> bytes | str:
"""Fetch the contents of the entry."""
return _get_contents_map[self._func_get_contents](self)
@ -1275,11 +1279,11 @@ class Node(metaclass=NoSlotsPyPy):
not self.linked and \
not self.rexists()
def remove(self):
def remove(self) -> None:
"""Remove this Node: no-op by default."""
return None
def add_dependency(self, depend):
def add_dependency(self, depend: list[Node]) -> None:
"""Adds dependencies."""
try:
self._add_child(self.depends, self.depends_set, depend)
@ -1291,14 +1295,14 @@ class Node(metaclass=NoSlotsPyPy):
s = str(e)
raise SCons.Errors.UserError("attempted to add a non-Node dependency to %s:\n\t%s is a %s, not a Node" % (str(self), s, type(e)))
def add_prerequisite(self, prerequisite) -> None:
def add_prerequisite(self, prerequisite: list[Node]) -> None:
"""Adds prerequisites"""
if self.prerequisites is None:
self.prerequisites = UniqueList()
self.prerequisites.extend(prerequisite)
self._children_reset()
def add_ignore(self, depend):
def add_ignore(self, depend: list[Node]) -> None:
"""Adds dependencies to ignore."""
try:
self._add_child(self.ignore, self.ignore_set, depend)
@ -1310,7 +1314,7 @@ class Node(metaclass=NoSlotsPyPy):
s = str(e)
raise SCons.Errors.UserError("attempted to ignore a non-Node dependency of %s:\n\t%s is a %s, not a Node" % (str(self), s, type(e)))
def add_source(self, source):
def add_source(self, source: list[Node]) -> None:
"""Adds sources."""
if self._specific_sources:
return
@ -1324,7 +1328,7 @@ class Node(metaclass=NoSlotsPyPy):
s = str(e)
raise SCons.Errors.UserError("attempted to add a non-Node as source of %s:\n\t%s is a %s, not a Node" % (str(self), s, type(e)))
def _add_child(self, collection, set, child) -> None:
def _add_child(self, collection: list[Node], set: set[Node], child: list[Node]) -> None:
"""Adds 'child' to 'collection', first checking 'set' to see if it's
already present."""
added = None
@ -1336,11 +1340,11 @@ class Node(metaclass=NoSlotsPyPy):
if added:
self._children_reset()
def set_specific_source(self, source) -> None:
def set_specific_source(self, source: list[Node]) -> None:
self.add_source(source)
self._specific_sources = True
def add_wkid(self, wkid) -> None:
def add_wkid(self, wkid: Node) -> None:
"""Add a node to the list of kids waiting to be evaluated"""
if self.wkids is not None:
self.wkids.append(wkid)
@ -1352,7 +1356,7 @@ class Node(metaclass=NoSlotsPyPy):
self.executor_cleanup()
@SCons.Memoize.CountMethodCall
def _children_get(self):
def _children_get(self) -> list[Node]:
try:
return self._memo['_children_get']
except KeyError:
@ -1383,12 +1387,12 @@ class Node(metaclass=NoSlotsPyPy):
if i not in self.ignore_set:
children.append(i)
else:
children = self.all_children(scan=0)
children = self.all_children(scan=False)
self._memo['_children_get'] = children
return children
def all_children(self, scan: int=1):
def all_children(self, scan: bool = True) -> list[Node]:
"""Return a list of all the node's direct children."""
if scan:
self.scan()
@ -1412,27 +1416,27 @@ class Node(metaclass=NoSlotsPyPy):
# internally anyway...)
return list(chain.from_iterable([_f for _f in [self.sources, self.depends, self.implicit] if _f]))
def children(self, scan: int=1):
def children(self, scan: bool = True) -> list[Node]:
"""Return a list of the node's direct children, minus those
that are ignored by this node."""
if scan:
self.scan()
return self._children_get()
def set_state(self, state) -> None:
def set_state(self, state: int) -> None:
self.state = state
def get_state(self):
def get_state(self) -> int:
return self.state
def get_env(self):
def get_env(self) -> Environment:
env = self.env
if not env:
import SCons.Defaults
env = SCons.Defaults.DefaultEnvironment()
return env
def Decider(self, function) -> None:
def Decider(self, function: Callable[[Node, Node, NodeInfoBase, Node | None], bool]) -> None:
foundkey = None
for k, v in _decider_map.items():
if v == function:
@ -1443,19 +1447,19 @@ class Node(metaclass=NoSlotsPyPy):
_decider_map[foundkey] = function
self.changed_since_last_build = foundkey
def Tag(self, key, value) -> None:
def Tag(self, key: str, value: Any | None) -> None:
""" Add a user-defined tag. """
if not self._tags:
self._tags = {}
self._tags[key] = value
def GetTag(self, key):
def GetTag(self, key: str) -> Any | None:
""" Return a user-defined tag. """
if not self._tags:
return None
return self._tags.get(key, None)
def changed(self, node=None, allowcache: bool=False):
def changed(self, node: Node | None = None, allowcache: bool = False) -> bool:
"""
Returns if the node is up-to-date with respect to the BuildInfo
stored last time it was built. The default behavior is to compare
@ -1534,7 +1538,7 @@ class Node(metaclass=NoSlotsPyPy):
if self.always_build:
return False
state = 0
for kid in self.children(None):
for kid in self.children(False):
s = kid.get_state()
if s and (not state or s > state):
state = s
@ -1559,13 +1563,13 @@ class Node(metaclass=NoSlotsPyPy):
path = self.get_build_scanner_path(scanner)
else:
path = None
def f(node, env=env, scanner=scanner, path=path):
def f(node: Node, env: Environment = env, scanner: ScannerBase = scanner, path=path):
return node.get_found_includes(env, scanner, path)
return render_tree(s, f, 1)
else:
return None
def get_abspath(self):
def get_abspath(self) -> str:
"""
Return an absolute path to the Node. This will return simply
str(Node) by default, but for Node types that have a concept of
@ -1573,7 +1577,7 @@ class Node(metaclass=NoSlotsPyPy):
"""
return str(self)
def for_signature(self):
def for_signature(self) -> str:
"""
Return a string representation of the Node that will always
be the same for this particular Node, no matter what. This
@ -1588,7 +1592,7 @@ class Node(metaclass=NoSlotsPyPy):
"""
return str(self)
def get_string(self, for_signature):
def get_string(self, for_signature: bool) -> str:
"""This is a convenience function designed primarily to be
used in command generators (i.e., CommandGeneratorActions or
Environment variables that are callable), which are called
@ -1719,9 +1723,9 @@ class NodeList(collections.UserList):
def __str__(self) -> str:
return str(list(map(str, self.data)))
def get_children(node, parent): return node.children()
def ignore_cycle(node, stack) -> None: pass
def do_nothing(node, parent) -> None: pass
def get_children(node: Node, parent: Node | None) -> list[Node]: return node.children()
def ignore_cycle(node: Node, stack: list[Node]) -> None: pass
def do_nothing(node: Node, parent: Node | None) -> None: pass
class Walker:
"""An iterator for walking a Node tree.
@ -1736,15 +1740,19 @@ class Walker:
This class does not get caught in node cycles caused, for example,
by C header file include loops.
"""
def __init__(self, node, kids_func=get_children,
cycle_func=ignore_cycle,
eval_func=do_nothing) -> None:
def __init__(
self,
node: Node,
kids_func: Callable[[Node, Node | None], list[Node]] = get_children,
cycle_func: Callable[[Node, list[Node]], None] = ignore_cycle,
eval_func: Callable[[Node, Node | None], None] = do_nothing,
) -> None:
self.kids_func = kids_func
self.cycle_func = cycle_func
self.eval_func = eval_func
node.wkids = copy.copy(kids_func(node, None))
self.stack = [node]
self.history = {} # used to efficiently detect and avoid cycles
self.history: dict[Node, Any | None] = {} # used to efficiently detect and avoid cycles
self.history[node] = None
def get_next(self):

View File

@ -42,6 +42,7 @@ their own platform definition.
import SCons.compat
import atexit
import importlib
import os
import sys
@ -150,7 +151,7 @@ class TempFileMunge:
the length of command lines. Example::
env["TEMPFILE"] = TempFileMunge
env["LINKCOM"] = "${TEMPFILE('$LINK $TARGET $SOURCES','$LINKCOMSTR')}"
env["LINKCOM"] = "${TEMPFILE('$LINK $TARGET $SOURCES', '$LINKCOMSTR')}"
By default, the name of the temporary file used begins with a
prefix of '@'. This may be configured for other tool chains by
@ -258,31 +259,27 @@ class TempFileMunge:
fd, tmp = tempfile.mkstemp(suffix, dir=tempfile_dir, text=True)
native_tmp = SCons.Util.get_native_path(tmp)
# arrange for cleanup on exit:
def tmpfile_cleanup(file) -> None:
os.remove(file)
atexit.register(tmpfile_cleanup, tmp)
if env.get('SHELL', None) == 'sh':
# The sh shell will try to escape the backslashes in the
# path, so unescape them.
native_tmp = native_tmp.replace('\\', r'\\\\')
# In Cygwin, we want to use rm to delete the temporary
# file, because del does not exist in the sh shell.
rm = env.Detect('rm') or 'del'
else:
# Don't use 'rm' if the shell is not sh, because rm won't
# work with the Windows shells (cmd.exe or command.com) or
# Windows path names.
rm = 'del'
if 'TEMPFILEPREFIX' in env:
prefix = env.subst('$TEMPFILEPREFIX')
else:
prefix = '@'
prefix = "@"
tempfile_esc_func = env.get('TEMPFILEARGESCFUNC', SCons.Subst.quote_spaces)
args = [
tempfile_esc_func(arg)
for arg in cmd[1:]
]
args = [tempfile_esc_func(arg) for arg in cmd[1:]]
join_char = env.get('TEMPFILEARGJOIN', ' ')
os.write(fd, bytearray(join_char.join(args) + "\n", 'utf-8'))
os.write(fd, bytearray(join_char.join(args) + "\n", encoding="utf-8"))
os.close(fd)
# XXX Using the SCons.Action.print_actions value directly
@ -301,15 +298,20 @@ class TempFileMunge:
# purity get in the way of just being helpful, so we'll
# reach into SCons.Action directly.
if SCons.Action.print_actions:
cmdstr = env.subst(self.cmdstr, SCons.Subst.SUBST_RAW, target,
source) if self.cmdstr is not None else ''
cmdstr = (
env.subst(self.cmdstr, SCons.Subst.SUBST_RAW, target, source)
if self.cmdstr is not None
else ''
)
# Print our message only if XXXCOMSTR returns an empty string
if len(cmdstr) == 0 :
cmdstr = ("Using tempfile "+native_tmp+" for command line:\n"+
str(cmd[0]) + " " + " ".join(args))
if not cmdstr:
cmdstr = (
f"Using tempfile {native_tmp} for command line:\n"
f'{cmd[0]} {" ".join(args)}'
)
self._print_cmd_str(target, source, env, cmdstr)
cmdlist = [cmd[0], prefix + native_tmp + '\n' + rm, native_tmp]
cmdlist = [cmd[0], prefix + native_tmp]
# Store the temporary file command list into the target Node.attributes
# to avoid creating two temporary files one for print and one for execute.

View File

@ -46,7 +46,7 @@ def generate(env) -> None:
# make sure this works on Macs with Tiger or earlier
try:
dirlist = os.listdir('/etc/paths.d')
except FileNotFoundError:
except (FileNotFoundError, PermissionError):
dirlist = []
for file in dirlist:

View File

@ -31,6 +31,8 @@ Tests on the build system can detect if compiler sees header files, if
libraries are installed, if some command line options are supported etc.
"""
from __future__ import annotations
import SCons.compat
import atexit
@ -39,7 +41,6 @@ import os
import re
import sys
import traceback
from typing import Tuple
import SCons.Action
import SCons.Builder
@ -265,7 +266,7 @@ class SConfBuildTask(SCons.Taskmaster.AlwaysTask):
sys.excepthook(*self.exc_info())
return SCons.Taskmaster.Task.failed(self)
def collect_node_states(self) -> Tuple[bool, bool, bool]:
def collect_node_states(self) -> tuple[bool, bool, bool]:
# returns (is_up_to_date, cached_error, cachable)
# where is_up_to_date is True if the node(s) are up_to_date
# cached_error is True if the node(s) are up_to_date, but the
@ -1100,12 +1101,17 @@ def CheckCXXHeader(context, header, include_quotes: str = '""'):
def CheckLib(context, library = None, symbol: str = "main",
header = None, language = None, autoadd: bool=True,
append: bool=True, unique: bool=False) -> bool:
header = None, language = None, extra_libs = None,
autoadd: bool=True, append: bool=True, unique: bool=False) -> bool:
"""
A test for a library. See also CheckLibWithHeader.
A test for a library. See also :func:`CheckLibWithHeader`.
Note that library may also be None to test whether the given symbol
compiles without flags.
.. versionchanged:: 4.9.0
Added the *extra_libs* keyword parameter. The actual implementation
is in :func:`SCons.Conftest.CheckLib` which already accepted this
parameter, so this is only exposing existing functionality.
"""
if not library:
@ -1115,9 +1121,9 @@ def CheckLib(context, library = None, symbol: str = "main",
library = [library]
# ToDo: accept path for the library
res = SCons.Conftest.CheckLib(context, library, symbol, header = header,
language = language, autoadd = autoadd,
append=append, unique=unique)
res = SCons.Conftest.CheckLib(context, library, symbol, header=header,
language=language, extra_libs=extra_libs,
autoadd=autoadd, append=append, unique=unique)
context.did_show_result = True
return not res
@ -1125,15 +1131,21 @@ def CheckLib(context, library = None, symbol: str = "main",
# Bram: Can only include one header and can't use #ifdef HAVE_HEADER_H.
def CheckLibWithHeader(context, libs, header, language,
call = None, autoadd: bool=True, append: bool=True, unique: bool=False) -> bool:
# ToDo: accept path for library. Support system header files.
extra_libs = None, call = None, autoadd: bool=True,
append: bool=True, unique: bool=False) -> bool:
"""
Another (more sophisticated) test for a library.
Checks, if library and header is available for language (may be 'C'
or 'CXX'). Call maybe be a valid expression _with_ a trailing ';'.
As in CheckLib, we support library=None, to test if the call compiles
As in :func:`CheckLib`, we support library=None, to test if the call compiles
without extra link flags.
.. versionchanged:: 4.9.0
Added the *extra_libs* keyword parameter. The actual implementation
is in :func:`SCons.Conftest.CheckLib` which already accepted this
parameter, so this is only exposing existing functionality.
"""
# ToDo: accept path for library. Support system header files.
prog_prefix, dummy = createIncludesFromHeaders(header, 0)
if not libs:
libs = [None]
@ -1142,8 +1154,8 @@ def CheckLibWithHeader(context, libs, header, language,
libs = [libs]
res = SCons.Conftest.CheckLib(context, libs, None, prog_prefix,
call = call, language = language, autoadd=autoadd,
append=append, unique=unique)
extra_libs = extra_libs, call = call, language = language,
autoadd=autoadd, append=append, unique=unique)
context.did_show_result = 1
return not res

View File

@ -28,6 +28,8 @@ CConditionalScanner, which must be explicitly selected by calling
add_scanner() for each affected suffix.
"""
from typing import Dict
import SCons.Node.FS
import SCons.cpp
import SCons.Util
@ -65,32 +67,85 @@ class SConsCPPScanner(SCons.cpp.PreProcessor):
self.missing.append((file, self.current_file))
return ''
def dictify_CPPDEFINES(env) -> dict:
"""Returns CPPDEFINES converted to a dict.
def dictify_CPPDEFINES(env, replace: bool = False) -> dict:
"""Return CPPDEFINES converted to a dict for preprocessor emulation.
This should be similar to :func:`~SCons.Defaults.processDefines`.
Unfortunately, we can't do the simple thing of calling that routine and
passing the result to the dict() constructor, because it turns the defines
into a list of "name=value" pairs, which the dict constructor won't
consume correctly. Also cannot just call dict on CPPDEFINES itself - it's
fine if it's stored in the converted form (currently deque of tuples), but
CPPDEFINES could be in other formats too.
The concept is similar to :func:`~SCons.Defaults.processDefines`:
turn the values stored in an internal form in ``env['CPPDEFINES']``
into one needed for a specific context - in this case the cpp-like
work the C/C++ scanner will do. We can't reuse ``processDefines``
output as that's a list of strings for the command line. We also can't
pass the ``CPPDEFINES`` variable directly to the ``dict`` constructor,
as SCons allows it to be stored in several different ways - it's only
after ``Append`` and relatives has been called we know for sure it will
be a deque of tuples.
So we have to do all the work here - keep concepts in sync with
``processDefines``.
If requested (*replace* is true), simulate some of the macro
replacement that would take place if an actual preprocessor ran,
to avoid some conditional inclusions comeing out wrong. A bit
of an edge case, but does happen (GH #4623). See 6.10.5 in the C
standard and 15.6 in the C++ standard).
Args:
replace: if true, simulate macro replacement
.. versionchanged:: 4.9.0
Simple macro replacement added, and *replace* arg to enable it.
"""
def _replace(mapping: Dict) -> Dict:
"""Simplistic macro replacer for dictify_CPPDEFINES.
Scan *mapping* for a value that is the same as a key in the dict,
and replace with the value of that key; the process is repeated a few
times, but not forever in case someone left a case that can't be
fully resolved. This is a cheap approximation of the preprocessor's
macro replacement rules with no smarts - it doesn't "look inside"
the values, so only triggers on object-like macros, not on
function-like macros, and will not work on complex values, e.g.
a value like ``(1UL << PR_MTE_TCF_SHIFT)`` would not have
``PR_MTE_TCF_SHIFT`` replaced if that was also a key in ``CPPDEFINES``.
Args:
mapping: a dictionary representing macro names and replacements.
Returns:
a dictionary with replacements made.
"""
old_ns = mapping
loops = 0
while loops < 5: # don't recurse forever in case there's circular data
# this was originally written as a dict comprehension, but unrolling
# lets us add a finer-grained check for whether another loop is
# needed, rather than comparing two dicts to see if one changed.
again = False
ns = {}
for k, v in old_ns.items():
if v in old_ns:
ns[k] = old_ns[v]
if not again and ns[k] != v:
again = True
else:
ns[k] = v
if not again:
break
old_ns = ns
loops += 1
return ns
cppdefines = env.get('CPPDEFINES', {})
result = {}
if cppdefines is None:
return result
if not cppdefines:
return {}
if SCons.Util.is_Tuple(cppdefines):
# single macro defined in a tuple
try:
return {cppdefines[0]: cppdefines[1]}
except IndexError:
return {cppdefines[0]: None}
if SCons.Util.is_Sequence(cppdefines):
# multiple (presumably) macro defines in a deque, list, etc.
result = {}
for c in cppdefines:
if SCons.Util.is_Sequence(c):
try:
@ -107,9 +162,12 @@ def dictify_CPPDEFINES(env) -> dict:
else:
# don't really know what to do here
result[c] = None
return result
if replace:
return _replace(result)
return(result)
if SCons.Util.is_String(cppdefines):
# single macro define in a string
try:
name, value = cppdefines.split('=')
return {name: value}
@ -117,6 +175,9 @@ def dictify_CPPDEFINES(env) -> dict:
return {cppdefines: None}
if SCons.Util.is_Dict(cppdefines):
# already in the desired form
if replace:
return _replace(cppdefines)
return cppdefines
return {cppdefines: None}
@ -136,7 +197,9 @@ class SConsCPPScannerWrapper:
def __call__(self, node, env, path=()):
cpp = SConsCPPScanner(
current=node.get_dir(), cpppath=path, dict=dictify_CPPDEFINES(env)
current=node.get_dir(),
cpppath=path,
dict=dictify_CPPDEFINES(env, replace=True),
)
result = cpp(node)
for included, includer in cpp.missing:
@ -149,6 +212,7 @@ class SConsCPPScannerWrapper:
def recurse_nodes(self, nodes):
return nodes
def select(self, node):
return self

View File

@ -169,6 +169,11 @@ class LaTeX(ScannerBase):
'addsectionbib': 'BIBINPUTS',
'makeindex': 'INDEXSTYLE',
'usepackage': 'TEXINPUTS',
'usetheme': 'TEXINPUTS',
'usecolortheme': 'TEXINPUTS',
'usefonttheme': 'TEXINPUTS',
'useinnertheme': 'TEXINPUTS',
'useoutertheme': 'TEXINPUTS',
'lstinputlisting': 'TEXINPUTS'}
env_variables = SCons.Util.unique(list(keyword_paths.values()))
two_arg_commands = ['import', 'subimport',
@ -193,6 +198,7 @@ class LaTeX(ScannerBase):
| addglobalbib
| addsectionbib
| usepackage
| use(?:|color|font|inner|outer)theme(?:\s*\[[^\]]+\])?
)
\s*{([^}]*)} # first arg
(?: \s*{([^}]*)} )? # maybe another arg
@ -362,6 +368,9 @@ class LaTeX(ScannerBase):
if inc_type in self.two_arg_commands:
inc_subdir = os.path.join(subdir, include[1])
inc_list = include[2].split(',')
elif re.match('use(|color|font|inner|outer)theme', inc_type):
inc_list = [re.sub('use', 'beamer', inc_type) + _ + '.sty' for _ in
include[1].split(',')]
else:
inc_list = include[1].split(',')
for inc in inc_list:
@ -411,7 +420,7 @@ class LaTeX(ScannerBase):
if n is None:
# Do not bother with 'usepackage' warnings, as they most
# likely refer to system-level files
if inc_type != 'usepackage':
if inc_type != 'usepackage' or re.match("use(|color|font|inner|outer)theme", inc_type):
SCons.Warnings.warn(
SCons.Warnings.DependencyWarning,
"No dependency generated for file: %s "

View File

@ -31,9 +31,12 @@ some other module. If it's specific to the "scons" script invocation,
it goes here.
"""
from __future__ import annotations
import SCons.compat
import importlib.util
import optparse
import os
import re
import sys
@ -41,7 +44,6 @@ import time
import traceback
import platform
import threading
from typing import Optional, List, TYPE_CHECKING
import SCons.CacheDir
import SCons.Debug
@ -59,14 +61,13 @@ import SCons.Taskmaster
import SCons.Util
import SCons.Warnings
import SCons.Script.Interactive
if TYPE_CHECKING:
from SCons.Script import SConsOption
from .SConsOptions import SConsOption
from SCons.Util.stats import count_stats, memory_stats, time_stats, ENABLE_JSON, write_scons_stats_file, JSON_OUTPUT_FILE
from SCons import __version__ as SConsVersion
# these define the range of versions SCons supports
minimum_python_version = (3, 6, 0)
minimum_python_version = (3, 7, 0)
deprecated_python_version = (3, 7, 0)
# ordered list of SConstruct names to look for if there is no -f flag
@ -508,29 +509,41 @@ class FakeOptionParser:
# TODO: to quiet checkers, FakeOptionParser should also define
# raise_exception_on_error, preserve_unknown_options, largs and parse_args
def add_local_option(self, *args, **kw) -> "SConsOption":
def add_local_option(self, *args, **kw) -> SConsOption:
pass
OptionsParser = FakeOptionParser()
def AddOption(*args, settable: bool = False, **kw) -> "SConsOption":
def AddOption(*args, **kw) -> SConsOption:
"""Add a local option to the option parser - Public API.
If the *settable* parameter is true, the option will be included in the
list of settable options; all other keyword arguments are passed on to
:meth:`~SCons.Script.SConsOptions.SConsOptionParser.add_local_option`.
If the SCons-specific *settable* kwarg is true (default ``False``),
the option will allow calling :func:``SetOption`.
.. versionchanged:: 4.8.0
The *settable* parameter added to allow including the new option
to the table of options eligible to use :func:`SetOption`.
in the table of options eligible to use :func:`SetOption`.
"""
settable = kw.get('settable', False)
if len(args) == 1 and isinstance(args[0], SConsOption):
# If they passed an SConsOption object, ignore kw - the underlying
# add_option method relies on seeing zero kwargs to recognize this.
# Since we don't support an omitted default, overrwrite optparse's
# marker to get the same effect as setting it in kw otherwise.
optobj = args[0]
if optobj.default is optparse.NO_DEFAULT:
optobj.default = None
# make sure settable attribute exists; positive setting wins
attr_settable = getattr(optobj, "settable")
if attr_settable is None or settable > attr_settable:
optobj.settable = settable
return OptionsParser.add_local_option(*args)
if 'default' not in kw:
kw['default'] = None
kw['settable'] = settable
result = OptionsParser.add_local_option(*args, **kw)
return result
kw['settable'] = settable # just to make sure it gets set
return OptionsParser.add_local_option(*args, **kw)
def GetOption(name: str):
"""Get the value from an option - Public API."""
@ -540,7 +553,7 @@ def SetOption(name: str, value):
"""Set the value of an option - Public API."""
return OptionsParser.values.set_option(name, value)
def DebugOptions(json: Optional[str] = None) -> None:
def DebugOptions(json: str | None = None) -> None:
"""Specify options to SCons debug logic - Public API.
Currently only *json* is supported, which changes the JSON file
@ -669,8 +682,8 @@ def _scons_internal_error() -> None:
sys.exit(2)
def _SConstruct_exists(
dirname: str, repositories: List[str], filelist: List[str]
) -> Optional[str]:
dirname: str, repositories: list[str], filelist: list[str]
) -> str | None:
"""Check that an SConstruct file exists in a directory.
Arguments:
@ -1344,12 +1357,6 @@ def _build_targets(fs, options, targets, target_top):
# various print_* settings, tree_printer list, etc.
BuildTask.options = options
is_pypy = platform.python_implementation() == 'PyPy'
# As of 3.7, python removed support for threadless platforms.
# See https://www.python.org/dev/peps/pep-0011/
is_37_or_later = sys.version_info >= (3, 7)
# python_has_threads = sysconfig.get_config_var('WITH_THREAD') or is_pypy or is_37_or_later
# As of python 3.4 threading has a dummy_threading module for use when there is no threading
# it's get_ident() will allways return -1, while real threading modules get_ident() will
# always return a positive integer
@ -1412,7 +1419,7 @@ def _exec_main(parser, values) -> None:
class SConsPdb(pdb.Pdb):
"""Specialization of Pdb to help find SConscript files."""
def lookupmodule(self, filename: str) -> Optional[str]:
def lookupmodule(self, filename: str) -> str | None:
"""Helper function for break/clear parsing -- SCons version.
Translates (possibly incomplete) file or module name

View File

@ -21,6 +21,8 @@
# OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
# WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
from __future__ import annotations
import gettext
import optparse
import re
@ -73,7 +75,7 @@ class SConsValues(optparse.Values):
1. set on the command line.
2. set in an SConscript file via :func:`~SCons.Script.Main.SetOption`.
3. the default setting (from the the ``op.add_option()``
calls in the :func:`Parser` function, below).
calls in the :func:`Parser` function.
The command line always overrides a value set in a SConscript file,
which in turn always overrides default settings. Because we want
@ -144,16 +146,14 @@ class SConsValues(optparse.Values):
]
def set_option(self, name: str, value) -> None:
"""Sets an option *name* from an SConscript file.
"""Set an option value from a :func:`~SCons.Script.Main.SetOption` call.
Vvalidation steps for known (that is, defined in SCons itself)
options are in-line here. Validation should be along the same
lines as for options processed from the command line -
it's kind of a pain to have to duplicate. Project-defined options
can specify callbacks for the command-line version, but will have
no inbuilt validation here. It's up to the build system maintainer
to make sure :func:`~SCons.Script.Main.SetOption` is being used
correctly, we can't really do any better here.
Validation steps for settable options (those defined in SCons
itself) are in-line here. Duplicates the logic for the matching
command-line options in :func:`Parse` - these need to be kept
in sync. Cannot provide validation for options added via
:func:`~SCons.Script.Main.AddOption` since we don't know about those
ahead of time - it is up to the developer to figure that out.
Raises:
UserError: the option is not settable.
@ -233,13 +233,37 @@ class SConsValues(optparse.Values):
class SConsOption(optparse.Option):
def convert_value(self, opt, value):
"""SCons added option.
Changes :attr:`CHECK_METHODS` and :attr:`CONST_ACTIONS` settings from
:class:`optparse.Option` base class to tune for our usage.
New function :meth:`_check_nargs_optional` implements the ``nargs=?``
syntax from :mod:`argparse`, and is added to the ``CHECK_METHODS`` list.
Overridden :meth:`convert_value` supports this usage.
.. versionchanged:: 4.9.0
The *settable* attribute is added to ``ATTRS``, allowing it to be
set in the option. A parameter to mark the option settable was added
in 4.8.0, but was not initially made part of the option object itself.
"""
# can uncomment to have a place to trap SConsOption creation for debugging:
# def __init__(self, *args, **kwargs):
# super().__init__(*args, **kwargs)
def convert_value(self, opt: str, value):
"""SCons override: recognize nargs="?"."""
if value is not None:
if self.nargs in (1, '?'):
return self.check_value(opt, value)
return tuple([self.check_value(opt, v) for v in value])
def process(self, opt, value, values, parser):
"""Process a value.
Direct copy of optparse version including the comments -
we don't change anything so this could just be dropped.
"""
# First, convert the value(s) to the right type. Howl if any
# value(s) are bogus.
value = self.convert_value(opt, value)
@ -250,15 +274,17 @@ class SConsOption(optparse.Option):
return self.take_action(
self.action, self.dest, opt, value, values, parser)
def _check_nargs_optional(self):
def _check_nargs_optional(self) -> None:
"""SCons added: deal with optional option-arguments."""
if self.nargs == '?' and self._short_opts:
fmt = "option %s: nargs='?' is incompatible with short options"
raise SCons.Errors.UserError(fmt % self._short_opts[0])
ATTRS = optparse.Option.ATTRS + ['settable'] # added for SCons
CHECK_METHODS = optparse.Option.CHECK_METHODS
if CHECK_METHODS is None:
CHECK_METHODS = []
CHECK_METHODS = CHECK_METHODS + [_check_nargs_optional]
CHECK_METHODS += [_check_nargs_optional] # added for SCons
CONST_ACTIONS = optparse.Option.CONST_ACTIONS + optparse.Option.TYPED_ACTIONS
@ -270,8 +296,8 @@ class SConsOptionGroup(optparse.OptionGroup):
lined up with the normal "SCons Options".
"""
def format_help(self, formatter):
""" Format an option group's help text.
def format_help(self, formatter) -> str:
"""SCons-specific formatting of an option group's help text.
The title is dedented so it's flush with the "SCons Options"
title we print at the top.
@ -285,14 +311,15 @@ class SConsOptionGroup(optparse.OptionGroup):
class SConsBadOptionError(optparse.BadOptionError):
"""Exception used to indicate that invalid command line options were specified
:ivar str opt_str: The offending option specified on command line which is not recognized
:ivar OptionParser parser: The active argument parser
"""Raised if an invalid option value is encountered on the command line.
Attributes:
opt_str: The unrecognized command-line option.
parser: The active argument parser.
"""
# TODO why is 'parser' needed? Not called in current code base.
def __init__(self, opt_str, parser=None) -> None:
def __init__(self, opt_str: str, parser: SConsOptionParser | None = None) -> None:
self.opt_str = opt_str
self.parser = parser
@ -304,8 +331,8 @@ class SConsOptionParser(optparse.OptionParser):
preserve_unknown_options = False
raise_exception_on_error = False
def error(self, msg):
"""Overridden OptionValueError exception handler."""
def error(self, msg: str) -> None:
"""SCons-specific handling of option errors."""
if self.raise_exception_on_error:
raise SConsBadOptionError(msg, self)
else:
@ -313,15 +340,16 @@ class SConsOptionParser(optparse.OptionParser):
sys.stderr.write("SCons Error: %s\n" % msg)
sys.exit(2)
def _process_long_opt(self, rargs, values):
""" SCons-specific processing of long options.
def _process_long_opt(self, rargs, values) -> None:
"""SCons-specific processing of long options.
This is copied directly from the normal
``optparse._process_long_opt()`` method, except that, if configured
to do so, we catch the exception thrown when an unknown option
is encountered and just stick it back on the "leftover" arguments
for later (re-)processing. This is because we may see the option
definition later, while processing SConscript files.
This is copied directly from the normal Optparse
:meth:`~optparse.OptionParser._process_long_opt` method, except
that, if configured to do so, we catch the exception thrown
when an unknown option is encountered and just stick it back
on the "leftover" arguments for later (re-)processing. This is
because we may see the option definition later, while processing
SConscript files.
"""
arg = rargs.pop(0)
@ -341,9 +369,9 @@ class SConsOptionParser(optparse.OptionParser):
% (opt, self._match_long_opt(opt))
)
except optparse.BadOptionError:
# SCons addition: if requested, add unknown options to
# the "leftover arguments" list for later processing.
if self.preserve_unknown_options:
# SCons-specific: if requested, add unknown options to
# the "leftover arguments" list for later processing.
self.largs.append(arg)
if had_explicit_value:
# The unknown option will be re-processed later,
@ -355,6 +383,7 @@ class SConsOptionParser(optparse.OptionParser):
option = self._long_opt[opt]
if option.takes_value():
nargs = option.nargs
# SCons addition: recognize '?' for nargs
if nargs == '?':
if had_explicit_value:
value = rargs.pop(0)
@ -362,6 +391,7 @@ class SConsOptionParser(optparse.OptionParser):
value = option.const
elif len(rargs) < nargs:
if nargs == 1:
# SCons addition: nicer msg if option had choices
if not option.choices:
self.error(_("%s option requires an argument") % opt)
else:
@ -386,6 +416,66 @@ class SConsOptionParser(optparse.OptionParser):
option.process(opt, value, values, self)
def _process_short_opts(self, rargs, values) -> None:
"""SCons-specific processing of short options.
This is copied directly from the normal Optparse
:meth:`~optparse.OptionParser._process_short_opts` method, except
that, if configured to do so, we catch the exception thrown
when an unknown option is encountered and just stick it back
on the "leftover" arguments for later (re-)processing. This is
because we may see the option definition later, while processing
SConscript files.
"""
arg = rargs.pop(0)
stop = False
i = 1
for ch in arg[1:]:
opt = "-" + ch
option = self._short_opt.get(opt)
i += 1 # we have consumed a character
try:
if not option:
raise optparse.BadOptionError(opt)
except optparse.BadOptionError:
# SCons addition: if requested, add unknown options to
# the "leftover arguments" list for later processing.
if self.preserve_unknown_options:
self.largs.append(arg)
return
raise
if option.takes_value():
# Any characters left in arg? Pretend they're the
# next arg, and stop consuming characters of arg.
if i < len(arg):
rargs.insert(0, arg[i:])
stop = True
nargs = option.nargs
if len(rargs) < nargs:
if nargs == 1:
self.error(_("%s option requires an argument") % opt)
else:
self.error(_("%s option requires %d arguments")
% (opt, nargs))
elif nargs == 1:
value = rargs.pop(0)
else:
value = tuple(rargs[0:nargs])
del rargs[0:nargs]
else: # option doesn't take a value
value = None
option.process(opt, value, values, self)
if stop:
break
def reparse_local_options(self) -> None:
"""Re-parse the leftover command-line options.
@ -399,10 +489,8 @@ class SConsOptionParser(optparse.OptionParser):
allow exact matches for long-opts only (no partial argument names!).
Otherwise there could be problems in :meth:`add_local_option`
below. When called from there, we try to reparse the
command-line arguments that
1. haven't been processed so far (`self.largs`), but
2. are possibly not added to the list of options yet.
command-line arguments that haven't been processed so far
(``self.largs``), but are possibly not added to the options list yet.
So, when we only have a value for ``--myargument`` so far,
a command-line argument of ``--myarg=test`` would set it,
@ -450,29 +538,31 @@ class SConsOptionParser(optparse.OptionParser):
self.largs = self.largs + largs_restore
def add_local_option(self, *args, **kw) -> SConsOption:
""" Adds a local option to the parser.
"""Add a local option to the parser.
This is initiated by an :func:`~SCons.Script.Main.AddOption` call to
add a user-defined command-line option. Add the option to a separate
option group for the local options, creating the group if necessary.
This is the implementation of :func:`~SCons.Script.Main.AddOption`,
to add a project-defined command-line option. Local options
are added to a separate option group, which is created if necessary.
The keyword argument *settable* is recognized specially (and
removed from *kw*). If true, the option is marked as modifiable;
by default "local" (project-added) options are not eligible for
for :func:`~SCons.Script.Main.SetOption` calls.
.. versionchanged:: 4.8.0
Added special handling of *settable*.
:func:`~SCons.Script.Main.SetOption` calls.
.. versionchanged:: NEXT_VERSION
If the option's *settable* attribute is true, it is added to
the :attr:`SConsValues.settable` list. *settable* handling was
added in 4.8.0, but was not made an option attribute at the time.
"""
group: SConsOptionGroup
try:
group = self.local_option_group
except AttributeError:
group = SConsOptionGroup(self, 'Local Options')
group = self.add_option_group(group)
self.add_option_group(group)
self.local_option_group = group
settable = kw.pop('settable')
# this gives us an SConsOption due to the setting of self.option_class
result = group.add_option(*args, **kw)
if result:
# The option was added successfully. We now have to add the
@ -483,15 +573,16 @@ class SConsOptionParser(optparse.OptionParser):
# any value overridden on the command line is immediately
# available if the user turns around and does a GetOption()
# right away.
# TODO: what if dest is None?
setattr(self.values.__defaults__, result.dest, result.default)
self.reparse_local_options()
if settable:
if result.settable:
SConsValues.settable.append(result.dest)
return result
def format_local_option_help(self, formatter=None, file=None):
"""Return the help for the project-level ("local") options.
"""Return the help for the project-level ("local") SCons options.
.. versionadded:: 4.6.0
"""
@ -514,7 +605,7 @@ class SConsOptionParser(optparse.OptionParser):
return local_help
def print_local_option_help(self, file=None):
"""Print help for just project-defined options.
"""Print help for just local SCons options.
Writes to *file* (default stdout).
@ -527,11 +618,11 @@ class SConsOptionParser(optparse.OptionParser):
class SConsIndentedHelpFormatter(optparse.IndentedHelpFormatter):
def format_usage(self, usage) -> str:
""" Formats the usage message. """
"""Format the usage message for SCons."""
return "usage: %s\n" % usage
def format_heading(self, heading):
""" Translates heading to "SCons Options"
"""Translate heading to "SCons Options"
Heading of "Options" changed to "SCons Options."
Unfortunately, we have to do this here, because those titles
@ -542,11 +633,10 @@ class SConsIndentedHelpFormatter(optparse.IndentedHelpFormatter):
return super().format_heading(heading)
def format_option(self, option):
""" Customized option formatter.
"""SCons-specific option formatter.
A copy of the normal ``optparse.IndentedHelpFormatter.format_option()``
method. This has been snarfed so we can modify text wrapping to
our liking:
A copy of the :meth:`optparse.IndentedHelpFormatter.format_option`
method. Overridden so we can modify text wrapping to our liking:
* add our own regular expression that doesn't break on hyphens
(so things like ``--no-print-directory`` don't get broken).
@ -556,21 +646,25 @@ class SConsIndentedHelpFormatter(optparse.IndentedHelpFormatter):
The help for each option consists of two parts:
* the opt strings and metavars e.g. ("-x", or
"-fFILENAME, --file=FILENAME")
* the opt strings and metavars e.g. (``-x``, or
``-fFILENAME, --file=FILENAME``)
* the user-supplied help string e.g.
("turn on expert mode", "read data from FILENAME")
(``turn on expert mode``, ``read data from FILENAME``)
If possible, we write both of these on the same line::
-x turn on expert mode
But if the opt string list is too long, we put the help
If the opt string list is too long, we put the help
string on a second line, indented to the same column it would
start in if it fit on the first line::
-fFILENAME, --file=FILENAME
read data from FILENAME
Help strings are wrapped for terminal width and do not preserve
any hand-made formatting that may have been used in the ``AddOption``
call, so don't attempt prettying up a list of choices (for example).
"""
result = []
opts = self.option_strings[option]

View File

@ -23,6 +23,8 @@
"""This module defines the Python API provided to SConscript files."""
from __future__ import annotations
import SCons
import SCons.Action
import SCons.Builder
@ -45,7 +47,6 @@ import re
import sys
import traceback
import time
from typing import Tuple
class SConscriptReturn(Exception):
pass
@ -386,7 +387,7 @@ class SConsEnvironment(SCons.Environment.Base):
# Private methods of an SConsEnvironment.
#
@staticmethod
def _get_major_minor_revision(version_string: str) -> Tuple[int, int, int]:
def _get_major_minor_revision(version_string: str) -> tuple[int, int, int]:
"""Split a version string into major, minor and (optionally)
revision parts.
@ -485,7 +486,7 @@ class SConsEnvironment(SCons.Environment.Base):
SCons.Script._Set_Default_Targets(self, targets)
@staticmethod
def GetSConsVersion() -> Tuple[int, int, int]:
def GetSConsVersion() -> tuple[int, int, int]:
"""Return the current SCons version.
.. versionadded:: 4.8.0
@ -535,25 +536,27 @@ class SConsEnvironment(SCons.Environment.Base):
name = self.subst(name)
return SCons.Script.Main.GetOption(name)
def Help(self, text, append: bool = False, keep_local: bool = False) -> None:
def Help(self, text, append: bool = False, local_only: bool = False) -> None:
"""Update the help text.
The previous help text has *text* appended to it, except on the
first call. On first call, the values of *append* and *keep_local*
first call. On first call, the values of *append* and *local_only*
are considered to determine what is appended to.
Arguments:
text: string to add to the help text.
append: on first call, if true, keep the existing help text
(default False).
keep_local: on first call, if true and *append* is also true,
local_only: on first call, if true and *append* is also true,
keep only the help text from AddOption calls.
.. versionchanged:: 4.6.0
The *keep_local* parameter was added.
.. versionchanged:: 4.9.0
The *keep_local* parameter was renamed *local_only* to match manpage
"""
text = self.subst(text, raw=1)
SCons.Script.HelpFunction(text, append=append, keep_local=keep_local)
SCons.Script.HelpFunction(text, append=append, local_only=local_only)
def Import(self, *vars):
try:

View File

@ -35,6 +35,7 @@ import time
start_time = time.time()
import collections
import itertools
import os
from io import StringIO
@ -53,9 +54,17 @@ import sys
# to not add the shims. So we use a special-case, up-front check for
# the "--debug=memoizer" flag and enable Memoizer before we import any
# of the other modules that use it.
# Update: this breaks if the option isn't exactly "--debug=memoizer",
# like if there is more than one debug option as a csv. Do a bit more work.
_args = sys.argv + os.environ.get('SCONSFLAGS', '').split()
if "--debug=memoizer" in _args:
_args = sys.argv + os.environ.get("SCONSFLAGS", "").split()
_args = (
arg[len("--debug=") :].split(",")
for arg in _args
if arg.startswith("--debug=")
)
_args = list(itertools.chain.from_iterable(_args))
if "memoizer" in _args:
import SCons.Memoize
import SCons.Warnings
try:
@ -251,19 +260,21 @@ def _Set_Default_Targets(env, tlist) -> None:
help_text = None
def HelpFunction(text, append: bool = False, keep_local: bool = False) -> None:
def HelpFunction(text, append: bool = False, local_only: bool = False) -> None:
"""The implementaion of the the ``Help`` method.
See :meth:`~SCons.Script.SConscript.Help`.
.. versionchanged:: 4.6.0
The *keep_local* parameter was added.
.. versionchanged:: 4.9.0
The *keep_local* parameter was renamed *local_only* to match manpage
"""
global help_text
if help_text is None:
if append:
with StringIO() as s:
PrintHelp(s, local_only=keep_local)
PrintHelp(s, local_only=local_only)
help_text = s.getvalue()
else:
help_text = ""

View File

@ -23,10 +23,11 @@
"""SCons string substitution."""
from __future__ import annotations
import collections
import re
from inspect import signature, Parameter
from typing import Optional
import SCons.Errors
from SCons.Util import is_String, is_Sequence
@ -807,7 +808,7 @@ _separate_args = re.compile(r'(%s|\s+|[^\s$]+|\$)' % _dollar_exps_str)
_space_sep = re.compile(r'[\t ]+(?![^{]*})')
def scons_subst(strSubst, env, mode=SUBST_RAW, target=None, source=None, gvars={}, lvars={}, conv=None, overrides: Optional[dict] = None):
def scons_subst(strSubst, env, mode=SUBST_RAW, target=None, source=None, gvars={}, lvars={}, conv=None, overrides: dict | None = None):
"""Expand a string or list containing construction variable
substitutions.
@ -889,7 +890,7 @@ def scons_subst(strSubst, env, mode=SUBST_RAW, target=None, source=None, gvars={
return result
def scons_subst_list(strSubst, env, mode=SUBST_RAW, target=None, source=None, gvars={}, lvars={}, conv=None, overrides: Optional[dict] = None):
def scons_subst_list(strSubst, env, mode=SUBST_RAW, target=None, source=None, gvars={}, lvars={}, conv=None, overrides: dict | None = None):
"""Substitute construction variables in a string (or list or other
object) and separate the arguments into a command list.

View File

@ -23,9 +23,10 @@
"""Routines for setting up Fortran, common to all dialects."""
from __future__ import annotations
import re
import os.path
from typing import Tuple, List
import SCons.Scanner.Fortran
import SCons.Tool
@ -96,7 +97,7 @@ def ShFortranEmitter(target, source, env) -> Tuple:
return SharedObjectEmitter(target, source, env)
def ComputeFortranSuffixes(suffixes: List[str], ppsuffixes: List[str]) -> None:
def ComputeFortranSuffixes(suffixes: list[str], ppsuffixes: list[str]) -> None:
"""Update the suffix lists to reflect the platform requirements.
If upper-cased suffixes can be distinguished from lower, those are
@ -119,7 +120,7 @@ def ComputeFortranSuffixes(suffixes: List[str], ppsuffixes: List[str]) -> None:
def CreateDialectActions(
dialect: str,
) -> Tuple[CommandAction, CommandAction, CommandAction, CommandAction]:
) -> tuple[CommandAction, CommandAction, CommandAction, CommandAction]:
"""Create dialect specific actions."""
CompAction = Action(f'${dialect}COM ', cmdstr=f'${dialect}COMSTR')
CompPPAction = Action(f'${dialect}PPCOM ', cmdstr=f'${dialect}PPCOMSTR')
@ -131,8 +132,8 @@ def CreateDialectActions(
def DialectAddToEnv(
env,
dialect: str,
suffixes: List[str],
ppsuffixes: List[str],
suffixes: list[str],
ppsuffixes: list[str],
support_mods: bool = False,
) -> None:
"""Add dialect specific construction variables.

View File

@ -23,11 +23,12 @@
"""Common routines for processing Java. """
from __future__ import annotations
import os
import re
import glob
from pathlib import Path
from typing import List
import SCons.Util
@ -491,7 +492,7 @@ else:
return os.path.split(fn)
def get_java_install_dirs(platform, version=None) -> List[str]:
def get_java_install_dirs(platform, version=None) -> list[str]:
""" Find possible java jdk installation directories.
Returns a list for use as `default_paths` when looking up actual
@ -540,7 +541,7 @@ def get_java_install_dirs(platform, version=None) -> List[str]:
return []
def get_java_include_paths(env, javac, version) -> List[str]:
def get_java_include_paths(env, javac, version) -> list[str]:
"""Find java include paths for JNI building.
Cannot be called in isolation - `javac` refers to an already detected

View File

@ -34,15 +34,25 @@ from contextlib import suppress
from subprocess import DEVNULL, PIPE
from pathlib import Path
import SCons.Errors
import SCons.Util
import SCons.Warnings
class MSVCCacheInvalidWarning(SCons.Warnings.WarningOnByDefault):
pass
def _check_logfile(logfile):
if logfile and '"' in logfile:
err_msg = (
"SCONS_MSCOMMON_DEBUG value contains double quote character(s)\n"
f" SCONS_MSCOMMON_DEBUG={logfile}"
)
raise SCons.Errors.UserError(err_msg)
return logfile
# SCONS_MSCOMMON_DEBUG is internal-use so undocumented:
# set to '-' to print to console, else set to filename to log to
LOGFILE = os.environ.get('SCONS_MSCOMMON_DEBUG')
LOGFILE = _check_logfile(os.environ.get('SCONS_MSCOMMON_DEBUG'))
if LOGFILE:
import logging
@ -129,7 +139,15 @@ if LOGFILE:
log_handler = logging.StreamHandler(sys.stdout)
else:
log_prefix = ''
log_handler = logging.FileHandler(filename=LOGFILE)
try:
log_handler = logging.FileHandler(filename=LOGFILE)
except (OSError, FileNotFoundError) as e:
err_msg = (
"Could not create logfile, check SCONS_MSCOMMON_DEBUG\n"
f" SCONS_MSCOMMON_DEBUG={LOGFILE}\n"
f" {e.__class__.__name__}: {str(e)}"
)
raise SCons.Errors.UserError(err_msg)
log_formatter = _CustomFormatter(log_prefix)
log_handler.setFormatter(log_formatter)
logger = logging.getLogger(name=__name__)

View File

@ -33,10 +33,11 @@ one needs to use or tie in to this subsystem in order to roll their own
tool specifications.
"""
from __future__ import annotations
import sys
import os
import importlib.util
from typing import Optional
import SCons.Builder
import SCons.Errors
@ -101,6 +102,7 @@ TOOL_ALIASES = {
'gettext': 'gettext_tool',
'clang++': 'clangxx',
'as': 'asm',
'ninja' : 'ninja_tool'
}
@ -691,8 +693,8 @@ def tool_list(platform, env):
if str(platform) == 'win32':
"prefer Microsoft tools on Windows"
linkers = ['mslink', 'gnulink', 'ilink', 'linkloc', 'ilink32']
c_compilers = ['msvc', 'mingw', 'gcc', 'intelc', 'icl', 'icc', 'cc', 'bcc32']
cxx_compilers = ['msvc', 'intelc', 'icc', 'g++', 'cxx', 'bcc32']
c_compilers = ['msvc', 'mingw', 'gcc', 'clang', 'intelc', 'icl', 'icc', 'cc', 'bcc32']
cxx_compilers = ['msvc', 'intelc', 'icc', 'g++', 'clang++', 'cxx', 'bcc32']
assemblers = ['masm', 'nasm', 'gas', '386asm']
fortran_compilers = ['gfortran', 'g77', 'ifl', 'cvf', 'f95', 'f90', 'fortran']
ars = ['mslib', 'ar', 'tlib']
@ -757,8 +759,8 @@ def tool_list(platform, env):
else:
"prefer GNU tools on all other platforms"
linkers = ['gnulink', 'ilink']
c_compilers = ['gcc', 'intelc', 'icc', 'cc']
cxx_compilers = ['g++', 'intelc', 'icc', 'cxx']
c_compilers = ['gcc', 'clang', 'intelc', 'icc', 'cc']
cxx_compilers = ['g++', 'clang++', 'intelc', 'icc', 'cxx']
assemblers = ['gas', 'nasm', 'masm']
fortran_compilers = ['gfortran', 'g77', 'ifort', 'ifl', 'f95', 'f90', 'f77']
ars = ['ar', ]
@ -824,7 +826,7 @@ def tool_list(platform, env):
return [x for x in tools if x]
def find_program_path(env, key_program, default_paths=None, add_path: bool=False) -> Optional[str]:
def find_program_path(env, key_program, default_paths=None, add_path: bool=False) -> str | None:
"""
Find the location of a tool using various means.

View File

@ -43,6 +43,8 @@ from .cxx import CXXSuffixes
from .cc import CSuffixes
from .asm import ASSuffixes, ASPPSuffixes
DEFAULT_DB_NAME = 'compile_commands.json'
# TODO: Is there a better way to do this than this global? Right now this exists so that the
# emitter we add can record all of the things it emits, so that the scanner for the top level
# compilation database can access the complete list, and also so that the writer has easy
@ -189,9 +191,8 @@ def compilation_db_emitter(target, source, env):
if not target and len(source) == 1:
target = source
# Default target name is compilation_db.json
if not target:
target = ['compile_commands.json', ]
target = [DEFAULT_DB_NAME]
# No source should have been passed. Drop it.
if source:
@ -224,13 +225,17 @@ def generate(env, **kwargs) -> None:
),
itertools.product(
ASSuffixes,
[(static_obj, SCons.Defaults.StaticObjectEmitter, "$ASCOM")],
[(shared_obj, SCons.Defaults.SharedObjectEmitter, "$ASCOM")],
[
(static_obj, SCons.Defaults.StaticObjectEmitter, "$ASCOM"),
(shared_obj, SCons.Defaults.SharedObjectEmitter, "$ASCOM")
],
),
itertools.product(
ASPPSuffixes,
[(static_obj, SCons.Defaults.StaticObjectEmitter, "$ASPPCOM")],
[(shared_obj, SCons.Defaults.SharedObjectEmitter, "$ASPPCOM")],
[
(static_obj, SCons.Defaults.StaticObjectEmitter, "$ASPPCOM"),
(shared_obj, SCons.Defaults.SharedObjectEmitter, "$ASPPCOM")
],
),
)

Some files were not shown because too many files have changed in this diff Show More