diff --git a/.appveyor.yml b/.appveyor.yml index 0ad936fb59..3c38229db7 100644 --- a/.appveyor.yml +++ b/.appveyor.yml @@ -35,7 +35,7 @@ environment: SCONS_CACHE_MSVC_CONFIG: "true" matrix: # Test oldest and newest supported Pythons, and a subset in between. - # Skipping 3.8, 3.10, 3.12 at this time + # Skipping 3.10, 3.12 at this time - WINPYTHON: "Python313" - WINPYTHON: "Python311" - WINPYTHON: "Python39" @@ -45,7 +45,7 @@ environment: # to fine tune the number and platforms tested matrix: exclude: - # test python 3.7 on Visual Studio 2017 image + # test python 3.8 on Visual Studio 2017 image - image: Visual Studio 2017 WINPYTHON: "Python313" - image: Visual Studio 2017 @@ -59,13 +59,13 @@ matrix: - image: Visual Studio 2019 WINPYTHON: "Python311" - image: Visual Studio 2019 - WINPYTHON: "Python37" + WINPYTHON: "Python38" # test python 3.11, 3.13 on Visual Studio 2022 image - image: Visual Studio 2022 WINPYTHON: "Python39" - image: Visual Studio 2022 - WINPYTHON: "Python37" + WINPYTHON: "Python38" # Remove some binaries we don't want to be found # Note this is no longer needed, git-windows bin/ is quite minimal now. diff --git a/.github/workflows/runtest.yml b/.github/workflows/runtest.yml index 1b36854303..8dff56d170 100644 --- a/.github/workflows/runtest.yml +++ b/.github/workflows/runtest.yml @@ -21,7 +21,7 @@ jobs: fail-fast: false matrix: cfg: [ - {os: 'ubuntu-22.04', py: '3.7'}, + {os: 'ubuntu-22.04', py: '3.9'}, {os: 'ubuntu-24.04', py: '3.13'}, ] diff --git a/AGENTS.md b/AGENTS.md index 24e626ec1e..25c52cf71a 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -13,7 +13,7 @@ SCons is an open-source software construction tool (build tool) implemented in P ## Building and Running ### Prerequisites -* Python 3.7 or higher. +* Python 3.9 or higher. * Development dependencies: `python -m pip install -r requirements-dev.txt` ### Running SCons (Development) diff --git a/ReleaseConfig b/ReleaseConfig index bf799da7bb..a93f1c6303 100755 --- a/ReleaseConfig +++ b/ReleaseConfig @@ -37,7 +37,7 @@ version_tuple = (4, 10, 2, 'a', 0) # when that version is used. Python versions prior to deprecate_python_version # cause a warning to be issued (assuming it's not disabled). These values are # mandatory and must be present in the configuration file. -unsupported_python_version = (3, 7, 0) +unsupported_python_version = (3, 9, 0) deprecated_python_version = (3, 9, 0) # If release_date is (yyyy, mm, dd, hh, mm, ss), that is used as the release diff --git a/SCons/ActionTests.py b/SCons/ActionTests.py index c07d5cd97a..9bd0aa933f 100644 --- a/SCons/ActionTests.py +++ b/SCons/ActionTests.py @@ -1549,8 +1549,6 @@ def LocalFunc() -> None: # Since the python bytecode has per version differences, we need different expected results per version func_matches = { - (3, 7): bytearray(b'0, 0, 0, 0,(),(),(d\x00S\x00),(),()'), - (3, 8): bytearray(b'0, 0, 0, 0,(),(),(d\x00S\x00),(),()'), (3, 9): bytearray(b'0, 0, 0, 0,(),(),(d\x00S\x00),(),()'), (3, 10): bytearray(b'0, 0, 0, 0,(),(),(d\x00S\x00),(),()'), (3, 11): bytearray(b'0, 0, 0, 0,(),(),(\x97\x00d\x00S\x00),(),()'), @@ -1730,8 +1728,6 @@ def LocalFunc() -> None: pass func_matches = { - (3, 7): bytearray(b'0, 0, 0, 0,(),(),(d\x00S\x00),(),()'), - (3, 8): bytearray(b'0, 0, 0, 0,(),(),(d\x00S\x00),(),()'), (3, 9): bytearray(b'0, 0, 0, 0,(),(),(d\x00S\x00),(),()'), (3, 10): bytearray(b'0, 0, 0, 0,(),(),(d\x00S\x00),(),()'), (3, 11): bytearray(b'0, 0, 0, 0,(),(),(\x97\x00d\x00S\x00),(),()'), @@ -1742,8 +1738,6 @@ def LocalFunc() -> None: } meth_matches = { - (3, 7): bytearray(b'1, 1, 0, 0,(),(),(d\x00S\x00),(),()'), - (3, 8): bytearray(b'1, 1, 0, 0,(),(),(d\x00S\x00),(),()'), (3, 9): bytearray(b'1, 1, 0, 0,(),(),(d\x00S\x00),(),()'), (3, 10): bytearray(b'1, 1, 0, 0,(),(),(d\x00S\x00),(),()'), (3, 11): bytearray(b'1, 1, 0, 0,(),(),(\x97\x00d\x00S\x00),(),()'), @@ -1983,8 +1977,6 @@ def LocalFunc() -> None: pass func_matches = { - (3, 7): bytearray(b'0, 0, 0, 0,(),(),(d\x00S\x00),(),()'), - (3, 8): bytearray(b'0, 0, 0, 0,(),(),(d\x00S\x00),(),()'), (3, 9): bytearray(b'0, 0, 0, 0,(),(),(d\x00S\x00),(),()'), (3, 10): bytearray(b'0, 0, 0, 0,(),(),(d\x00S\x00),(),()'), (3, 11): bytearray(b'0, 0, 0, 0,(),(),(\x97\x00d\x00S\x00),(),()'), @@ -2046,8 +2038,6 @@ def LocalFunc() -> None: pass matches = { - (3, 7): b'd\x00S\x00', - (3, 8): b'd\x00S\x00', (3, 9): b'd\x00S\x00', (3, 10): b'd\x00S\x00', (3, 11): b'\x97\x00d\x00S\x00', @@ -2249,8 +2239,6 @@ def func1(a, b, c): # we need different expected results per version # Note unlike the others, this result is a tuple, use assertIn expected = { - (3, 7): (bytearray(b"3, 3, 0, 0,(),(),(|\x00S\x00),(),()"),), - (3, 8): (bytearray(b"3, 3, 0, 0,(),(),(|\x00S\x00),(),()"),), (3, 9): (bytearray(b"3, 3, 0, 0,(),(),(|\x00S\x00),(),()"),), (3, 10): ( # 3.10.1, 3.10.2 bytearray(b"3, 3, 0, 0,(N.),(),(|\x00S\x00),(),()"), @@ -2277,12 +2265,6 @@ def test_object_contents(self) -> None: # Since the python bytecode has per version differences, # we need different expected results per version expected = { - (3, 7): bytearray( - b"{TestClass:__main__}[[[(, ()), [(, (,))]]]]{{1, 1, 0, 0,(a,b),(a,b),(d\x01|\x00_\x00d\x02|\x00_\x01d\x00S\x00),(),(),2, 2, 0, 0,(),(),(d\x00S\x00),(),()}}{{{a=a,b=b}}}" - ), - (3, 8): bytearray( - b"{TestClass:__main__}[[[(, ()), [(, (,))]]]]{{1, 1, 0, 0,(a,b),(a,b),(d\x01|\x00_\x00d\x02|\x00_\x01d\x00S\x00),(),(),2, 2, 0, 0,(),(),(d\x00S\x00),(),()}}{{{a=a,b=b}}}" - ), (3, 9): bytearray( b"{TestClass:__main__}[[[(, ()), [(, (,))]]]]{{1, 1, 0, 0,(a,b),(a,b),(d\x01|\x00_\x00d\x02|\x00_\x01d\x00S\x00),(),(),2, 2, 0, 0,(),(),(d\x00S\x00),(),()}}{{{a=a,b=b}}}" ), @@ -2312,12 +2294,6 @@ def test_code_contents(self) -> None: # Since the python bytecode has per version differences, we need different expected results per version expected = { - (3, 7): bytearray( - b"0, 0, 0, 0,(Hello, World!),(print),(e\x00d\x00\x83\x01\x01\x00d\x01S\x00)" - ), - (3, 8): bytearray( - b"0, 0, 0, 0,(Hello, World!),(print),(e\x00d\x00\x83\x01\x01\x00d\x01S\x00)" - ), (3, 9): bytearray( b"0, 0, 0, 0,(Hello, World!),(print),(e\x00d\x00\x83\x01\x01\x00d\x01S\x00)" ), diff --git a/SCons/CacheDir.py b/SCons/CacheDir.py index b91ddc633e..7c3cc223b9 100644 --- a/SCons/CacheDir.py +++ b/SCons/CacheDir.py @@ -210,44 +210,22 @@ def _mkdir_atomic(self, path: str) -> bool: return False try: - # TODO: Python 3.7. See comment below. - # tempdir = tempfile.TemporaryDirectory(dir=os.path.dirname(directory)) - tempdir = tempfile.mkdtemp(dir=os.path.dirname(directory)) + tempdir = tempfile.TemporaryDirectory(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: - 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 + 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 def _readconfig(self, path: str) -> None: """Read the cache config from *path*. diff --git a/SCons/Script/Main.py b/SCons/Script/Main.py index 5ee0c8b499..3ea51bdacd 100644 --- a/SCons/Script/Main.py +++ b/SCons/Script/Main.py @@ -67,7 +67,7 @@ from SCons import __version__ as SConsVersion # these define the range of versions SCons supports -minimum_python_version = (3, 7, 0) +minimum_python_version = (3, 9, 0) deprecated_python_version = (3, 9, 0) # ordered list of SConstruct names to look for if there is no -f flag diff --git a/SCons/Tool/install.py b/SCons/Tool/install.py index cd1f471ade..0b229401ff 100644 --- a/SCons/Tool/install.py +++ b/SCons/Tool/install.py @@ -31,7 +31,7 @@ import os import stat -from shutil import copy2, copystat +from shutil import copy2, copytree import SCons.Action import SCons.Tool @@ -50,109 +50,6 @@ class CopytreeError(OSError): pass - -def scons_copytree(src, dst, symlinks: bool=False, ignore=None, copy_function=copy2, - ignore_dangling_symlinks: bool=False, dirs_exist_ok: bool=False): - """Recursively copy a directory tree, SCons version. - - This is a modified copy of the Python 3.7 shutil.copytree function. - SCons update: dirs_exist_ok dictates whether to raise an - exception in case dst or any missing parent directory already - exists. Implementation depends on os.makedirs having a similar - flag, which it has since Python 3.2. This version also raises an - SCons-defined exception rather than the one defined locally to shtuil. - This version uses a change from Python 3.8. - TODO: we can remove this forked copy once the minimum Py version is 3.8. - - If exception(s) occur, an Error is raised with a list of reasons. - - If the optional symlinks flag is true, symbolic links in the - source tree result in symbolic links in the destination tree; if - it is false, the contents of the files pointed to by symbolic - links are copied. If the file pointed by the symlink doesn't - exist, an exception will be added in the list of errors raised in - an Error exception at the end of the copy process. - - You can set the optional ignore_dangling_symlinks flag to true if you - want to silence this exception. Notice that this has no effect on - platforms that don't support os.symlink. - - The optional ignore argument is a callable. If given, it - is called with the `src` parameter, which is the directory - being visited by copytree(), and `names` which is the list of - `src` contents, as returned by os.listdir(): - - callable(src, names) -> ignored_names - - Since copytree() is called recursively, the callable will be - called once for each directory that is copied. It returns a - list of names relative to the `src` directory that should - not be copied. - - The optional copy_function argument is a callable that will be used - to copy each file. It will be called with the source path and the - destination path as arguments. By default, copy2() is used, but any - function that supports the same signature (like copy()) can be used. - - """ - names = os.listdir(src) - if ignore is not None: - ignored_names = ignore(src, names) - else: - ignored_names = set() - - os.makedirs(dst, exist_ok=dirs_exist_ok) - errors = [] - for name in names: - if name in ignored_names: - continue - srcname = os.path.join(src, name) - dstname = os.path.join(dst, name) - try: - if os.path.islink(srcname): - linkto = os.readlink(srcname) - if symlinks: - # We can't just leave it to `copy_function` because legacy - # code with a custom `copy_function` may rely on copytree - # doing the right thing. - os.symlink(linkto, dstname) - copystat(srcname, dstname, follow_symlinks=not symlinks) - else: - # ignore dangling symlink if the flag is on - if not os.path.exists(linkto) and ignore_dangling_symlinks: - continue - # otherwise let the copy occurs. copy2 will raise an error - if os.path.isdir(srcname): - scons_copytree(srcname, dstname, symlinks=symlinks, - ignore=ignore, copy_function=copy_function, - ignore_dangling_symlinks=ignore_dangling_symlinks, - dirs_exist_ok=dirs_exist_ok) - else: - copy_function(srcname, dstname) - elif os.path.isdir(srcname): - scons_copytree(srcname, dstname, symlinks=symlinks, - ignore=ignore, copy_function=copy_function, - ignore_dangling_symlinks=ignore_dangling_symlinks, - dirs_exist_ok=dirs_exist_ok) - else: - # Will raise a SpecialFileError for unsupported file types - copy_function(srcname, dstname) - # catch the Error from the recursive copytree so that we can - # continue with other files - except CopytreeError as err: # SCons change - errors.extend(err.args[0]) - except OSError as why: - errors.append((srcname, dstname, str(why))) - try: - copystat(src, dst) - except OSError as why: - # Copying file access times may fail on Windows - if getattr(why, 'winerror', None) is None: - errors.append((src, dst, str(why))) - if errors: - raise CopytreeError(errors) # SCons change - return dst - # # Functions doing the actual work of the Install Builder. # @@ -173,7 +70,7 @@ def copyFunc(dest, source, env) -> int: parent = os.path.split(dest)[0] if not os.path.exists(parent): os.makedirs(parent) - scons_copytree(source, dest, dirs_exist_ok=True) + copytree(source, dest, dirs_exist_ok=True) else: copy2(source, dest) st = os.stat(source) diff --git a/SCons/Util/hashes.py b/SCons/Util/hashes.py index 4950f6805e..10d134a208 100644 --- a/SCons/Util/hashes.py +++ b/SCons/Util/hashes.py @@ -330,14 +330,8 @@ def hash_file_signature(fname: str, chunksize: int=65536, hash_format=None) -> s """ m = _get_hash_object(hash_format) with open(fname, "rb") as f: - while True: - blck = f.read(chunksize) - if not blck: - break - m.update(to_bytes(blck)) - # TODO: can use this when base is Python 3.8+ - # while (blk := f.read(chunksize)) != b'': - # m.update(to_bytes(blk)) + while (blk := f.read(chunksize)): + m.update(to_bytes(blk)) return m.hexdigest() diff --git a/pyproject.toml b/pyproject.toml index 56d8f1d65c..3535d7c6da 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -5,7 +5,7 @@ requires = ["setuptools"] [project] name = "SCons" description = "Open Source next-generation build tool." -requires-python = ">=3.7" +requires-python = ">=3.9" license = "MIT" # PEP 639 form (new - setuptools >= 77.0) # Should include docbook license, but this fails: # license = "MIT AND DocBook-stylesheet" @@ -22,8 +22,6 @@ classifiers = [ "Programming Language :: Python", "Programming Language :: Python :: 3", "Programming Language :: Python :: 3 :: Only", - "Programming Language :: Python :: 3.7", - "Programming Language :: Python :: 3.8", "Programming Language :: Python :: 3.9", "Programming Language :: Python :: 3.10", "Programming Language :: Python :: 3.11", @@ -97,7 +95,7 @@ dist-dir = "build/dist" dist-dir = "build/dist" [tool.ruff] -target-version = "py37" # Lowest python version supported +target-version = "py38" # Lowest python version supported extend-include = ["SConstruct", "SConscript"] extend-exclude = [ "bench/", diff --git a/scripts/scons-configure-cache.py b/scripts/scons-configure-cache.py index c30859db14..22f97213ad 100644 --- a/scripts/scons-configure-cache.py +++ b/scripts/scons-configure-cache.py @@ -48,10 +48,10 @@ import os import sys -# python compatibility check -if sys.version_info < (3, 7, 0): +# Python compatibility check +if sys.version_info < (3, 9, 0): msg = "scons: *** SCons version %s does not run under Python version %s.\n\ -Python >= 3.7.0 is required.\n" +Python >= 3.9.0 is required.\n" sys.stderr.write(msg % (__version__, sys.version.split()[0])) sys.exit(1) diff --git a/scripts/scons.py b/scripts/scons.py index a6a113f71f..e10c7dfe48 100755 --- a/scripts/scons.py +++ b/scripts/scons.py @@ -42,9 +42,9 @@ import sys # Python compatibility check -if sys.version_info < (3, 7, 0): +if sys.version_info < (3, 9, 0): msg = "scons: *** SCons version %s does not run under Python version %s.\n\ -Python >= 3.7.0 is required.\n" +Python >= 3.9.0 is required.\n" sys.stderr.write(msg % (__version__, sys.version.split()[0])) sys.exit(1) diff --git a/scripts/sconsign.py b/scripts/sconsign.py index 1120aa33ea..0be93bf271 100644 --- a/scripts/sconsign.py +++ b/scripts/sconsign.py @@ -40,10 +40,10 @@ import os import sys -# python compatibility check -if sys.version_info < (3, 7, 0): +# Python compatibility check +if sys.version_info < (3, 9, 0): msg = "scons: *** SCons version %s does not run under Python version %s.\n\ -Python >= 3.7.0 is required.\n" +Python >= 3.9.0 is required.\n" sys.stderr.write(msg % (__version__, sys.version.split()[0])) sys.exit(1) @@ -84,6 +84,7 @@ # END STANDARD SCons SCRIPT HEADER ############################################################################## from SCons.Utilities.sconsign import main + if __name__ == "__main__": main() sys.exit(0) diff --git a/test/packaging/rpm/explicit-target.py b/test/packaging/rpm/explicit-target.py index 16554cf74d..f451c80f15 100644 --- a/test/packaging/rpm/explicit-target.py +++ b/test/packaging/rpm/explicit-target.py @@ -75,11 +75,7 @@ ) """ % locals()) - -if sys.version_info.minor >= 8: - line_number = 12 -else: - line_number = 23 +line_number = 12 expect = """ scons: *** Setting target is not supported for rpm. diff --git a/testing/framework/TestSCons.py b/testing/framework/TestSCons.py index a3ada082fe..f509dc62c3 100644 --- a/testing/framework/TestSCons.py +++ b/testing/framework/TestSCons.py @@ -57,7 +57,7 @@ default_version = '4.10.2ayyyymmdd' # TODO: these need to be hand-edited when there are changes -python_version_unsupported = (3, 7, 0) +python_version_unsupported = (3, 9, 0) python_version_deprecated = (3, 9, 0) python_version_supported_str = "3.9.0" # str of lowest non-deprecated Python