#!/usr/bin/env python3 """Generate deterministic gzip-compressed LittleFS web assets.""" from __future__ import annotations import argparse import gzip import os import shutil from pathlib import Path SUPPORTED_SUFFIXES = {".css", ".html", ".ico", ".js", ".json", ".svg"} def generate_assets(source_directory: Path, output_directory: Path) -> list[Path]: source_directory = source_directory.resolve() output_directory = output_directory.resolve() if not source_directory.is_dir(): raise ValueError(f"Web source directory does not exist: {source_directory}") if source_directory == output_directory: raise ValueError("Web source and output directories must differ") source_files = sorted( path for path in source_directory.rglob("*") if path.is_file() and path.suffix.lower() in SUPPORTED_SUFFIXES ) if not source_files: raise ValueError(f"No supported web assets found in {source_directory}") if output_directory.exists(): shutil.rmtree(output_directory) output_directory.mkdir(parents=True) generated_files: list[Path] = [] for source_path in source_files: relative_path = source_path.relative_to(source_directory) output_path = output_directory / f"{relative_path}.gz" output_path.parent.mkdir(parents=True, exist_ok=True) with source_path.open("rb") as source, output_path.open("wb") as output: with gzip.GzipFile( filename="", mode="wb", compresslevel=9, fileobj=output, mtime=0, ) as compressed: shutil.copyfileobj(source, compressed) os.utime(output_path, ns=(0, 0)) generated_files.append(output_path) for directory in sorted( (path for path in output_directory.rglob("*") if path.is_dir()), reverse=True, ): os.utime(directory, ns=(0, 0)) os.utime(output_directory, ns=(0, 0)) return generated_files def main() -> int: parser = argparse.ArgumentParser(description=__doc__) parser.add_argument("--source", type=Path, default=Path("web")) parser.add_argument("--output", type=Path, default=Path("data")) arguments = parser.parse_args() generated_files = generate_assets(arguments.source, arguments.output) for path in generated_files: print(path) return 0 if __name__ == "__main__": raise SystemExit(main())