#!/usr/bin/env python3 """Post-process `fdroid update` output for IPFS publishing. For each repodir (repo, archive) discovered from config.yml: - rename index-v2.json to index-v2-.json (cache-busts gateways) - rename diff/.json to diff/-.json - rewrite entry.json so name/ipfsCIDv1 point at the renamed files - re-sign entry.jar via fdroidserver.signindex.sign_index - regenerate entry.json.asc if GPG signing is configured Run in the same working directory as `fdroid update` (where config.yml lives). """ import argparse import hashlib import json import os import subprocess import sys from pathlib import Path # Make fdroidserver importable + invocable when this script is run outside the # upstream image's ENTRYPOINT (which sources /etc/profile.d/bsenv.sh). The # buildserver-style image places fdroidserver at $fdroidserver — typically # /home/vagrant/fdroidserver. Both sys.path (for the import below) and PATH # (for the `fdroid gpgsign` subprocess call) need it. _FDS = os.environ.get('fdroidserver') or '/home/vagrant/fdroidserver' if os.path.isdir(_FDS): if _FDS not in sys.path: sys.path.insert(0, _FDS) if _FDS not in os.environ.get('PATH', '').split(os.pathsep): os.environ['PATH'] = _FDS + os.pathsep + os.environ.get('PATH', '') from fdroidserver import common, signindex HASH_SUFFIX_LEN = 12 def sha256_last_chars(path: Path) -> str: h = hashlib.sha256() with path.open('rb') as fp: for chunk in iter(lambda: fp.read(65536), b''): h.update(chunk) return h.hexdigest()[-HASH_SUFFIX_LEN:] def write_entry_json(entry: dict, path: Path) -> None: with path.open('w', encoding='utf-8') as fp: json.dump(entry, fp, ensure_ascii=False, sort_keys=True) def rehash_entry_file(repodir: Path, file_entry: dict) -> None: """Rename the file referenced by file_entry["name"] and update name/ipfsCIDv1 in place.""" old_rel = file_entry['name'].lstrip('/') old_path = repodir / old_rel suffix = sha256_last_chars(old_path) new_path = old_path.with_name(f"{old_path.stem}-{suffix}{old_path.suffix}") cid = common.calculate_IPFS_cid(str(old_path)) if new_path != old_path: old_path.rename(new_path) file_entry['name'] = '/' + new_path.relative_to(repodir).as_posix() if cid: file_entry['ipfsCIDv1'] = cid def rehash_repodir(repodir_name: str) -> bool: repodir = Path(repodir_name) entry_json = repodir / 'entry.json' if not entry_json.exists(): return False with entry_json.open(encoding='utf-8') as fp: entry = json.load(fp) rehash_entry_file(repodir, entry['index']) for diff_entry in entry.get('diffs', {}).values(): rehash_entry_file(repodir, diff_entry) write_entry_json(entry, entry_json) signindex.sign_index(repodir_name, 'entry.json') return True def regenerate_gpg_sigs(repodirs: list) -> None: """Drop stale .asc files we just invalidated, then let `fdroid gpgsign` recreate them. We detect "GPG is part of this pipeline" by the prior existence of entry.json.asc rather than from config keys — matches whatever the user's pipeline actually does. gpgsign only signs files missing a sibling .asc, so deleting the stale ones is enough to trigger a fresh signature on the new filenames. """ pipeline_uses_gpg = any( (Path(r) / 'entry.json.asc').exists() for r in repodirs ) if not pipeline_uses_gpg: return for repodir_name in repodirs: repodir = Path(repodir_name) stale = [repodir / 'entry.json.asc'] stale += list(repodir.glob('index-v2*.json.asc')) if (repodir / 'diff').is_dir(): stale += list((repodir / 'diff').glob('*.json.asc')) for path in stale: if path.exists(): path.unlink() subprocess.run(['fdroid', 'gpgsign'], check=True) def main() -> int: parser = argparse.ArgumentParser(description=__doc__) parser.add_argument( 'repodirs', nargs='*', help='Repo directories to process. Default: auto-discover from config.yml.', ) args = parser.parse_args() config = common.read_config() signindex.config = config repodirs = args.repodirs if not repodirs: repodirs = ['repo'] if config.get('archive_older', 0) != 0: repodirs.append('archive') processed = [] for repodir in repodirs: if rehash_repodir(repodir): processed.append(repodir) regenerate_gpg_sigs(processed) return 0 if __name__ == '__main__': sys.exit(main())