#!/usr/bin/env python3 """Verify and prepare the pinned official Discord mobile SDK for CI.""" import argparse import hashlib import os import shutil import subprocess import sys import tempfile import zipfile from pathlib import Path from typing import Optional VERSION = "1.10.19337" PROJECT_ROOT = Path(__file__).resolve().parent.parent DEFAULT_OUTPUT = PROJECT_ROOT / ".dart_tool" / "discord-social-sdk" / VERSION ENCRYPTED_ARCHIVE = PROJECT_ROOT / "third_party" / "discord" / f"discord-mobile-{VERSION}.zip.gpg" XCFRAMEWORK = "lib/release/discord_partner_sdk.xcframework" FILES = { "License-Notices.txt": "e8afa66340c225431e69768543cc34a7240f3494a9d759189fe118620ea8eebf", "include/discordpp.h": "822e71509bf11b7155becca5fb00d215a7db7b305f357c0f3b5f6e27ff45b6ab", "lib/release/discord_partner_sdk.aar": "b1b2491f1e1848c79fd6f1986d5aa1e0c8019e88a6e62c7be06b89e8e4870933", f"{XCFRAMEWORK}/Info.plist": "bb227b40a5adb4e4a02447b46a91b7a0fe28c9d238fbb80e19b7215c7ae3f431", } for slice_name, plist, binary in ( ("ios-arm64", "41bc76fed4b9d3adb7d711e5b44848b87f775598ed24173abfd1f4a6a0656a90", "8d86063b1a6c5c562b822378d34f55d335b37358e20f3425d446cc335ffae8bd"), ("ios-arm64-simulator", "a3e162d5587897f9435e7897dbcde4d5b32c6dd445a0c8ff897157f5f340e4a1", "eddc98dcb0e5614d64bef4c17476e5bb1ad53b3a7e8d6d8fc550b3abfc067f98"), ): framework = f"{XCFRAMEWORK}/{slice_name}/discord_partner_sdk.framework" FILES.update({ f"{framework}/Info.plist": plist, f"{framework}/discord_partner_sdk": binary, f"{framework}/Headers/cdiscord.h": "86ef9280ab8432493952bb974bd18c5a25d7725011393b5dba05162bcfdb3371", f"{framework}/Headers/discord_partner_sdk.h": "b049099dab197146011b3ceecc823ec25fdbbda7a37ea1584be8e472501a390b", f"{framework}/Headers/discordpp.h": FILES["include/discordpp.h"], f"{framework}/Modules/module.modulemap": "48879037011d015ad2c9802fdc478862785ba073e7f96dfdf6c0865b6075a542", }) MAX_FILE_BYTES = 64 * 1024 * 1024 class SdkError(Exception): pass def verify(directory: Path) -> None: for relative, expected in FILES.items(): path = directory / relative if not path.is_file() or path.is_symlink(): raise SdkError("Missing or invalid Discord SDK file: " + relative) digest = hashlib.sha256() with path.open("rb") as stream: for chunk in iter(lambda: stream.read(1024 * 1024), b""): digest.update(chunk) if digest.hexdigest() != expected: raise SdkError(f"Discord SDK {VERSION} checksum mismatch: {relative}") def unpack(archive: Path, destination: Path) -> None: with zipfile.ZipFile(archive) as zf: for relative in FILES: matches = [info for info in zf.infolist() if not info.is_dir() and (info.filename == relative or info.filename.endswith("/" + relative))] if len(matches) != 1: raise SdkError("SDK archive must contain exactly one " + relative) if matches[0].file_size > MAX_FILE_BYTES: raise SdkError("SDK archive entry exceeds size limit: " + relative) path = destination / relative path.parent.mkdir(parents=True, exist_ok=True) # Write only pinned destinations, never paths supplied by the ZIP. with zf.open(matches[0]) as source, path.open("wb") as target: shutil.copyfileobj(source, target) if relative.endswith(".framework/discord_partner_sdk"): path.chmod(0o755) def prepare(output: Path, source_dir: Optional[Path] = None, archive: Optional[Path] = None, passphrase: Optional[str] = None) -> Path: output = output.expanduser().resolve() if source_dir is None and archive is None and output.is_dir(): verify(output) return output if source_dir is None and archive is None and not passphrase: raise SdkError( f"Discord SDK {VERSION} is required. Supply --source-dir, --archive, " "or the Actions secret DISCORD_MOBILE_SDK_PASSPHRASE." ) output.parent.mkdir(parents=True, exist_ok=True) with tempfile.TemporaryDirectory(prefix="discord-sdk-", dir=output.parent) as temporary: staging = Path(temporary) / "sdk" if source_dir is not None: source_dir = source_dir.expanduser().resolve() verify(source_dir) for relative in FILES: target = staging / relative target.parent.mkdir(parents=True, exist_ok=True) shutil.copy2(source_dir / relative, target) else: if archive is None: archive = Path(temporary) / "sdk.zip" try: result = subprocess.run( ["gpg", "--batch", "--quiet", "--no-symkey-cache", "--pinentry-mode", "loopback", "--passphrase-fd", "0", "--output", str(archive), "--decrypt", str(ENCRYPTED_ARCHIVE)], input=passphrase.encode(), stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL, check=False, ) except FileNotFoundError as exc: raise SdkError("Install GnuPG to decrypt the Discord SDK") from exc if result.returncode != 0: raise SdkError("SDK decryption failed; check DISCORD_MOBILE_SDK_PASSPHRASE") unpack(archive, staging) verify(staging) for relative in FILES: target = output / relative target.parent.mkdir(parents=True, exist_ok=True) (staging / relative).replace(target) verify(output) return output def main() -> int: parser = argparse.ArgumentParser(description=__doc__) parser.add_argument("--output", type=Path, default=DEFAULT_OUTPUT) parser.add_argument("--source-dir", type=Path) parser.add_argument("--archive", type=Path, help="official SDK ZIP for offline setup") args = parser.parse_args() try: output = prepare(args.output, args.source_dir, args.archive, os.environ.get("DISCORD_MOBILE_SDK_PASSPHRASE")) except (SdkError, OSError, zipfile.BadZipFile) as exc: print(f"error: {exc}", file=sys.stderr) return 1 # stdout contains only the verified directory, suitable for setup_discord_sdk.sh. print(output) return 0 if __name__ == "__main__": raise SystemExit(main())