clone

a photo toolkit for your phone that never uploads: fuji film looks for raw files, social crops, error level analysis

main
gudpics / scripts / package.py
package.py 4.0K
1"""Package public assets into dist/ with content-hashed filenames.2SPDX-License-Identifier: AGPL-3.0-or-later34Scripts, styles and wasm land in dist/static/ as name.<hash>.ext and every5reference to them is rewritten, so browsers and the service worker can cache6them forever while HTML pages are always fetched fresh.7"""8from pathlib import Path9import shutil, hashlib, re, os10root = Path(__file__).resolve().parent.parent11dist = root / 'dist'12if dist.exists(): shutil.rmtree(dist)13(dist / 'static').mkdir(parents=True)1415# Hashed files in dependency order: leaves first, so a file's hash covers its rewritten references.16LUTS = sorted(p.relative_to(root).as_posix() for p in (root / 'assets' / 'luts').glob('*.png'))17HASHED = ['assets/core.wasm', 'assets/core.mjs', 'assets/raw.wasm', 'assets/raw.mjs'] + LUTS + ['geometry.mjs', 'worker.mjs', 'app.mjs', 'raf.mjs', 'lens.mjs', 'rawworker.mjs', 'fuji.mjs', 'social.mjs', 'elaworker.mjs', 'ela.mjs', 'home.mjs', 'viewport.js', 'style.css']18# Fixed-name text files that reference hashed ones.19PAGES = ['index.html', 'perspective.html', 'fuji.html', 'social.html', 'ela.html']20FIXED = ['manifest.webmanifest', 'NOTICE.md', 'README.md', '_headers']21DIRS = ['LICENSES']22ICONS = ['assets/icon.svg', 'assets/icon-192.png', 'assets/icon-512.png']2324def digest(data): return hashlib.sha256(data).hexdigest()[:10]2526# `forge site deploy` sets FORGE_SITE_BASE and serves dist/ from a plain file server under that27# path. It has no extensionless routes like Cloudflare's, so there pages are reached by file name.28ROUTES = ['perspective', 'fuji', 'social', 'ela'] if os.environ.get('FORGE_SITE_BASE') else []29def routed(text):30 for r in ROUTES:31 for q in '"\'':32 text = text.replace(f'{q}./{r}{q}', f'{q}./{r}.html{q}').replace(f'{q}/{r}{q}', f'{q}/{r}.html{q}')33 return text3435mapping = {} # source path -> dist path (relative to dist)3637def rewrite(text, at_dir):38 """Replace './<source path>' references with the hashed dist path relative to at_dir."""39 for src, dst in mapping.items():40 rel = Path(dst).relative_to(at_dir) if Path(dst).is_relative_to(at_dir) else Path('..') / dst41 text = text.replace("'./" + src + "'", "'./" + rel.as_posix() + "'").replace('"./' + src + '"', '"./' + rel.as_posix() + '"')42 return text4344for src in HASHED:45 p = root / src46 data = p.read_bytes()47 if p.suffix not in ('.wasm', '.png'):48 text = rewrite(data.decode(), 'static')49 if src in ('assets/core.mjs', 'assets/raw.mjs'): # emscripten loader refers to its binary by bare name50 wasm = src.replace('.mjs', '.wasm'); text = text.replace('"' + Path(wasm).name + '"', '"' + Path(mapping[wasm]).name + '"')51 data = text.encode()52 name = f'{p.stem}.{digest(data)}{p.suffix}'53 mapping[src] = f'static/{name}'54 (dist / 'static' / name).write_bytes(data)5556for src in PAGES + FIXED:57 (dist / src).parent.mkdir(parents=True, exist_ok=True)58 text = (root / src).read_text()59 if src.endswith('.html'): text = rewrite(text, '.')60 if src.endswith(('.html', '.webmanifest')): text = routed(text)61 (dist / src).write_text(text)62for d in DIRS: shutil.copytree(root / d, dist / d)63(dist / 'assets').mkdir(exist_ok=True)64for src in ICONS: shutil.copyfile(root / src, dist / src)6566# Service worker: precache list and a cache name derived from everything in dist.67precache = ['./', './perspective', './fuji', './social', './ela'] + ['./' + v for v in mapping.values()] + ['./' + f for f in ['manifest.webmanifest', 'NOTICE.md'] + ICONS] + ['./LICENSES/' + p.name for p in sorted((root / 'LICENSES').iterdir()) if p.is_file()]68h = hashlib.sha256()69for p in sorted(dist.rglob('*')):70 if p.is_file(): h.update(p.read_bytes())71sw = (root / 'sw.js').read_text().replace('__CACHE__', 'gudpics-' + h.hexdigest()[:12]).replace('__FILES__', ','.join(f"'{f}'" for f in precache))72(dist / 'sw.js').write_text(routed(sw))73print(f'Packaged {sum(1 for p in dist.rglob("*") if p.is_file())} files; ' + ', '.join(Path(v).name for v in mapping.values()))