Repository: SCons/scons

Issue:
Install()'d files may not be writable (regression)
SCons 4.0.0 introduced a change to the code in the `Install` builder (and relatives) which stopped making the "installed" files writeable, unconditionally.  Although this behavior was not documented, dropping it is a regression from behavior users are depending on, namely the ability to do a subsequent build and not having it fail because existing files in the install location are read-only due to their source having been read-only, and now they can't be copied on top of.

The situation raises plenty of questions about "correct" behavior but those will be tracked elsewhere, this ticket is filed to restore the previous behavior, at least until some sensible alternative can be introduced.

scons-users mailing list link: http://four.pairlist.net/pipermail/scons-users/2021-April/008504.html


Discussion:
Thanks for opening this issue.

Our use case is that we are distributing big binary data (resource files and dlls) to our build servers and developer machines with Perforce, which by default keeps files as read-only in its workspace. Our scons build copies files from there into the destination directory where our final built product ends up.

Whenever the source files change, SCons will copy the updated files to the destination directory.

We've relied on this use case working for a number of years, but it stopped working for us in SCons 4.0.0, since the Install builder now propagates the read-only property of the source file in this case. It now fails to overwrite a previously copied file unless we manually delete it first. Since a developer doesn't know which files to delete, he/she instead ends up clearing our entire target folder (including built targets) every time this happens.

One can certainly debate what is "correct". I would vote for a pragmatic solution, where SCons implements what is the most useful in typical build system use cases. Restoring the old behaviour until a better solution could be implemented would be great for us at least, and as far as I know it hasn't bothered a lot of user in the past.

I don't mind if proper thought is given to it though. It's good to get this kind of behaviour right.

Resolving patch:
diff --git a/SCons/Tool/install.py b/SCons/Tool/install.py
--- a/SCons/Tool/install.py
+++ b/SCons/Tool/install.py
@@ -31,6 +31,7 @@
 #
 
 import os
+import stat
 from shutil import copy2, copymode, copystat
 
 import SCons.Action
@@ -152,11 +153,14 @@ def scons_copytree(src, dst, symlinks=False, ignore=None, copy_function=copy2,
 #
 # Functions doing the actual work of the Install Builder.
 #
-def copyFunc(dest, source, env):
-    """Install a source file or directory into a destination by copying,
+def copyFunc(dest, source, env) -> int:
+    """Install a source file or directory into a destination by copying.
 
-    Mode/permissions bits will be copied as well.
+    Mode/permissions bits will be copied as well, except that the target
+    will be made writable.
 
+    Returns:
+        POSIX-style error code - 0 for success, non-zero for fail
     """
     if os.path.isdir(source):
         if os.path.exists(dest):
@@ -169,19 +173,24 @@ def copyFunc(dest, source, env):
         scons_copytree(source, dest, dirs_exist_ok=True)
     else:
         copy2(source, dest)
-        copymode(source, dest)
+        st = os.stat(source)
+        os.chmod(dest, stat.S_IMODE(st[stat.ST_MODE]) | stat.S_IWRITE)
 
     return 0
 
 #
 # Functions doing the actual work of the InstallVersionedLib Builder.
 #
-def copyFuncVersionedLib(dest, source, env):
-    """Install a versioned library into a destination by copying,
+def copyFuncVersionedLib(dest, source, env) -> int:
+    """Install a versioned library into a destination by copying.
 
-    Mode/permissions bits will be copied as well.
     Any required symbolic links for other library names are created.
 
+    Mode/permissions bits will be copied as well, except that the target
+    will be made writable.
+
+    Returns:
+        POSIX-style error code - 0 for success, non-zero for fail
     """
     if os.path.isdir(source):
         raise SCons.Errors.UserError("cannot install directory `%s' as a version library" % str(source) )
@@ -192,7 +201,8 @@ def copyFuncVersionedLib(dest, source, env):
         except:
             pass
         copy2(source, dest)
-        copymode(source, dest)
+        st = os.stat(source)
+        os.chmod(dest, stat.S_IMODE(st[stat.ST_MODE]) | stat.S_IWRITE)
         installShlibLinks(dest, source, env)
 
     return 0
@@ -223,38 +233,58 @@ def installShlibLinks(dest, source, env):
         CreateLibSymlinks(env, symlinks)
     return
 
-def installFunc(target, source, env):
-    """Install a source file into a target using the function specified
-    as the INSTALL construction variable."""
+def installFunc(target, source, env) -> int:
+    """Install a source file into a target.
+
+    Uses the function specified in the INSTALL construction variable.
+
+    Returns:
+        POSIX-style error code - 0 for success, non-zero for fail
+    """
+
     try:
         install = env['INSTALL']
     except KeyError:
         raise SCons.Errors.UserError('Missing INSTALL construction variable.')
 
-    assert len(target)==len(source), \
-           "Installing source %s into target %s: target and source lists must have same length."%(list(map(str, source)), list(map(str, target)))
-    for t,s in zip(target,source):
-        if install(t.get_path(),s.get_path(),env):
+    assert len(target) == len(source), (
+        "Installing source %s into target %s: "
+        "target and source lists must have same length."
+        % (list(map(str, source)), list(map(str, target)))
+    )
+    for t, s in zip(target, source):
+        if install(t.get_path(), s.get_path(), env):
             return 1
 
     return 0
 
-def installFuncVersionedLib(target, source, env):
-    """Install a versioned library into a target using the function specified
-    as the INSTALLVERSIONEDLIB construction variable."""
+def installFuncVersionedLib(target, source, env) -> int:
+    """Install a versioned library into a target.
+
+    Uses the function specified in the INSTALL construction variable.
+
+    Returns:
+        POSIX-style error code - 0 for success, non-zero for fail
+    """
+
     try:
         install = env['INSTALLVERSIONEDLIB']
     except KeyError:
-        raise SCons.Errors.UserError('Missing INSTALLVERSIONEDLIB construction variable.')
-
-    assert len(target)==len(source), \
-           "Installing source %s into target %s: target and source lists must have same length."%(list(map(str, source)), list(map(str, target)))
-    for t,s in zip(target,source):
+        raise SCons.Errors.UserError(
+            'Missing INSTALLVERSIONEDLIB construction variable.'
+        )
+
+    assert len(target) == len(source), (
+        "Installing source %s into target %s: "
+        "target and source lists must have same length."
+        % (list(map(str, source)), list(map(str, target)))
+    )
+    for t, s in zip(target, source):
         if hasattr(t.attributes, 'shlibname'):
             tpath = os.path.join(t.get_dir(), t.attributes.shlibname)
         else:
             tpath = t.get_path()
-        if install(tpath,s.get_path(),env):
+        if install(tpath, s.get_path(), env):
             return 1
 
     return 0
@@ -461,12 +491,12 @@ def generate(env):
     try:
         env['INSTALL']
     except KeyError:
-        env['INSTALL']    = copyFunc
+        env['INSTALL'] = copyFunc
 
     try:
         env['INSTALLVERSIONEDLIB']
     except KeyError:
-        env['INSTALLVERSIONEDLIB']    = copyFuncVersionedLib
+        env['INSTALLVERSIONEDLIB'] = copyFuncVersionedLib
 
 def exists(env):
     return 1


Test patch:
diff --git a/test/Install/Clone.py b/test/Install/Clone.py
--- a/test/Install/Clone.py
+++ b/test/Install/Clone.py
@@ -1,6 +1,8 @@
 #!/usr/bin/env python
 #
-# __COPYRIGHT__
+# MIT Licenxe
+#
+# 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,6 @@
 # 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__"
 
 """
 Verify that we can Install() and InstallAs() from a construction
diff --git a/test/Install/INSTALLSTR.py b/test/Install/INSTALLSTR.py
--- a/test/Install/INSTALLSTR.py
+++ b/test/Install/INSTALLSTR.py
@@ -1,6 +1,8 @@
 #!/usr/bin/env python
 #
-# __COPYRIGHT__
+# MIT Licenxe
+#
+# 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,6 @@
 # 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 that the $INSTALLSTR variable is displayed when we install a file.
diff --git a/test/Install/Install-ro.py b/test/Install/Install-ro.py
new file mode 100644
--- /dev/null
+++ b/test/Install/Install-ro.py
@@ -0,0 +1,43 @@
+#!/usr/bin/env python
+#
+# MIT Licenxe
+#
+# 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 that SCons allows Install on top of an existing read-only file.
+"""
+
+import TestSCons
+
+test = TestSCons.TestSCons()
+
+test.file_fixture('fixture/SConstruct-multi', 'SConstruct')
+test.run(arguments=["-Q"])
+test.run(arguments=["-Q"])
+
+test.pass_test()
+
+# Local Variables:
+# tab-width:4
+# indent-tabs-mode:nil
+# End:
diff --git a/test/Install/Install.py b/test/Install/Install.py
--- a/test/Install/Install.py
+++ b/test/Install/Install.py
@@ -1,6 +1,8 @@
 #!/usr/bin/env python
 #
-# __COPYRIGHT__
+# MIT Licenxe
+#
+# 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,6 @@
 # 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__"
 
 """
 Verify that the Install() Builder works
diff --git a/test/Install/InstallAs.py b/test/Install/InstallAs.py
--- a/test/Install/InstallAs.py
+++ b/test/Install/InstallAs.py
@@ -1,6 +1,8 @@
 #!/usr/bin/env python
 #
-# __COPYRIGHT__
+# MIT Licenxe
+#
+# 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,6 @@
 # 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 the InstallAs() Environment method.
@@ -83,7 +82,6 @@
 
 test.up_to_date(arguments = '.')
 
-#
 test.pass_test()
 
 # Local Variables:
diff --git a/test/Install/dir-exists.py b/test/Install/dir-exists.py
--- a/test/Install/dir-exists.py
+++ b/test/Install/dir-exists.py
@@ -1,6 +1,8 @@
 #!/usr/bin/env python
 #
-# __COPYRIGHT__
+# MIT Licenxe
+#
+# 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,6 @@
 # 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 using Install() on directories that exist.
@@ -49,7 +48,7 @@
 echo hi > a%sf
 Install directory: "a" as "b%sa"
 """%(os.sep, os.sep)
-test.run(arguments = ["-Q"], stdout = expect)
+test.run(arguments=["-Q"], stdout=expect)
 
 test.must_exist(test.workpath('a', 'f'))
 test.must_exist(test.workpath('b', 'a', 'f'))
diff --git a/test/Install/directories.py b/test/Install/directories.py
--- a/test/Install/directories.py
+++ b/test/Install/directories.py
@@ -1,6 +1,8 @@
 #!/usr/bin/env python
 #
-# __COPYRIGHT__
+# MIT Licenxe
+#
+# 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,6 @@
 # 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 using Install() on directories.
@@ -33,16 +32,18 @@
 
 test = TestSCons.TestSCons()
 
-test.subdir('outside',
-            'work',
-            ['work', 'dir1'],
-            ['work', 'dir1', 'sub'],
-            ['work', 'dir2'],
-            ['work', 'dir2', 'sub'],
-            ['work', 'dir3'],
-            ['work', 'dir3', 'sub'],
-            ['work', 'dir4'],
-            ['work', 'dir4', 'sub'])
+test.subdir(
+    'outside',
+    'work',
+    ['work', 'dir1'],
+    ['work', 'dir1', 'sub'],
+    ['work', 'dir2'],
+    ['work', 'dir2', 'sub'],
+    ['work', 'dir3'],
+    ['work', 'dir3', 'sub'],
+    ['work', 'dir4'],
+    ['work', 'dir4', 'sub'],
+)
 
 test.write(['work', 'SConstruct'], """\
 DefaultEnvironment(tools=[])
@@ -80,7 +81,7 @@
 Install directory: "dir4" as "%s"
 """ % tuple(arguments))
 
-test.run(chdir = 'work', arguments = arguments, stdout = expect)
+test.run(chdir='work', arguments=arguments, stdout=expect)
 
 test.must_match(test.workpath('outside', 'dir1', 'f2'),         "work/dir1/f2\n")
 test.must_match(test.workpath('outside', 'dir1', 'sub', 'f3'),  "work/dir1/sub/f3\n")
diff --git a/test/Install/fixture/SConstruct-multi b/test/Install/fixture/SConstruct-multi
new file mode 100644
--- /dev/null
+++ b/test/Install/fixture/SConstruct-multi
@@ -0,0 +1,24 @@
+# first run creates a src file, makes it read-only, and installs.
+# second run updates src, Install should successfully replace
+# the previous install (read-only attr on Windows might fail it)
+
+import os
+import pathlib
+import stat
+
+destdir = pathlib.Path("bin")
+destdir.mkdir(exist_ok=True)
+
+srcfile = pathlib.Path("hello")
+try:
+    srcfile.chmod(stat.S_IREAD | stat.S_IWRITE)
+except OSError:
+    pass
+
+with srcfile.open(mode="w") as f:
+    print("Hello from ", os.getpid(), file=f)
+srcfile.chmod(stat.S_IREAD)
+
+DefaultEnvironment(tools=[])
+env = Environment(tools=[])
+env.Install('bin', 'hello')
diff --git a/test/Install/multi-dir.py b/test/Install/multi-dir.py
--- a/test/Install/multi-dir.py
+++ b/test/Install/multi-dir.py
@@ -1,6 +1,8 @@
 #!/usr/bin/env python
 #
-# __COPYRIGHT__
+# MIT Licenxe
+#
+# 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,6 @@
 # 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__"
 
 """
 Verify that using Install to create multiple dir hierarchies outside
diff --git a/test/Install/multi-dir/src/SConstruct b/test/Install/multi-dir/src/SConstruct
--- a/test/Install/multi-dir/src/SConstruct
+++ b/test/Install/multi-dir/src/SConstruct
@@ -1,7 +1,8 @@
 # This tests for a bug where installing a sequence dirs and subdirs
 # outside the source tree can cause SCons to fail to create the dest
 # dir.
-import os, os.path, shutil
+import os
+
 DefaultEnvironment(tools=[])
 env=Environment(tools=[])
 dst='../build'
diff --git a/test/Install/multi.py b/test/Install/multi.py
--- a/test/Install/multi.py
+++ b/test/Install/multi.py
@@ -1,6 +1,8 @@
 #!/usr/bin/env python
 #
-# __COPYRIGHT__
+# MIT Licenxe
+#
+# 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,6 @@
 # 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__"
 
 """
 Verify that multiple calls to test.Install() with the same file
@@ -42,8 +41,7 @@
 
 test.write('file1', "file1\n")
 
-test.run(arguments = '.')
-
+test.run(arguments='.')
 test.must_match(['install', 'file1'], "file1\n")
 
 test.pass_test()
diff --git a/test/Install/no-top-relative.py b/test/Install/no-top-relative.py
--- a/test/Install/no-top-relative.py
+++ b/test/Install/no-top-relative.py
@@ -1,6 +1,8 @@
 #!/usr/bin/env python
 #
-# __COPYRIGHT__
+# MIT Licenxe
+#
+# 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,6 @@
 # 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__"
 
 """
 Verify that we can install a file if its file name portion begins
diff --git a/test/Install/non-ascii-name.py b/test/Install/non-ascii-name.py
--- a/test/Install/non-ascii-name.py
+++ b/test/Install/non-ascii-name.py
@@ -1,7 +1,8 @@
-# -*- coding: utf-8 -*-
 #!/usr/bin/env python
 #
-# __COPYRIGHT__
+# MIT Licenxe
+#
+# 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
@@ -21,9 +22,6 @@
 # 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__"
 
 """
 Verify that the Install() Builder works
diff --git a/test/Install/option--install-sandbox.py b/test/Install/option--install-sandbox.py
--- a/test/Install/option--install-sandbox.py
+++ b/test/Install/option--install-sandbox.py
@@ -1,6 +1,8 @@
 #!/usr/bin/env python
 #
-# __COPYRIGHT__
+# MIT Licenxe
+#
+# 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,6 @@
 # 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 the --install-sandbox commandline option for Install() and InstallAs().
@@ -43,11 +42,8 @@
 target_file2_out = os.path.join(target, 'file2.out')
 subdir_file3_in = os.path.join('subdir', 'file3.in')
 target_subdir_file3_out = os.path.join(target, 'subdir', 'file3.out')
-file1_out       = target+os.path.join( target,
-                                       os.path.splitdrive(destdir)[1],
-                                       'file1.out' )
+file1_out = target + os.path.join(target, os.path.splitdrive(destdir)[1], 'file1.out')
 
-#
 test.write('SConstruct', r"""
 DefaultEnvironment(tools=[])
 env = Environment(tools=[], SUBDIR='subdir')
diff --git a/test/Install/tool.py b/test/Install/tool.py
--- a/test/Install/tool.py
+++ b/test/Install/tool.py
@@ -1,6 +1,8 @@
 #!/usr/bin/env python
 #
-# __COPYRIGHT__
+# MIT Licenxe
+#
+# 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,6 @@
 # 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__"
 
 """
 Verify that we can still call Install() and InstallAs() even when
@@ -37,14 +36,14 @@
 
 test.write('SConstruct', """
 DefaultEnvironment(tools=[])
-env = Environment(tools = [])
+env = Environment(tools=[])
 env.Install('iii', 'foo.in')
 env.InstallAs('foo.out', 'foo.in')
 """)
 
 test.write('foo.in', "foo.in\n")
 
-test.run(arguments = '.')
+test.run(arguments='.')
 
 test.must_match(['iii', 'foo.in'], "foo.in\n")
 test.must_match('foo.out', "foo.in\n")
diff --git a/test/Install/wrap-by-attribute.py b/test/Install/wrap-by-attribute.py
--- a/test/Install/wrap-by-attribute.py
+++ b/test/Install/wrap-by-attribute.py
@@ -1,6 +1,8 @@
 #!/usr/bin/env python
 #
-# __COPYRIGHT__
+# MIT Licenxe
+#
+# 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,6 @@
 # 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__"
 
 """
 
@@ -45,6 +44,7 @@
 DefaultEnvironment(tools=[])
 import os.path
 
+
 def cat(env, source, target):
     target = str(target[0])
     with open(target, 'wb') as ofp:
@@ -52,18 +52,23 @@ def cat(env, source, target):
             with open(str(src), 'rb') as ifp:
                 ofp.write(ifp.read())
 
+
 env = Environment(tools=[], DESTDIR='dest')
-env.Append(BUILDERS={'Cat':Builder(action=cat)})
+env.Append(BUILDERS={'Cat': Builder(action=cat)})
 
 env.SconsInternalInstallFunc = env.Install
 env.SconsInternalInstallAsFunc = env.InstallAs
 
+
 def InstallWithDestDir(dir, source):
     abspath = os.path.splitdrive(env.Dir(dir).get_abspath())[1]
-    return env.SconsInternalInstallFunc('$DESTDIR'+abspath, source)
+    return env.SconsInternalInstallFunc('$DESTDIR' + abspath, source)
+
+
 def InstallAsWithDestDir(target, source):
     abspath = os.path.splitdrive(env.File(target).get_abspath())[1]
-    return env.SconsInternalInstallAsFunc('$DESTDIR'+abspath, source)
+    return env.SconsInternalInstallAsFunc('$DESTDIR' + abspath, source)
+
 
 # Add the wrappers directly as attributes.
 env.Install = InstallWithDestDir
@@ -82,7 +87,6 @@ def InstallAsWithDestDir(target, source):
 e2.Install('export', source=t)
 t = e2.Cat(target='f4.out', source='f4.in')
 e2.InstallAs('export/f4-new.out', source=t)
-
 """)
 
 test.write('f1.in', "f1.in\n")
@@ -90,7 +94,7 @@ def InstallAsWithDestDir(target, source):
 test.write('f3.in', "f3.in\n")
 test.write('f4.in', "f4.in\n")
 
-test.run(arguments = '.')
+test.run(arguments='.')
 
 export = os.path.splitdrive(test.workpath('export'))[1]
 
@@ -104,7 +108,7 @@ def InstallAsWithDestDir(target, source):
 test.must_match(f3_out,         "f3.in\n")
 test.must_match(f4_new_out,     "f4.in\n")
 
-test.up_to_date(arguments = '.')
+test.up_to_date(arguments='.')
 
 test.pass_test()