diff --git a/pex/scie/__init__.py b/pex/scie/__init__.py index 9d1c81a21..9523f9fc5 100644 --- a/pex/scie/__init__.py +++ b/pex/scie/__init__.py @@ -187,6 +187,23 @@ def register_options(parser): "less than half the size of the distributions that ship with debug symbols." ), ) + parser.add_argument( + "--scie-hash-alg", + dest="scie_hash_algorithms", + default=[], + action="append", + type=str, + help=( + "Output a checksum file for each scie generated that is compatible with the shasum " + "family of tools. For each unique --scie-hash-alg specified, a sibling file to each " + "scie executable will be generated with the same stem as that scie file and hash " + "algorithm name suffix. The file will contain the hex fingerprint of the scie " + "executable using that algorithm to hash it. Supported algorithms include at least " + "md5, sha1, sha256, sha384 and sha512. For the complete list of supported hash " + "algorithms, see the science tool --hash documentation here: " + "https://science.scie.app/cli.html#science-lift-build." + ), + ) parser.add_argument( "--scie-science-binary", dest="scie_science_binary", @@ -225,6 +242,9 @@ def render_options(options): args.append(".".join(map(str, options.python_version))) if options.pbs_stripped: args.append("--scie-pbs-stripped") + for hash_algorithm in options.hash_algorithms: + args.append("--scie-hash-alg") + args.append(hash_algorithm) if options.science_binary: args.append("--scie-science-binary") args.append(options.science_binary) @@ -310,6 +330,7 @@ def extract_options(options): pypy_release=options.scie_pypy_release, python_version=python_version, pbs_stripped=options.scie_pbs_stripped, + hash_algorithms=tuple(options.scie_hash_algorithms), science_binary=science_binary, ) diff --git a/pex/scie/model.py b/pex/scie/model.py index 5c6a31b53..0d7218ce9 100644 --- a/pex/scie/model.py +++ b/pex/scie/model.py @@ -355,6 +355,7 @@ class ScieOptions(object): default=None ) # type: Optional[Union[Tuple[int, int], Tuple[int, int, int]]] pbs_stripped = attr.ib(default=False) # type: bool + hash_algorithms = attr.ib(default=()) # type: Tuple[str, ...] science_binary = attr.ib(default=None) # type: Optional[Union[File, Url]] def create_configuration(self, targets): diff --git a/pex/scie/science.py b/pex/scie/science.py index 8e1b3a83d..6c6753b34 100644 --- a/pex/scie/science.py +++ b/pex/scie/science.py @@ -442,6 +442,8 @@ def build( ) if use_platform_suffix: args.append("--use-platform-suffix") + for hash_algorithm in configuration.options.hash_algorithms: + args.extend(["--hash", hash_algorithm]) args.append(manifest.path) with open(os.devnull, "wb") as devnull: process = subprocess.Popen(args=args, stdout=devnull, stderr=subprocess.PIPE) diff --git a/tests/integration/scie/test_pex_scie.py b/tests/integration/scie/test_pex_scie.py index 16d3e592f..f3f7c5c9e 100644 --- a/tests/integration/scie/test_pex_scie.py +++ b/tests/integration/scie/test_pex_scie.py @@ -100,6 +100,49 @@ def test_basic( assert b"| PAR! |" in subprocess.check_output(args=[scie, "PAR!"], env=make_env(PATH=None)) +@pytest.mark.skipif( + PY_VER < (3, 8) and not IS_PYPY, + reason="Scie output is not supported for {interpreter}".format(interpreter=sys.version), +) +@pytest.mark.skipif( + not any( + is_exe(os.path.join(entry, "shasum")) + for entry in os.environ.get("PATH", os.path.defpath).split(os.pathsep) + ), + reason="This test requires the `shasum` utility be available on the PATH.", +) +def test_hashes(tmpdir): + # type: (Any) -> None + + pex = os.path.join(str(tmpdir), "cowsay") + run_pex_command( + args=[ + "cowsay==5.0", + "-c", + "cowsay", + "-o", + pex, + "--scie", + "lazy", + "--scie-hash-alg", + "sha256", + "--scie-hash-alg", + "sha512", + ] + ).assert_success() + + assert b"| PEX-scie wabbit! |" in subprocess.check_output( + args=[pex, "PEX-scie wabbit!"], env=make_env(PATH=None) + ) + + for alg in "sha256", "sha512": + shasum_file = "{pex}.{alg}".format(pex=pex, alg=alg) + assert os.path.exists(shasum_file), "Expected {shasum_file} to be generated.".format( + shasum_file=shasum_file + ) + subprocess.check_call(args=["shasum", "-c", os.path.basename(shasum_file)], cwd=str(tmpdir)) + + def test_multiple_platforms(tmpdir): # type: (Any) -> None