Repository: SCons/scons

Issue:
Generator functions in Textfile subst expansion are being called with for_signature=True
**Describe the bug**
The following program used to write `val` to `target`, but with current HEAD, it now writes `sig`:

```python
env = Environment()

def generator(source, target, env, for_signature):
    if for_signature:
        return "sig"
    return "val"

env['GENERATOR'] = generator

env.Textfile(
    target="target",
    source=[
        "@generated@",
    ],
    SUBST_DICT={
        '@generated@' : '$GENERATOR',
    },
)
```

**Required information**
* Link to SCons Users thread discussing your issue.
Discussed on discord with @bdbaddog and @mwichmann.

* Version of SCons
HEAD

* Version of Python
N/A

* Which python distribution if applicable (python.org, cygwin, anaconda, macports, brew,etc)
N/A

* How you installed SCons
Clone of master

* What Platform are you on? (Linux/Windows and which version)
Linux, but I'm pretty sure this is platform independent

* How to reproduce your issue?  Please include a small self contained reproducer. Likely a SConstruct should do for most issues.
See the above repo. With SCons 4.2 and older the file `target` will be produced and contain `val`, but on HEAD at f6cefb239a24f4662b4b06307d1c9099583a130e it now contains `sig`. This broke one of the MongoDB custom tools.

* How you invoke scons (The command line you're using "scons --flags some_arguments")
You can run the above reproducer just by running `scons`.



Discussion:
Likely this is coming to light due to pr #4021  where `env.subst()` is now called with `env.subst(value, raw=1)` to avoid incorrectly subst'ing `$$(` 

Okay, after getting a headache going through needless recursion, it's pretty clear where this comes from: in `StringSubber`'s `expand` method: 
```python
                s = s(target=lvars['TARGETS'],
                     source=lvars['SOURCES'],
                     env=self.env,
                     for_signature=(self.mode != SUBST_CMD))
```
The mode comes straight down from how we were called into this chain - the value of `raw` to the `env.subst` call (which is made by the Textfile action) is eventually saved as `self.mode`.  My tenous grasp of logic suggests that last kwarg should maybe be `for_signature=(self.mode == SUBST_SIG)`.  Wonder how much stuff that would break...
surprisingly, it breaks nothing in the testsuite, and fixes the case in this report.
Yup. Seems like the right solution to me!
That codes from 2005.. :)

Resolving patch:
diff --git a/SCons/Subst.py b/SCons/Subst.py
--- a/SCons/Subst.py
+++ b/SCons/Subst.py
@@ -416,6 +416,7 @@ def func(l, conv=self.conv, substitute=self.substitute, lvars=lvars):
                 return conv(substitute(l, lvars))
             return list(map(func, s))
         elif callable(s):
+
             # SCons has the unusual Null class where any __getattr__ call returns it's self, 
             # which does not work the signature module, and the Null class returns an empty
             # string if called on, so we make an exception in this condition for Null class
@@ -427,7 +428,7 @@ def func(l, conv=self.conv, substitute=self.substitute, lvars=lvars):
                 s = s(target=lvars['TARGETS'],
                      source=lvars['SOURCES'],
                      env=self.env,
-                     for_signature=(self.mode != SUBST_CMD))
+                     for_signature=(self.mode == SUBST_SIG))
             else:
                 # This probably indicates that it's a callable
                 # object that doesn't match our calling arguments


Test patch:
diff --git a/test/textfile/fixture/SConstruct.issue-4037 b/test/textfile/fixture/SConstruct.issue-4037
new file mode 100644
--- /dev/null
+++ b/test/textfile/fixture/SConstruct.issue-4037
@@ -0,0 +1,18 @@
+env = Environment()
+
+def generator(source, target, env, for_signature):
+    if for_signature:
+        return "sig"
+    return "val"
+
+env['GENERATOR'] = generator
+
+env.Textfile(
+    target="target",
+    source=[
+        "@generated@",
+    ],
+    SUBST_DICT={
+        '@generated@' : '$GENERATOR',
+    },
+)
diff --git a/test/textfile/issue-3540.py b/test/textfile/issue-3540.py
--- a/test/textfile/issue-3540.py
+++ b/test/textfile/issue-3540.py
@@ -1,6 +1,8 @@
 #!/usr/bin/env python
 #
-# __COPYRIGHT__
+# MIT License
+#
+# Copyright The SCons Foundation
 #
 # Permission is hereby granted, free of charge, to any person obtaining
 # a copy of this software and associated documentation files (the
@@ -20,9 +22,7 @@
 # LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
 # OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
 # WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
-#
 
-__revision__ = "__FILE__ __REVISION__ __DATE__ __DEVELOPER__"
 """
 Test for GH Issue 3540
 
diff --git a/test/textfile/issue-3550.py b/test/textfile/issue-3550.py
--- a/test/textfile/issue-3550.py
+++ b/test/textfile/issue-3550.py
@@ -1,6 +1,8 @@
 #!/usr/bin/env python
 #
-# __COPYRIGHT__
+# MIT License
+#
+# Copyright The SCons Foundation
 #
 # Permission is hereby granted, free of charge, to any person obtaining
 # a copy of this software and associated documentation files (the
@@ -20,14 +22,13 @@
 # LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
 # OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
 # WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
-#
 
-__revision__ = "__FILE__ __REVISION__ __DATE__ __DEVELOPER__"
 """
 Test for GH Issue 3550
 
- You can't pass a Windows path in a value to be interpolated, 
- because SCons will try to pass it through re.sub which rejects certain character sequences.
+You can't pass a Windows path in a value to be interpolated, 
+because SCons will try to pass it through re.sub which rejects
+certain character sequences.
 """
 
 import TestSCons
diff --git a/test/textfile/issue-4037.py b/test/textfile/issue-4037.py
new file mode 100644
--- /dev/null
+++ b/test/textfile/issue-4037.py
@@ -0,0 +1,52 @@
+#!/usr/bin/env python
+#
+# MIT License
+#
+# Copyright The SCons Foundation
+#
+# Permission is hereby granted, free of charge, to any person obtaining
+# a copy of this software and associated documentation files (the
+# "Software"), to deal in the Software without restriction, including
+# without limitation the rights to use, copy, modify, merge, publish,
+# distribute, sublicense, and/or sell copies of the Software, and to
+# permit persons to whom the Software is furnished to do so, subject to
+# the following conditions:
+#
+# The above copyright notice and this permission notice shall be included
+# in all copies or substantial portions of the Software.
+#
+# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY
+# KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE
+# WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
+# NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
+# LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
+# OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
+# WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
+
+"""
+Test for GH Issue 4037
+
+The fix for #4031 created a code path where the subst function
+used by textfile was called with SUBST_RAW, which, if the items to
+subst was a callable, caused it to be called with for_signature=True.
+This did not happen previously as the test was "!= SUBST_CMD",
+and the mode coming in was indeed SUBST_CMD.
+"""
+
+import TestSCons
+
+test = TestSCons.TestSCons()
+
+match_mode = 'r'
+
+test.file_fixture('fixture/SConstruct.issue-4037', 'SConstruct')
+
+test.run(arguments='.')
+
+test.must_match(
+    'target.txt',
+    "val",
+    mode=match_mode,
+)
+
+test.pass_test()
diff --git a/test/textfile/textfile.py b/test/textfile/textfile.py
--- a/test/textfile/textfile.py
+++ b/test/textfile/textfile.py
@@ -1,4 +1,5 @@
 #!/usr/bin/env python
+#
 # MIT License
 #
 # Copyright The SCons Foundation