Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add option to output file with installer hashes #816

Merged
merged 13 commits into from
Jul 30, 2024
3 changes: 3 additions & 0 deletions CONSTRUCT.md
Original file line number Diff line number Diff line change
Expand Up @@ -825,6 +825,9 @@ _type:_ list<br/>
Additional artifacts to be produced after building the installer.
It expects either a list of strings or single-key dictionaries:
Allowed keys are:
- `hash`: The hash of the installer files.
- `algorithm`: The hash algorithm. Must be among `hashlib`'s available algorithms:
https://docs.python.org/3/library/hashlib.html#hashlib.algorithms_available
- `info.json`: The internal `info` object, serialized to JSON. Takes no options.
- `pkgs_list`: The list of packages contained in a given environment. Options:
- `env` (optional, default=`base`): Name of an environment in `extra_envs` to export.
Expand Down
30 changes: 26 additions & 4 deletions constructor/build_outputs.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@

Update documentation in `construct.py` if any changes are made.
"""
import hashlib
import json
import logging
import os
Expand Down Expand Up @@ -33,14 +34,34 @@ def process_build_outputs(info):
f"Available keys: {tuple(OUTPUT_HANDLERS.keys())}"
)
outpath = handler(info, **config)
logger.info("build_outputs: '%s' created '%s'.", name, os.path.abspath(outpath))
logger.info("build_outputs: '%s' created '%s'.", name, outpath)


def dump_hash(info, algorithm=""):
if algorithm not in hashlib.algorithms_available:
raise ValueError(f"Invalid algorithm: {', '.join(algorithm)}")
jaimergp marked this conversation as resolved.
Show resolved Hide resolved
BUFFER_SIZE = 65536
if isinstance(info["_outpath"], str):
installers = [Path(info["_outpath"])]
else:
installers = [Path(outpath) for outpath in info["_outpath"]]
outpaths = []
for installer in installers:
filehash = hashlib.new(algorithm)
with open(installer, "rb") as f:
while buffer := f.read(BUFFER_SIZE):
filehash.update(buffer)
outpath = Path(f"{installer}.{algorithm}")
outpath.write_text(f"{filehash.hexdigest()} {installer.name}\n")
outpaths.append(str(outpath.absolute()))
return ", ".join(outpaths)


def dump_info(info):
outpath = os.path.join(info["_output_dir"], "info.json")
with open(outpath, "w") as f:
json.dump(info, f, indent=2, default=repr)
return outpath
return os.path.abspath(outpath)


def dump_packages_list(info, env="base"):
Expand All @@ -55,7 +76,7 @@ def dump_packages_list(info, env="base"):
with open(outpath, 'w') as fo:
fo.write(f"# {info['name']} {info['version']}, env={env}\n")
fo.write("\n".join(dists))
return outpath
return os.path.abspath(outpath)


def dump_licenses(info, include_text=False, text_errors=None):
Expand Down Expand Up @@ -105,10 +126,11 @@ def dump_licenses(info, include_text=False, text_errors=None):
outpath = os.path.join(info["_output_dir"], "licenses.json")
with open(outpath, "w") as f:
json.dump(licenses, f, indent=2, default=repr)
return outpath
return os.path.abspath(outpath)


OUTPUT_HANDLERS = {
"hash": dump_hash,
"info.json": dump_info,
"pkgs_list": dump_packages_list,
"licenses": dump_licenses,
Expand Down
3 changes: 3 additions & 0 deletions constructor/construct.py
Original file line number Diff line number Diff line change
Expand Up @@ -606,6 +606,9 @@
Additional artifacts to be produced after building the installer.
It expects either a list of strings or single-key dictionaries:
Allowed keys are:
- `hash`: The hash of the installer files.
- `algorithm`: The hash algorithm. Must be among `hashlib`'s available algorithms:
https://docs.python.org/3/library/hashlib.html#hashlib.algorithms_available
- `info.json`: The internal `info` object, serialized to JSON. Takes no options.
- `pkgs_list`: The list of packages contained in a given environment. Options:
- `env` (optional, default=`base`): Name of an environment in `extra_envs` to export.
Expand Down
12 changes: 12 additions & 0 deletions constructor/main.py
Original file line number Diff line number Diff line change
Expand Up @@ -229,6 +229,7 @@ def main_build(dir_path, output_dir='.', platform=cc_platform,
# '_dists': List[Dist]
# '_urls': List[Tuple[url, md5]]

info_dicts = []
for itype in itypes:
if itype == 'sh':
from .shar import create as shar_create
Expand All @@ -242,8 +243,19 @@ def main_build(dir_path, output_dir='.', platform=cc_platform,
info['installer_type'] = itype
info['_outpath'] = abspath(join(output_dir, get_output_filename(info)))
create(info, verbose=verbose)
if len(itypes) > 1:
info_dicts.append(info.copy())
logger.info("Successfully created '%(_outpath)s'.", info)

# Merge info files for each installer type
if len(itypes) > 1:
keys = set()
for info_dict in info_dicts:
keys.update(info_dict.keys())
for key in keys:
if any(info_dict.get(key) != info.get(key) for info_dict in info_dicts):
info[key] = [info_dict.get(key, "") for info_dict in info_dicts]

process_build_outputs(info)


Expand Down
3 changes: 3 additions & 0 deletions docs/source/construct-yaml.md
Original file line number Diff line number Diff line change
Expand Up @@ -825,6 +825,9 @@ _type:_ list<br/>
Additional artifacts to be produced after building the installer.
It expects either a list of strings or single-key dictionaries:
Allowed keys are:
- `hash`: The hash of the installer files.
- `algorithm`: The hash algorithm. Must be among `hashlib`'s available algorithms:
https://docs.python.org/3/library/hashlib.html#hashlib.algorithms_available
- `info.json`: The internal `info` object, serialized to JSON. Takes no options.
- `pkgs_list`: The list of packages contained in a given environment. Options:
- `env` (optional, default=`base`): Name of an environment in `extra_envs` to export.
Expand Down
19 changes: 19 additions & 0 deletions news/816-output-installer-hashes
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
### Enhancements

* Add option to output hashes of installer files. (#816)

### Bug fixes

* <news item>

### Deprecations

* <news item>

### Docs

* <news item>

### Other

* <news item>
27 changes: 27 additions & 0 deletions tests/test_outputs.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
from pathlib import Path

from constructor.build_outputs import dump_hash


def test_hash_dump(tmp_path):
testfile = tmp_path / "test.txt"
testfile.write_text("test string")
testfile = tmp_path / "test2.txt"
testfile.write_text("another test")
expected = (
"d5579c46dfcc7f18207013e65b44e4cb4e2c2298f4ac457ba8f82743f31e930b",
"64320dd12e5c2caeac673b91454dac750c08ba333639d129671c2f58cb5d0ad1",
)
info = {
"_outpath": [
str(tmp_path / "test.txt"),
str(tmp_path / "test2.txt"),
]
}
dump_hash(info, algorithm="sha256")
for f, file in enumerate(info["_outpath"]):
hashfile = Path(f"{file}.sha256")
assert hashfile.exists()
filehash, filename = hashfile.read_text().strip().split()
assert filehash == expected[f]
assert filename == Path(file).name