mirror of
https://github.com/kamb205/KB-RPCS3.git
synced 2026-10-23 17:01:11 +02:00
208 lines
8.7 KiB
Python
208 lines
8.7 KiB
Python
#!/usr/bin/env python3
|
|
"""PS5 Vulkan Template - the assets the title ships, and their notices.
|
|
|
|
build-assets.py --install dist/PPSA99130 lay the assets of the samples
|
|
linked in (build/ps5/samples.txt)
|
|
into the title folder's assets/
|
|
build-assets.py --notices rewrite ps5/ASSETS.md
|
|
|
|
Every asset comes from ps5/assets.json, which names its origin and licence:
|
|
"download" (files at pinned URLs and SHA-256 hashes, among them the asset pack's
|
|
whose licences are clear), "file" (kept in the repository), "generated" (made by ps5/tools/generate_assets.py, deterministically,
|
|
from geometry and noise written there or from CC0 files it downloads at pinned
|
|
SHA-256 hashes into build/ps5/downloads/), or "kit" (the UI kit's fonts and
|
|
sounds, from the pinned PS5_VKHomebrewUI that ps5/ui/setup-kit.sh exports into
|
|
.deps/hui; installed when the title links the UI module, "@ui" in used_by). The
|
|
title's assets/NOTICES.txt lists what was installed, with the licences.
|
|
|
|
Copyright (C) 2026 Mihawk
|
|
SPDX-License-Identifier: MIT
|
|
"""
|
|
|
|
import argparse
|
|
import hashlib
|
|
import json
|
|
import os
|
|
import shutil
|
|
import sys
|
|
import urllib.request
|
|
from pathlib import Path
|
|
|
|
PS5 = Path(__file__).resolve().parent.parent
|
|
ROOT = PS5.parent
|
|
BUILD = ROOT / "build" / "ps5"
|
|
DOWNLOADS = BUILD / "downloads"
|
|
GENERATED = BUILD / "generated"
|
|
KIT = ROOT / ".deps" / "hui"
|
|
|
|
|
|
def load_manifest():
|
|
return json.loads((PS5 / "assets.json").read_text())
|
|
|
|
|
|
def linked_samples():
|
|
listing = BUILD / "samples.txt"
|
|
if not listing.is_file():
|
|
sys.exit("no build/ps5/samples.txt: build the title first (ps5/tools/build.sh)")
|
|
return [s for s in listing.read_text().strip().split(";") if s]
|
|
|
|
|
|
def ui_linked():
|
|
"""The build compiled the UI module (ps5/ui/ui.cmake writes build/ps5/ui.txt)."""
|
|
return (BUILD / "ui.txt").is_file()
|
|
|
|
|
|
def licence_path(asset):
|
|
"""A licence text kept in ps5/, or one the kit carries ("kit:<path>")."""
|
|
name = asset["licence_file"]
|
|
return KIT / name[4:] if name.startswith("kit:") else PS5 / name
|
|
|
|
|
|
def download(url, sha256):
|
|
DOWNLOADS.mkdir(parents=True, exist_ok=True)
|
|
target = DOWNLOADS / sha256
|
|
if target.is_file() and hashlib.sha256(target.read_bytes()).hexdigest() == sha256:
|
|
return target
|
|
print(f"download {url}")
|
|
request = urllib.request.Request(url, headers={"User-Agent": "ps5-vulkan-samples-asset-build"})
|
|
with urllib.request.urlopen(request, timeout=120) as response:
|
|
data = response.read()
|
|
actual = hashlib.sha256(data).hexdigest()
|
|
if actual != sha256:
|
|
sys.exit(f"{url}: sha256 {actual}, expected {sha256}")
|
|
target.write_bytes(data)
|
|
return target
|
|
|
|
|
|
def source_path(asset, manifest):
|
|
kind = asset["from"]
|
|
if kind == "download":
|
|
# One file ("" its name), or a folder's files by their paths in it
|
|
files = asset["files"]
|
|
if list(files) == [""]:
|
|
return download(*files[""])
|
|
folder = DOWNLOADS / "folders" / asset["path"]
|
|
for name, (url, sha256) in files.items():
|
|
(folder / name).parent.mkdir(parents=True, exist_ok=True)
|
|
shutil.copy2(download(url, sha256), folder / name)
|
|
return folder
|
|
if kind == "file":
|
|
# Kept in the repository itself (a title made by new-title.py has no asset pack)
|
|
return ROOT / asset["source"]
|
|
if kind == "kit":
|
|
path = KIT / asset["source"]
|
|
if not path.exists():
|
|
sys.exit(f"{asset['path']}: no {path} (the UI kit is exported when the title is built)")
|
|
return path
|
|
if kind == "generated":
|
|
sys.path.insert(0, str(PS5 / "tools"))
|
|
import generate_assets
|
|
generate_assets.build(asset["generator"], GENERATED, download)
|
|
path = GENERATED / asset["path"]
|
|
if not path.exists():
|
|
sys.exit(f"generator {asset['generator']} did not make {asset['path']}")
|
|
return path
|
|
sys.exit(f"{asset['path']}: unknown source kind {kind}")
|
|
|
|
|
|
def copy_asset(source, target):
|
|
"""A file or a folder; a .gltf brings the buffers and images it names."""
|
|
target.parent.mkdir(parents=True, exist_ok=True)
|
|
if source.is_dir():
|
|
shutil.copytree(source, target, copy_function=shutil.copy2,
|
|
ignore=shutil.ignore_patterns("screenshot", "README.md"), dirs_exist_ok=True)
|
|
return
|
|
shutil.copy2(source, target)
|
|
if source.suffix == ".gltf":
|
|
gltf = json.loads(source.read_text())
|
|
for item in gltf.get("buffers", []) + gltf.get("images", []):
|
|
uri = item.get("uri", "")
|
|
if uri and not uri.startswith("data:"):
|
|
(target.parent / uri).parent.mkdir(parents=True, exist_ok=True)
|
|
shutil.copy2(source.parent / uri, target.parent / uri)
|
|
|
|
|
|
def install(folder):
|
|
manifest = load_manifest()
|
|
samples = set(linked_samples())
|
|
destination = Path(folder) / "assets"
|
|
staging = Path(folder) / "assets.new"
|
|
if staging.exists():
|
|
shutil.rmtree(staging)
|
|
staging.mkdir(parents=True)
|
|
installed = []
|
|
for asset in manifest["assets"]:
|
|
used = set(asset["used_by"])
|
|
if "*" not in used and not samples.intersection(used) and not ("@ui" in used and ui_linked()):
|
|
continue
|
|
copy_asset(source_path(asset, manifest), staging / asset["path"])
|
|
if asset.get("licence_file"):
|
|
licence = licence_path(asset)
|
|
(staging / "LICENSES").mkdir(exist_ok=True)
|
|
shutil.copy2(licence, staging / "LICENSES" / licence.name)
|
|
installed.append(asset)
|
|
(staging / "NOTICES.txt").write_text(notices_text(installed))
|
|
if destination.exists():
|
|
shutil.rmtree(destination)
|
|
staging.rename(destination)
|
|
size = sum(f.stat().st_size for f in destination.rglob("*") if f.is_file())
|
|
print(f"==> assets: {len(installed)} entries, {size / 1e6:.1f} MB in {destination}")
|
|
|
|
|
|
def notices_text(assets):
|
|
name = json.loads((PS5 / "sce_sys" / "param.json").read_text())["localizedParameters"]["en-US"]["titleName"]
|
|
lines = [f"{name} - the assets in this folder, their authors and licences.", ""]
|
|
for asset in assets:
|
|
lines.append(f"{asset['path']}: {asset['title']}, {asset['author']}. {asset['licence']}. {asset['url']}")
|
|
if asset.get("changes"):
|
|
lines.append(f" Changes: {asset['changes']}")
|
|
if asset.get("licence_file"):
|
|
lines.append(f" Licence text: LICENSES/{Path(asset['licence_file']).name}")
|
|
lines += ["", "CC0-1.0: https://creativecommons.org/publicdomain/zero/1.0/",
|
|
"CC-BY-3.0: https://creativecommons.org/licenses/by/3.0/",
|
|
"MIT: made by ps5/tools/generate_assets.py (from PS5 Vulkan Template, github.com/mihawk-99/PS5_VulkanTemplate), under LICENSE.md"]
|
|
# The UI kit's licences, when its assets are in
|
|
used = {asset["licence"] for asset in assets}
|
|
for licence, line in (("OFL-1.1", "OFL-1.1: https://openfontlicense.org"),
|
|
("Bitstream-Vera", "Bitstream-Vera: https://dejavu-fonts.github.io/License.html"),
|
|
("GPL-3.0-or-later", "GPL-3.0-or-later: https://www.gnu.org/licenses/gpl-3.0.html")):
|
|
if licence in used:
|
|
lines.append(line)
|
|
return "\n".join(lines) + "\n"
|
|
|
|
|
|
def write_notices():
|
|
manifest = load_manifest()
|
|
out = ["# Assets", "",
|
|
"The assets the title ships, written from `ps5/assets.json` by",
|
|
"`ps5/tools/build-assets.py --notices`. Only assets with a clear licence are",
|
|
"shipped: in PS5_VulkanTemplate, the asset pack's other files (the `assets`",
|
|
"submodule) stay out, and the samples that use them get the replacements here.", "",
|
|
"| Path under `/app0/assets/` | Asset | Author | Licence | Samples |",
|
|
"| --- | --- | --- | --- | --- |"]
|
|
for asset in manifest["assets"]:
|
|
used = ", ".join(asset["used_by"]).replace("*", "all").replace("@ui", "the UI module")
|
|
title = f"[{asset['title']}]({asset['url']})"
|
|
if asset.get("changes"):
|
|
title += f" ({asset['changes']})"
|
|
out.append(f"| `{asset['path']}` | {title} | {asset['author']} | {asset['licence']} | {used} |")
|
|
(PS5 / "ASSETS.md").write_text("\n".join(out) + "\n")
|
|
print(f"wrote {PS5 / 'ASSETS.md'}")
|
|
|
|
|
|
def main():
|
|
parser = argparse.ArgumentParser(description=__doc__, formatter_class=argparse.RawDescriptionHelpFormatter)
|
|
parser.add_argument("--install", metavar="TITLE_FOLDER")
|
|
parser.add_argument("--notices", action="store_true")
|
|
args = parser.parse_args()
|
|
if not args.install and not args.notices:
|
|
parser.error("nothing to do")
|
|
if args.notices:
|
|
write_notices()
|
|
if args.install:
|
|
install(args.install)
|
|
|
|
|
|
if __name__ == "__main__":
|
|
main()
|