Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 4 additions & 4 deletions .appveyor.yml
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand All @@ -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
Expand All @@ -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.
Expand Down
2 changes: 1 addition & 1 deletion .github/workflows/runtest.yml
Original file line number Diff line number Diff line change
Expand Up @@ -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'},
]

Expand Down
2 changes: 1 addition & 1 deletion AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down
2 changes: 1 addition & 1 deletion ReleaseConfig
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
24 changes: 0 additions & 24 deletions SCons/ActionTests.py
Original file line number Diff line number Diff line change
Expand Up @@ -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),(),()'),
Expand Down Expand Up @@ -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),(),()'),
Expand All @@ -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),(),()'),
Expand Down Expand Up @@ -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),(),()'),
Expand Down Expand Up @@ -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',
Expand Down Expand Up @@ -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),(),()"),
Expand All @@ -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__}[[[(<class 'object'>, ()), [(<class '__main__.TestClass'>, (<class 'object'>,))]]]]{{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__}[[[(<class 'object'>, ()), [(<class '__main__.TestClass'>, (<class 'object'>,))]]]]{{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__}[[[(<class 'object'>, ()), [(<class '__main__.TestClass'>, (<class 'object'>,))]]]]{{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}}}"
),
Expand Down Expand Up @@ -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)"
),
Expand Down
46 changes: 12 additions & 34 deletions SCons/CacheDir.py
Original file line number Diff line number Diff line change
Expand Up @@ -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*.
Expand Down
2 changes: 1 addition & 1 deletion SCons/Script/Main.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
107 changes: 2 additions & 105 deletions SCons/Tool/install.py
Original file line number Diff line number Diff line change
Expand Up @@ -31,7 +31,7 @@

import os
import stat
from shutil import copy2, copystat
from shutil import copy2, copytree

import SCons.Action
import SCons.Tool
Expand All @@ -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.
#
Expand All @@ -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)
Expand Down
10 changes: 2 additions & 8 deletions SCons/Util/hashes.py
Original file line number Diff line number Diff line change
Expand Up @@ -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()

Expand Down
6 changes: 2 additions & 4 deletions pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand All @@ -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",
Expand Down Expand Up @@ -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/",
Expand Down
6 changes: 3 additions & 3 deletions scripts/scons-configure-cache.py
Original file line number Diff line number Diff line change
Expand Up @@ -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)

Expand Down
Loading
Loading