summaryrefslogtreecommitdiffstats
path: root/SCons
diff options
context:
space:
mode:
Diffstat (limited to 'SCons')
-rw-r--r--SCons/Action.py2
-rw-r--r--SCons/CacheDirTests.py8
-rw-r--r--SCons/Defaults.py5
-rw-r--r--SCons/Environment.py4
-rw-r--r--SCons/Environment.xml5
-rw-r--r--SCons/Node/Alias.py4
-rw-r--r--SCons/Node/FS.py22
-rw-r--r--SCons/Node/FSTests.py10
-rw-r--r--SCons/Node/__init__.py8
-rw-r--r--SCons/SConf.py4
-rw-r--r--SCons/Script/Main.py5
-rw-r--r--SCons/Script/SConsOptions.py10
-rw-r--r--SCons/Util.py163
-rw-r--r--SCons/UtilTests.py43
14 files changed, 199 insertions, 94 deletions
diff --git a/SCons/Action.py b/SCons/Action.py
index d7f10df..fdb0463 100644
--- a/SCons/Action.py
+++ b/SCons/Action.py
@@ -54,7 +54,7 @@ other modules:
get_contents()
Fetches the "contents" of an Action for signature calculation
- plus the varlist. This is what gets MD5 checksummed to decide
+ plus the varlist. This is what gets checksummed to decide
if a target needs to be rebuilt because its action changed.
genstring()
diff --git a/SCons/CacheDirTests.py b/SCons/CacheDirTests.py
index f0278ac..c4a0ed7 100644
--- a/SCons/CacheDirTests.py
+++ b/SCons/CacheDirTests.py
@@ -96,10 +96,10 @@ class CacheDirTestCase(BaseTestCase):
# Verify how the cachepath() method determines the name
# of the file in cache.
- def my_collect(list):
+ def my_collect(list, hash_format=None):
return list[0]
- save_collect = SCons.Util.MD5collect
- SCons.Util.MD5collect = my_collect
+ save_collect = SCons.Util.hash_collect
+ SCons.Util.hash_collect = my_collect
try:
name = 'a_fake_bsig'
@@ -110,7 +110,7 @@ class CacheDirTestCase(BaseTestCase):
filename = os.path.join(dirname, name)
assert result == (dirname, filename), result
finally:
- SCons.Util.MD5collect = save_collect
+ SCons.Util.hash_collect = save_collect
class ExceptionTestCase(unittest.TestCase):
"""Test that the correct exceptions are thrown by CacheDir."""
diff --git a/SCons/Defaults.py b/SCons/Defaults.py
index 06d938c..6a87787 100644
--- a/SCons/Defaults.py
+++ b/SCons/Defaults.py
@@ -78,10 +78,7 @@ def DefaultEnvironment(*args, **kw):
if not _default_env:
import SCons.Util
_default_env = SCons.Environment.Environment(*args, **kw)
- if SCons.Util.md5:
- _default_env.Decider('MD5')
- else:
- _default_env.Decider('timestamp-match')
+ _default_env.Decider('content')
global DefaultEnvironment
DefaultEnvironment = _fetch_DefaultEnvironment
_default_env._CacheDir_path = None
diff --git a/SCons/Environment.py b/SCons/Environment.py
index cc62679..0bf5079 100644
--- a/SCons/Environment.py
+++ b/SCons/Environment.py
@@ -1483,10 +1483,8 @@ class Base(SubstitutionEnvironment):
def Decider(self, function):
copy_function = self._copy2_from_cache
if function in ('MD5', 'content'):
- if not SCons.Util.md5:
- raise UserError("MD5 signatures are not available in this version of Python.")
function = self._changed_content
- elif function == 'MD5-timestamp':
+ elif function in ('MD5-timestamp', 'content-timestamp'):
function = self._changed_timestamp_then_content
elif function in ('timestamp-newer', 'make'):
function = self._changed_timestamp_newer
diff --git a/SCons/Environment.xml b/SCons/Environment.xml
index a1fd8ec..7d3927b 100644
--- a/SCons/Environment.xml
+++ b/SCons/Environment.xml
@@ -1103,6 +1103,9 @@ that runs a build,
updates a file,
and runs the build again,
all within a single second.
+<literal>content-timestamp</literal>
+can be used as a synonym for
+<literal>MD5-timestamp</literal>.
</para>
</listitem>
</varlistentry>
@@ -1117,7 +1120,7 @@ Examples:
# Use exact timestamp matches by default.
Decider('timestamp-match')
-# Use MD5 content signatures for any targets built
+# Use hash content signatures for any targets built
# with the attached construction environment.
env.Decider('content')
</example_commands>
diff --git a/SCons/Node/Alias.py b/SCons/Node/Alias.py
index 64054d2..b5e4eb4 100644
--- a/SCons/Node/Alias.py
+++ b/SCons/Node/Alias.py
@@ -31,7 +31,7 @@ import collections
import SCons.Errors
import SCons.Node
import SCons.Util
-from SCons.Util import MD5signature
+from SCons.Util import hash_signature
class AliasNameSpace(collections.UserDict):
def Alias(self, name, **kw):
@@ -161,7 +161,7 @@ class Alias(SCons.Node.Node):
pass
contents = self.get_contents()
- csig = MD5signature(contents)
+ csig = hash_signature(contents)
self.get_ninfo().csig = csig
return csig
diff --git a/SCons/Node/FS.py b/SCons/Node/FS.py
index 967f007..1e5d5b8 100644
--- a/SCons/Node/FS.py
+++ b/SCons/Node/FS.py
@@ -50,7 +50,7 @@ import SCons.Node
import SCons.Node.Alias
import SCons.Subst
import SCons.Util
-from SCons.Util import MD5signature, MD5filesignature, MD5collect
+from SCons.Util import hash_signature, hash_file_signature, hash_collect
import SCons.Warnings
print_duplicate = 0
@@ -1865,7 +1865,7 @@ class Dir(Base):
node is called which has a child directory, the child
directory should return the hash of its contents."""
contents = self.get_contents()
- return MD5signature(contents)
+ return hash_signature(contents)
def do_duplicate(self, src):
pass
@@ -2630,7 +2630,7 @@ class File(Base):
BuildInfo = FileBuildInfo
# Although the command-line argument is in kilobytes, this is in bytes.
- md5_chunksize = 65536
+ hash_chunksize = 65536
def diskcheck_match(self):
diskcheck_match(self, self.isdir,
@@ -2726,13 +2726,13 @@ class File(Base):
def get_content_hash(self):
"""
- Compute and return the MD5 hash for this file.
+ Compute and return the hash for this file.
"""
if not self.rexists():
- return MD5signature('')
+ return hash_signature('')
fname = self.rfile().get_abspath()
try:
- cs = MD5filesignature(fname, chunksize=File.md5_chunksize)
+ cs = hash_file_signature(fname, chunksize=File.hash_chunksize)
except EnvironmentError as e:
if not e.filename:
e.filename = fname
@@ -3218,7 +3218,7 @@ class File(Base):
if csig is None:
try:
- if self.get_size() < File.md5_chunksize:
+ if self.get_size() < File.hash_chunksize:
contents = self.get_contents()
else:
csig = self.get_content_hash()
@@ -3230,7 +3230,7 @@ class File(Base):
csig = ''
else:
if not csig:
- csig = SCons.Util.MD5signature(contents)
+ csig = SCons.Util.hash_signature(contents)
ninfo.csig = csig
@@ -3619,7 +3619,7 @@ class File(Base):
cachedir, cachefile = self.get_build_env().get_CacheDir().cachepath(self)
if not self.exists() and cachefile and os.path.exists(cachefile):
- self.cachedir_csig = MD5filesignature(cachefile, File.md5_chunksize)
+ self.cachedir_csig = hash_file_signature(cachefile, File.hash_chunksize)
else:
self.cachedir_csig = self.get_csig()
return self.cachedir_csig
@@ -3639,7 +3639,7 @@ class File(Base):
executor = self.get_executor()
- result = self.contentsig = MD5signature(executor.get_contents())
+ result = self.contentsig = hash_signature(executor.get_contents())
return result
def get_cachedir_bsig(self):
@@ -3670,7 +3670,7 @@ class File(Base):
sigs.append(self.get_internal_path())
# Merge this all into a single signature
- result = self.cachesig = MD5collect(sigs)
+ result = self.cachesig = hash_collect(sigs)
return result
default_fs = None
diff --git a/SCons/Node/FSTests.py b/SCons/Node/FSTests.py
index 65cc363..457d199 100644
--- a/SCons/Node/FSTests.py
+++ b/SCons/Node/FSTests.py
@@ -2055,15 +2055,15 @@ class DirTestCase(_tempdirTestCase):
assert g.get_csig() + " g" == files[2], files
assert s.get_csig() + " sub" == files[3], files
- def test_md5_chunksize(self):
+ def test_hash_chunksize(self):
"""
- Test verifying that File.get_csig() correctly uses md5_chunksize. This
- variable is documented as the md5 chunksize in kilobytes. This test
- verifies that if the file size is less than the md5 chunksize,
+ Test verifying that File.get_csig() correctly uses hash_chunksize. This
+ variable is documented as the hash chunksize in kilobytes. This test
+ verifies that if the file size is less than the hash chunksize,
get_contents() is called; otherwise, it verifies that get_contents()
is not called.
"""
- chunksize_bytes = SCons.Node.FS.File.md5_chunksize
+ chunksize_bytes = SCons.Node.FS.File.hash_chunksize
test = self.test
test.subdir('chunksize_dir')
diff --git a/SCons/Node/__init__.py b/SCons/Node/__init__.py
index 8a1677e..6a3770e 100644
--- a/SCons/Node/__init__.py
+++ b/SCons/Node/__init__.py
@@ -54,7 +54,7 @@ from SCons.Debug import logInstanceCreation
import SCons.Executor
import SCons.Memoize
import SCons.Util
-from SCons.Util import MD5signature
+from SCons.Util import hash_signature
from SCons.Debug import Trace
@@ -1173,7 +1173,7 @@ class Node(object, metaclass=NoSlotsPyPy):
if self.has_builder():
binfo.bact = str(executor)
- binfo.bactsig = MD5signature(executor.get_contents())
+ binfo.bactsig = hash_signature(executor.get_contents())
if self._specific_sources:
sources = [s for s in self.sources if s not in ignore_set]
@@ -1211,7 +1211,7 @@ class Node(object, metaclass=NoSlotsPyPy):
return self.ninfo.csig
except AttributeError:
ninfo = self.get_ninfo()
- ninfo.csig = MD5signature(self.get_contents())
+ ninfo.csig = hash_signature(self.get_contents())
return self.ninfo.csig
def get_cachedir_csig(self):
@@ -1502,7 +1502,7 @@ class Node(object, metaclass=NoSlotsPyPy):
if self.has_builder():
contents = self.get_executor().get_contents()
- newsig = MD5signature(contents)
+ newsig = hash_signature(contents)
if bi.bactsig != newsig:
if t: Trace(': bactsig %s != newsig %s' % (bi.bactsig, newsig))
result = True
diff --git a/SCons/SConf.py b/SCons/SConf.py
index 70a98c8..d024600 100644
--- a/SCons/SConf.py
+++ b/SCons/SConf.py
@@ -598,7 +598,7 @@ class SConfBase:
f = "conftest"
if text is not None:
- textSig = SCons.Util.MD5signature(sourcetext)
+ textSig = SCons.Util.hash_signature(sourcetext)
textSigCounter = str(_ac_build_counter[textSig])
_ac_build_counter[textSig] += 1
@@ -617,7 +617,7 @@ class SConfBase:
target = None
action = builder.builder.action.get_contents(target=target, source=[source], env=self.env)
- actionsig = SCons.Util.MD5signature(action)
+ actionsig = SCons.Util.hash_signature(action)
f = "_".join([f, actionsig])
pref = self.env.subst( builder.builder.prefix )
diff --git a/SCons/Script/Main.py b/SCons/Script/Main.py
index 1f2a455..ea9bd95 100644
--- a/SCons/Script/Main.py
+++ b/SCons/Script/Main.py
@@ -1098,8 +1098,11 @@ def _main(parser):
SCons.Job.explicit_stack_size = options.stack_size
+ # Hash format and chunksize are set late to support SetOption being called
+ # in a SConscript or SConstruct file.
+ SCons.Util.set_hash_format(options.hash_format)
if options.md5_chunksize:
- SCons.Node.FS.File.md5_chunksize = options.md5_chunksize * 1024
+ SCons.Node.FS.File.hash_chunksize = options.md5_chunksize * 1024
platform = SCons.Platform.platform_module()
diff --git a/SCons/Script/SConsOptions.py b/SCons/Script/SConsOptions.py
index d25d657..cf132a2 100644
--- a/SCons/Script/SConsOptions.py
+++ b/SCons/Script/SConsOptions.py
@@ -130,6 +130,7 @@ class SConsValues(optparse.Values):
'clean',
'diskcheck',
'duplicate',
+ 'hash_format',
'help',
'implicit_cache',
'max_drift',
@@ -713,6 +714,11 @@ def Parser(version):
action="help",
help="Print this message and exit.")
+ op.add_option('--hash-format',
+ dest='hash_format',
+ action='store',
+ help='Hash format (e.g. md5, sha1, or sha256).')
+
op.add_option('-i', '--ignore-errors',
dest='ignore_errors', default=False,
action="store_true",
@@ -775,9 +781,9 @@ def Parser(version):
op.add_option('--md5-chunksize',
nargs=1, type="int",
- dest='md5_chunksize', default=SCons.Node.FS.File.md5_chunksize,
+ dest='md5_chunksize', default=SCons.Node.FS.File.hash_chunksize,
action="store",
- help="Set chunk-size for MD5 signature computation to N kilobytes.",
+ help="Set chunk-size for hash signature computation to N kilobytes.",
metavar="N")
op.add_option('-n', '--no-exec', '--just-print', '--dry-run', '--recon',
diff --git a/SCons/Util.py b/SCons/Util.py
index 88aeaae..053c5de 100644
--- a/SCons/Util.py
+++ b/SCons/Util.py
@@ -1468,66 +1468,143 @@ def AddMethod(obj, function, name=None):
setattr(obj, name, method)
-if hasattr(hashlib, 'md5'):
- md5 = True
+# Default hash function. SCons-internal.
+_hash_function = None
- def MD5signature(s):
- """
- Generate md5 signature of a string
- :param s: either string or bytes. Normally should be bytes
- :return: String of hex digits representing the signature
- """
- m = hashlib.md5()
+def set_hash_format(hash_format):
+ """
+ Sets the default hash format used by SCons. If hash_format is None or
+ an empty string, the default is determined by this function.
- try:
- m.update(to_bytes(s))
- except TypeError as e:
- m.update(to_bytes(str(s)))
+ Currently the default behavior is to use the first available format of
+ the following options: MD5, SHA1, SHA256.
+ """
+ global _hash_function
+
+ if hash_format:
+ hash_format_lower = hash_format.lower()
+ _hash_function = getattr(hashlib, hash_format_lower, None)
+ if _hash_function is None:
+ raise Exception(
+ 'Hash format "%s" is not available in your Python '
+ 'interpreter.' % hash_format_lower)
+ else:
+ # Set the default hash format based on what is available, defaulting
+ # to md5 for backwards compatibility.
+ choices = ['md5', 'sha1', 'sha256']
+ for choice in choices:
+ try:
+ _hash_function = getattr(hashlib, choice)
+ _hash_function()
+ break
+ except Exception:
+ pass
+ else:
+ # This is not expected to happen in practice.
+ raise Exception(
+ 'Your Python interpreter does not have MD5, SHA1, or SHA256. '
+ 'SCons requires at least one.')
+
+# Ensure that this is initialized in case either:
+# 1. This code is running in a unit test.
+# 2. This code is running in a consumer that does hash operations while
+# SConscript files are being loaded.
+# TODO: Should this go somewhere else? Is this unnecessary? Case #1 could be
+# handled in the TestCmd module, but I was worried about breaking people
+# who mischievously calls get_csig() during startup.
+set_hash_format('md5')
+
+
+def _get_hash_object(hash_format):
+ """
+ Allocates a hash object using the requested hash format.
- return m.hexdigest()
+ :param hash_format: Hash format to use.
+ :return: hashlib object.
+ """
+ if hash_format is None:
+ if _hash_function is None:
+ raise Exception('There is no default hash function. Did you call '
+ 'a hashing function before SCons was initialized?')
+ return _hash_function()
+ elif not hasattr(hashlib, hash_format):
+ raise Exception(
+ 'Hash format "%s" is not available in your Python interpreter.' %
+ hash_format)
+ else:
+ return getattr(hashlib, hash_format)()
- def MD5filesignature(fname, chunksize=65536):
- """
- Generate the md5 signature of a file
- :param fname: file to hash
- :param chunksize: chunk size to read
- :return: String of Hex digits representing the signature
- """
- m = hashlib.md5()
- with open(fname, "rb") as f:
- while True:
- blck = f.read(chunksize)
- if not blck:
- break
- m.update(to_bytes(blck))
- return m.hexdigest()
-else:
- # if md5 algorithm not available, just return data unmodified
- # could add alternative signature scheme here
- md5 = False
+def hash_signature(s, hash_format=None):
+ """
+ Generate hash signature of a string
- def MD5signature(s):
- return str(s)
+ :param s: either string or bytes. Normally should be bytes
+ :param hash_format: Specify to override default hash format
+ :return: String of hex digits representing the signature
+ """
+ m = _get_hash_object(hash_format)
+ try:
+ m.update(to_bytes(s))
+ except TypeError:
+ m.update(to_bytes(str(s)))
- def MD5filesignature(fname, chunksize=65536):
- with open(fname, "rb") as f:
- result = f.read()
- return result
+ return m.hexdigest()
-def MD5collect(signatures):
+def hash_file_signature(fname, chunksize=65536, hash_format=None):
+ """
+ Generate the md5 signature of a file
+
+ :param fname: file to hash
+ :param chunksize: chunk size to read
+ :param hash_format: Specify to override default hash format
+ :return: String of Hex digits representing the signature
+ """
+ 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))
+ return m.hexdigest()
+
+
+def hash_collect(signatures, hash_format=None):
"""
Collects a list of signatures into an aggregate signature.
- signatures - a list of signatures
- returns - the aggregate signature
+ :param signatures: a list of signatures
+ :param hash_format: Specify to override default hash format
+ :return: - the aggregate signature
"""
if len(signatures) == 1:
return signatures[0]
else:
- return MD5signature(', '.join(signatures))
+ return hash_signature(', '.join(signatures), hash_format)
+
+
+def MD5signature(s):
+ """
+ Deprecated. Use hash_signature instead.
+ """
+ return hash_signature(s)
+
+
+def MD5filesignature(fname, chunksize=65536):
+ """
+ Deprecated. Use hash_file_signature instead.
+ """
+ return hash_file_signature(fname, chunksize)
+
+
+def MD5collect(signatures):
+ """
+ Deprecated. Use hash_collect instead.
+ """
+ return hash_collect(signatures)
def silent_intern(x):
diff --git a/SCons/UtilTests.py b/SCons/UtilTests.py
index 72bbaf3..2be2672 100644
--- a/SCons/UtilTests.py
+++ b/SCons/UtilTests.py
@@ -23,6 +23,7 @@
import SCons.compat
+import functools
import io
import os
import sys
@@ -755,24 +756,44 @@ bling
assert id(s1) == id(s4)
-class MD5TestCase(unittest.TestCase):
+class HashTestCase(unittest.TestCase):
def test_collect(self):
"""Test collecting a list of signatures into a new signature value
"""
- s = list(map(MD5signature, ('111', '222', '333')))
-
- assert '698d51a19d8a121ce581499d7b701668' == MD5collect(s[0:1])
- assert '8980c988edc2c78cc43ccb718c06efd5' == MD5collect(s[0:2])
- assert '53fd88c84ff8a285eb6e0a687e55b8c7' == MD5collect(s)
+ for algorithm, expected in {
+ 'md5': ('698d51a19d8a121ce581499d7b701668',
+ '8980c988edc2c78cc43ccb718c06efd5',
+ '53fd88c84ff8a285eb6e0a687e55b8c7'),
+ 'sha1': ('6216f8a75fd5bb3d5f22b6f9958cdede3fc086c2',
+ '42eda1b5dcb3586bccfb1c69f22f923145271d97',
+ '2eb2f7be4e883ebe52034281d818c91e1cf16256'),
+ 'sha256': ('f6e0a1e2ac41945a9aa7ff8a8aaa0cebc12a3bcc981a929ad5cf810a090e11ae',
+ '25235f0fcab8767b7b5ac6568786fbc4f7d5d83468f0626bf07c3dbeed391a7a',
+ 'f8d3d0729bf2427e2e81007588356332e7e8c4133fae4bceb173b93f33411d17'),
+ }.items():
+ hs = functools.partial(hash_signature, hash_format=algorithm)
+ s = list(map(hs, ('111', '222', '333')))
+
+ assert expected[0] == hash_collect(s[0:1], hash_format=algorithm)
+ assert expected[1] == hash_collect(s[0:2], hash_format=algorithm)
+ assert expected[2] == hash_collect(s, hash_format=algorithm)
def test_MD5signature(self):
"""Test generating a signature"""
- s = MD5signature('111')
- assert '698d51a19d8a121ce581499d7b701668' == s, s
-
- s = MD5signature('222')
- assert 'bcbe3365e6ac95ea2c0343a2395834dd' == s, s
+ for algorithm, expected in {
+ 'md5': ('698d51a19d8a121ce581499d7b701668',
+ 'bcbe3365e6ac95ea2c0343a2395834dd'),
+ 'sha1': ('6216f8a75fd5bb3d5f22b6f9958cdede3fc086c2',
+ '1c6637a8f2e1f75e06ff9984894d6bd16a3a36a9'),
+ 'sha256': ('f6e0a1e2ac41945a9aa7ff8a8aaa0cebc12a3bcc981a929ad5cf810a090e11ae',
+ '9b871512327c09ce91dd649b3f96a63b7408ef267c8cc5710114e629730cb61f'),
+ }.items():
+ s = hash_signature('111', hash_format=algorithm)
+ assert expected[0] == s, s
+
+ s = hash_signature('222', hash_format=algorithm)
+ assert expected[1] == s, s
class NodeListTestCase(unittest.TestCase):