summaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
authorAdam Gross <grossag@vmware.com>2020-06-22 19:10:43 (GMT)
committerAdam Gross <grossag@vmware.com>2020-08-04 13:07:42 (GMT)
commit1fa19ef5eeb2b37c54d291a5aa5858b7b91bbb5d (patch)
tree34f1d076367301728471aa896e4a14e8082d5910
parent05e2dc91e95507b3c0f1bd42b93f41c0c8733371 (diff)
downloadSCons-1fa19ef5eeb2b37c54d291a5aa5858b7b91bbb5d.zip
SCons-1fa19ef5eeb2b37c54d291a5aa5858b7b91bbb5d.tar.gz
SCons-1fa19ef5eeb2b37c54d291a5aa5858b7b91bbb5d.tar.bz2
Add support for overriding the default hash format
This change adds support for a new --hash-format parameter that can be used to override the default hash format used by SCons. The default remains MD5, but this allows consumers to opt into SHA1, SHA256, or any other hash algorithm offered by their implementation of hashlib.
-rwxr-xr-xCHANGES.txt4
-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.py20
-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.py8
-rw-r--r--SCons/Util.py163
-rw-r--r--SCons/UtilTests.py43
-rw-r--r--doc/man/scons.xml15
-rw-r--r--test/option/hash-format.py49
-rw-r--r--test/option/hash-format/.exclude_tests1
-rw-r--r--test/option/hash-format/SConstruct27
-rw-r--r--test/option/hash-format/build.py3
-rw-r--r--test/option/hash-format/f1.in1
20 files changed, 293 insertions, 86 deletions
diff --git a/CHANGES.txt b/CHANGES.txt
index 09c3400..0135ea4 100755
--- a/CHANGES.txt
+++ b/CHANGES.txt
@@ -134,6 +134,10 @@ RELEASE 4.0.0 - Sat, 04 Jul 2020 12:00:27 +0000
when the value can't be converted to a string or if having a name is otherwise desirable.
- Fixed usage of abspath and path for RootDir objects on Windows. Previously
env.fs.Dir("T:").abspath would return "T:\T:" and now it correctly returns "T:".
+ - Added support for a new command-line parameter "--hash-format" to override the default
+ hash format that SCons uses. It can also be set via SetOption('hash_format'). Supported
+ values include md5, sha1, and sha256, but you can also use any other algorithm that is
+ offered by your Python interpreter's hashlib package.
From Ivan Kravets, PlatformIO
- New conditional C Scanner (`SCons.Scanner.C.CConditionalScanner()`)
diff --git a/SCons/Action.py b/SCons/Action.py
index 1f499ee..f29d6a7 100644
--- a/SCons/Action.py
+++ b/SCons/Action.py
@@ -31,7 +31,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 17294d5..798150f 100644
--- a/SCons/CacheDirTests.py
+++ b/SCons/CacheDirTests.py
@@ -98,10 +98,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'
@@ -112,7 +112,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 b3f6d1d..20ded91 100644
--- a/SCons/Defaults.py
+++ b/SCons/Defaults.py
@@ -84,10 +84,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 bb57e37..8e2b51b 100644
--- a/SCons/Environment.py
+++ b/SCons/Environment.py
@@ -1492,10 +1492,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 == ('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 ac40f74..08df523 100644
--- a/SCons/Environment.xml
+++ b/SCons/Environment.xml
@@ -1117,6 +1117,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>
@@ -1131,7 +1134,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 55d94f9..00e3726 100644
--- a/SCons/Node/Alias.py
+++ b/SCons/Node/Alias.py
@@ -37,7 +37,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):
@@ -167,7 +167,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 ad9dd6f..044b3b6 100644
--- a/SCons/Node/FS.py
+++ b/SCons/Node/FS.py
@@ -53,7 +53,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
from SCons.Debug import Trace
@@ -1870,7 +1870,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
@@ -2635,7 +2635,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,
@@ -2734,10 +2734,10 @@ class File(Base):
Compute and return the MD5 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
@@ -3223,7 +3223,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()
@@ -3235,7 +3235,7 @@ class File(Base):
csig = ''
else:
if not csig:
- csig = SCons.Util.MD5signature(contents)
+ csig = SCons.Util.hash_signature(contents)
ninfo.csig = csig
@@ -3624,7 +3624,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 = MD5filesignature(cachefile, File.hash_chunksize)
else:
self.cachedir_csig = self.get_csig()
return self.cachedir_csig
@@ -3644,7 +3644,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):
@@ -3675,7 +3675,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/__init__.py b/SCons/Node/__init__.py
index f7d9289..d64a468 100644
--- a/SCons/Node/__init__.py
+++ b/SCons/Node/__init__.py
@@ -58,7 +58,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
@@ -1181,7 +1181,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]
@@ -1219,7 +1219,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):
@@ -1510,7 +1510,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 6e2a0ba..1046019 100644
--- a/SCons/SConf.py
+++ b/SCons/SConf.py
@@ -602,7 +602,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
@@ -621,7 +621,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 66222ae..c8be54a 100644
--- a/SCons/Script/Main.py
+++ b/SCons/Script/Main.py
@@ -1111,8 +1111,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 9fee74e..e460fa7 100644
--- a/SCons/Script/SConsOptions.py
+++ b/SCons/Script/SConsOptions.py
@@ -131,6 +131,7 @@ class SConsValues(optparse.Values):
'clean',
'diskcheck',
'duplicate',
+ 'hash_format',
'help',
'implicit_cache',
'max_drift',
@@ -711,6 +712,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",
@@ -773,7 +779,7 @@ 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.",
metavar="N")
diff --git a/SCons/Util.py b/SCons/Util.py
index ae204a1..589bbd7 100644
--- a/SCons/Util.py
+++ b/SCons/Util.py
@@ -1464,66 +1464,145 @@ def RenameFunction(function, name):
function.__defaults__)
-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.
+
+ 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()
try:
- m.update(to_bytes(s))
- except TypeError as e:
- m.update(to_bytes(str(s)))
+ _hash_function = getattr(hashlib, hash_format_lower)
+ _hash_function()
+ except Exception:
+ 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 as e:
+ 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 c831108..319724d 100644
--- a/SCons/UtilTests.py
+++ b/SCons/UtilTests.py
@@ -25,6 +25,7 @@ __revision__ = "__FILE__ __REVISION__ __DATE__ __DEVELOPER__"
import SCons.compat
+import functools
import io
import os
import sys
@@ -766,24 +767,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):
diff --git a/doc/man/scons.xml b/doc/man/scons.xml
index 20d2d06..207433e 100644
--- a/doc/man/scons.xml
+++ b/doc/man/scons.xml
@@ -1069,6 +1069,21 @@ the help message not to be displayed.
</varlistentry>
<varlistentry>
+ <term><option>--hash-format=<replaceable>ALGORITHM</replaceable></option></term>
+ <listitem>
+<para>Set the hashing algorithm used by SCons to
+<replaceable>ALGORITHM</replaceable>.
+This value determines the hashing algorithm used in generating content signatures
+or &f-link-CacheDir; keys.</para>
+
+<para>The supported values for this parameter depend on your Python interpreter.
+Specifically, you can also use any algorithm that is offered by your Python
+interpreter's hashlib package. Commonly-supported algorithms include md5, sha1,
+and sha256.</para>
+ </listitem>
+ </varlistentry>
+
+ <varlistentry>
<term>
<option>-H</option>,
<option>--help-options</option>
diff --git a/test/option/hash-format.py b/test/option/hash-format.py
new file mode 100644
index 0000000..5dfa45d
--- /dev/null
+++ b/test/option/hash-format.py
@@ -0,0 +1,49 @@
+#!/usr/bin/env python
+#
+# __COPYRIGHT__
+#
+# 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.
+#
+
+__revision__ = "__FILE__ __REVISION__ __DATE__ __DEVELOPER__"
+
+import TestSCons
+
+test = TestSCons.TestSCons()
+
+test.dir_fixture('hash-format')
+test.write('f1.in', str(list(range(10))))
+test.write('f2.in', str(list(range(100000))))
+
+# Test passing the hash format by command-line.
+for algorithm in ['md5', 'sha1', 'sha256']:
+ test.run('--hash-format=%s .' % algorithm)
+
+# In this case, the SConstruct file will use SetOption to override the hash
+# format.
+test.run()
+
+test.pass_test()
+
+# Local Variables:
+# tab-width:4
+# indent-tabs-mode:nil
+# End:
+# vim: set expandtab tabstop=4 shiftwidth=4:
diff --git a/test/option/hash-format/.exclude_tests b/test/option/hash-format/.exclude_tests
new file mode 100644
index 0000000..3fb299e
--- /dev/null
+++ b/test/option/hash-format/.exclude_tests
@@ -0,0 +1 @@
+build.py
diff --git a/test/option/hash-format/SConstruct b/test/option/hash-format/SConstruct
new file mode 100644
index 0000000..07713ac
--- /dev/null
+++ b/test/option/hash-format/SConstruct
@@ -0,0 +1,27 @@
+import atexit
+import sys
+
+hash_format = GetOption('hash_format')
+if not hash_format:
+ # Override to SHA-256 to validate that override is effective
+ hash_format = 'sha256'
+ SetOption('hash_format', hash_format)
+
+DefaultEnvironment(tools=[])
+B = Builder(action = r'$PYTHON build.py $TARGETS $SOURCES')
+env = Environment(tools=[], BUILDERS = { 'B' : B })
+env['PYTHON'] = sys.executable
+f1 = env.B(target = 'f1.out', source = 'f1.in')
+
+def VerifyCsig():
+ csig = f1[0].get_csig()
+ if hash_format == 'md5':
+ assert csig == 'fe06ae4170d4fead2c958439c738859e', csig
+ elif hash_format == 'sha1':
+ assert csig == 'efe5c6daa743540e9561934e3e18628b336013f7', csig
+ elif hash_format == 'sha256':
+ assert csig == 'a28bb79aa5ca8a5eb2dc5910a103d1a6312e79d73ed8054787cee78cc532a6aa', csig
+ else:
+ raise Exception('Hash format %s is not supported in '
+ 'test/option/hash-format/SConstruct' % hash_format)
+atexit.register(VerifyCsig) \ No newline at end of file
diff --git a/test/option/hash-format/build.py b/test/option/hash-format/build.py
new file mode 100644
index 0000000..6b6baad
--- /dev/null
+++ b/test/option/hash-format/build.py
@@ -0,0 +1,3 @@
+import sys
+with open(sys.argv[1], 'wb') as f, open(sys.argv[2], 'rb') as infp:
+ f.write(infp.read()) \ No newline at end of file
diff --git a/test/option/hash-format/f1.in b/test/option/hash-format/f1.in
new file mode 100644
index 0000000..eafc800
--- /dev/null
+++ b/test/option/hash-format/f1.in
@@ -0,0 +1 @@
+[0, 1, 2, 3, 4, 5, 6, 7, 8, 9] \ No newline at end of file