AnyPS5/tools/build_web.py

115 lines
4.7 KiB
Python

import argparse
import hashlib
import json
import os
from pathlib import Path
import re
import shutil
import urllib.request
import zipfile
ROOT = Path(__file__).resolve().parent.parent
def copy_sources(output):
source = ROOT / "demo/web"
for file in source.iterdir():
if file.is_file():
shutil.copy2(file, output / file.name)
library = ROOT / "3rdparty/zip.js/lib"
pending = [library / "zip-no-worker.js"]
copied = set()
while pending:
file = pending.pop().resolve()
if file in copied:
continue
if not file.is_relative_to(library.resolve()):
raise ValueError(f"ZIP source import escapes library: {file}")
text = file.read_text(encoding="utf-8")
copied.add(file)
destination = output / "vendor/zip" / file.relative_to(library.resolve())
destination.parent.mkdir(parents=True, exist_ok=True)
shutil.copy2(file, destination)
for name in re.findall(r"(?:from\s+|import\s*)[\"'](\.[^\"']+)[\"']", text):
pending.append(file.parent / name)
shutil.copy2(ROOT / "3rdparty/zip.js/LICENSE", output / "vendor/zip/LICENSE")
(output / ".nojekyll").touch()
def request(url):
headers = {"User-Agent": "AnyPS5-Pages"}
token = os.environ.get("GITHUB_TOKEN")
if token and url.startswith("https://api.github.com/"):
headers["Authorization"] = f"Bearer {token}"
return urllib.request.urlopen(urllib.request.Request(url, headers=headers), timeout=60)
def publish_libraries(output):
with request("https://api.github.com/repos/boykopovar/AnyPS5/releases/latest") as response:
release = json.load(response)
tag = release["tag_name"]
if not re.fullmatch(r"v[0-9A-Za-z][0-9A-Za-z._-]*", tag):
raise ValueError("Invalid release tag")
manifest = {"tag": tag, "url": release["html_url"], "platforms": {}}
destination = output / "libraries"
destination.mkdir(exist_ok=True)
for platform in ("windows", "linux"):
name = f"prx-{platform}-{tag}.zip"
matches = [asset for asset in release["assets"] if asset["name"] == name]
if len(matches) != 1:
raise ValueError(f"Missing or ambiguous release asset: {name}")
asset = matches[0]
temporary = destination / f"{platform}.download"
try:
digest = hashlib.sha256()
size = 0
with request(asset["browser_download_url"]) as response, temporary.open("wb") as target:
while chunk := response.read(1024 * 1024):
size += len(chunk)
if size > asset["size"] or size > 512 * 1024 * 1024:
raise ValueError("Release archive exceeds expected size")
digest.update(chunk)
target.write(chunk)
if size != asset["size"]:
raise ValueError("Incomplete release archive")
checksum = digest.hexdigest()
if asset.get("digest") and asset["digest"] != "sha256:" + checksum:
raise ValueError("Release archive checksum mismatch")
with zipfile.ZipFile(temporary) as archive:
files = archive.infolist()
if not files or len(files) > 4096:
raise ValueError("Invalid library archive")
for file in files:
if not file.filename.startswith("libs/") or any(part in ("..", ".", "") for part in file.filename.rstrip("/").split("/")) or "\\" in file.filename:
raise ValueError("Invalid library archive layout")
corrupt = archive.testzip()
if corrupt:
raise ValueError(f"Corrupt release entry: {corrupt}")
expanded = sum(file.file_size for file in files)
path = f"libraries/{platform}-{checksum}.zip"
temporary.replace(output / path)
manifest["platforms"][platform] = {"path": path, "size": size, "expandedBytes": expanded, "sha256": checksum}
finally:
temporary.unlink(missing_ok=True)
(output / "release.json").write_text(json.dumps(manifest, indent=2) + "\n", encoding="utf-8")
print(f"Published library assets from {tag}")
def main():
parser = argparse.ArgumentParser()
parser.add_argument("--build", type=Path, required=True)
parser.add_argument("--libraries", action="store_true")
args = parser.parse_args()
output = args.build.resolve() / "web"
for name in ("relinker_wasm.mjs", "relinker_wasm.wasm"):
if not (output / "native" / name).is_file():
raise ValueError(f"Build relinker_wasm first: missing {name}")
copy_sources(output)
if args.libraries:
publish_libraries(output)
print(output)
if __name__ == "__main__":
main()