| """Creates the __main__.py for a zipapp by populating a template. |
| |
| This program also calculates a hash of the application files to include in |
| the template, which allows making the extraction directory unique to the |
| content of the zipapp. |
| """ |
| |
| import argparse |
| import hashlib |
| import os |
| |
| BLOCK_SIZE = 256 * 1024 |
| |
| |
| def create_parser() -> argparse.ArgumentParser: |
| parser = argparse.ArgumentParser(fromfile_prefix_chars="@") |
| parser.add_argument("--template", required=True) |
| parser.add_argument("--output", required=True) |
| parser.add_argument("--substitution", action="append", default=[]) |
| parser.add_argument( |
| "--hash_files_manifest", |
| required=True, |
| help="A file containing lines in rf-XXX formats (rf-empty, rf-file, rf-symlink, etc.)", |
| ) |
| return parser |
| |
| |
| def compute_inputs_hash(manifest_path: str) -> str: |
| h = hashlib.sha256() |
| with open(manifest_path, "r", encoding="utf-8") as f: |
| manifest_lines = f.read().splitlines() |
| |
| # Sort lines for determinism. Hash the paths (to capture structure) and the |
| # content. |
| for line in sorted(manifest_lines): |
| type_, _, rest = line.partition("|") |
| h.update(rest.encode("utf-8")) |
| parts = rest.split("|") |
| |
| if type_ == "rf-empty": |
| continue |
| if type_ == "symlink": |
| # The symlink path and the target it points to |
| # are captured by hashing the entire line above. |
| continue |
| |
| is_symlink_str = parts[0] |
| path = parts[-1] |
| |
| if is_symlink_str == "-1": |
| is_symlink = not os.path.exists(path) |
| else: |
| is_symlink = is_symlink_str == "1" |
| |
| if is_symlink: |
| h.update(os.readlink(path).encode("utf-8")) |
| else: |
| with open(path, "rb") as f: |
| while True: |
| chunk = f.read(BLOCK_SIZE) |
| if not chunk: |
| break |
| h.update(chunk) |
| |
| return h.hexdigest() |
| |
| |
| def expand_template(template_path: str, output_path: str, substitutions: dict) -> None: |
| with open(template_path, "r", encoding="utf-8") as f: |
| content = f.read() |
| |
| for key, val in substitutions.items(): |
| content = content.replace(key, val) |
| |
| with open(output_path, "w", encoding="utf-8") as f: |
| f.write(content) |
| |
| |
| def main(): |
| parser = create_parser() |
| args = parser.parse_args() |
| |
| app_hash = compute_inputs_hash(args.hash_files_manifest) |
| |
| substitutions = {"%APP_HASH%": app_hash} |
| for s in args.substitution: |
| key, val = s.split("=", 1) |
| substitutions[key] = val |
| |
| expand_template(args.template, args.output, substitutions) |
| |
| |
| if __name__ == "__main__": |
| main() |