mirror of
https://github.com/MrLowPr/overdrive.git
synced 2026-10-20 18:02:02 +02:00
378 lines
25 KiB
Python
378 lines
25 KiB
Python
# Copyright (C) 2026 half144 and PS5 React contributors
|
|
# SPDX-License-Identifier: GPL-3.0-or-later
|
|
# Additional attribution term: see LICENSE-ATTRIBUTION.
|
|
"""Offline-console PS5 title build. Network is used only for public dependencies."""
|
|
import argparse
|
|
import hashlib
|
|
import io
|
|
import json
|
|
import os
|
|
from pathlib import Path
|
|
import shutil
|
|
import shlex
|
|
import subprocess
|
|
import tarfile
|
|
import urllib.request
|
|
import zipfile
|
|
|
|
from rar_worker import build as build_rar_worker, unrar_source
|
|
from common import layouts, layout_directory, resource_files, ROOT, DEPS, LOCK, run, digest, verify, fetch, app_files, bundle, dependency, stb_image, refresh_rate_mask
|
|
from network_ports import ports, copy_notices
|
|
import ffmpeg
|
|
from text_fonts import harfbuzz, sheenbidi
|
|
|
|
BUILD = ROOT / ".build/starter/ps5"
|
|
TITLE = "PPSA99053"
|
|
HUI_REV = LOCK["platform"]["revision"]
|
|
ER_REV = LOCK["embeddedReact"]["revision"]
|
|
GL_HASH = LOCK["ps5OpenGL"]["sha256"]
|
|
GL_MANIFEST = LOCK["ps5OpenGL"]["manifestSha256"]
|
|
RT_HASH = LOCK["runtimeShim"]["sha256"]
|
|
BUILTINS_PACKAGE = LOCK["compilerRt"]["package"]
|
|
BUILTINS_HASH = LOCK["compilerRt"]["sha256"]
|
|
|
|
def main():
|
|
global BUILD, TITLE
|
|
parser = argparse.ArgumentParser()
|
|
source = parser.add_mutually_exclusive_group()
|
|
source.add_argument("--app", default="starter")
|
|
source.add_argument("--app-dir", type=Path)
|
|
parser.add_argument("--compile-only", action="store_true", help="Validate the PS5 ELF without generating an application package")
|
|
parser.add_argument("--ui-reference", type=Path, default=DEPS / "platform")
|
|
parser.add_argument("--embedded-react", type=Path, default=DEPS / "embeddedReact")
|
|
parser.add_argument("--payload-sdk", type=Path, default=DEPS / "sdk/ps5-payload-sdk")
|
|
parser.add_argument("--llvm", type=Path, default=Path("/opt/homebrew/opt/llvm@18/bin"))
|
|
parser.add_argument("--opengl-archive", type=Path)
|
|
args = parser.parse_args()
|
|
app_dir = args.app_dir.resolve() if args.app_dir else ROOT / "apps" / args.app
|
|
BUILD = ROOT / ".build" / app_dir.name / "ps5"
|
|
for directory in (BUILD, DEPS, BUILD / "obj", BUILD / "host"):
|
|
directory.mkdir(parents=True, exist_ok=True)
|
|
hui = dependency("platform", args.ui_reference)
|
|
er = dependency("embeddedReact", args.embedded_react)
|
|
sdk = args.payload_sdk.resolve()
|
|
llvm = args.llvm.resolve()
|
|
if not (sdk / "bin/prospero-lld").exists():
|
|
package = fetch(LOCK["sdk"]["url"], DEPS / "sdk.zip", LOCK["sdk"]["sha256"])
|
|
with zipfile.ZipFile(package) as archive:
|
|
archive.extractall(DEPS / "sdk")
|
|
for path in (sdk / "bin").iterdir():
|
|
path.chmod(path.stat().st_mode | 0o111)
|
|
if not (sdk / "bin/prospero-lld").exists():
|
|
raise RuntimeError("SDK archive layout differs; set --payload-sdk")
|
|
env = dict(os.environ, LLVM_CONFIG=str(llvm / "llvm-config"),
|
|
PS5_CLANG=str(llvm / "clang"), PS5_PAYLOAD_SDK=str(sdk), USE_CCACHE="0")
|
|
|
|
archive = args.opengl_archive or DEPS / "ps5-opengl-sdk-1.0.0.tar.gz"
|
|
fetch("https://github.com/blackbearreloaded/ps5-opengl/releases/download/v1.0.0/ps5-opengl-sdk-1.0.0.tar.gz",
|
|
archive, GL_HASH)
|
|
gl = DEPS / "ps5-opengl-sdk-1.0.0/sdk"
|
|
if not gl.exists():
|
|
with tarfile.open(archive) as package:
|
|
selected = [m for m in package.getmembers() if "/sdk/" in m.name or
|
|
m.name.endswith(("LICENSE", "THIRD_PARTY_NOTICES.md"))]
|
|
package.extractall(DEPS, members=selected, filter="data")
|
|
with tarfile.open(archive) as package:
|
|
package.extractall(DEPS, members=[m for m in package.getmembers() if "/LICENSES/" in m.name], filter="data")
|
|
verify(gl / "manifest.sha256", GL_MANIFEST)
|
|
for line in (gl / "manifest.sha256").read_text().splitlines():
|
|
checksum, name = line.split(maxsplit=1)
|
|
verify(gl / name.lstrip("*"), checksum)
|
|
|
|
deb = fetch("https://apt.llvm.org/jammy/pool/main/l/llvm-toolchain-18/" + BUILTINS_PACKAGE,
|
|
DEPS / "compiler-rt-18.deb", BUILTINS_HASH)
|
|
builtins = DEPS / "libclang_rt.builtins-x86_64.a"
|
|
if not builtins.exists():
|
|
entries = subprocess.check_output(["ar", "t", str(deb)], text=True).splitlines()
|
|
name = next(x for x in entries if x.startswith("data.tar"))
|
|
data = subprocess.check_output(["ar", "p", str(deb), name])
|
|
try:
|
|
package = tarfile.open(fileobj=io.BytesIO(data))
|
|
except tarfile.ReadError:
|
|
# Local fix over upstream: the pinned LLVM package carries data.tar.zst, and tarfile reads
|
|
# zstd only from Python 3.14 while the documented minimum is 3.12. Use the zstd CLI there.
|
|
try:
|
|
data = subprocess.check_output(["zstd", "-dc"], input=data)
|
|
except FileNotFoundError as error:
|
|
raise RuntimeError(f"Unpacking {name} needs the zstd command on this Python") from error
|
|
package = tarfile.open(fileobj=io.BytesIO(data))
|
|
with package:
|
|
member = next(m for m in package.getmembers() if m.name.endswith("/libclang_rt.builtins-x86_64.a"))
|
|
builtins.write_bytes(package.extractfile(member).read())
|
|
|
|
_, config, generated = bundle(app_dir, er)
|
|
TITLE = config["titleId"]
|
|
access_client = dependency("filesystemHelperClient") if config.get("filesystemAccess") == "console" else None
|
|
helper = BUILD / "filesystem-helper"
|
|
if access_client:
|
|
run(["python3", ROOT / "tools/test_filesystem_access.py", access_client, BUILD / "access-tests"],
|
|
log=BUILD / "filesystem-access-tests.log")
|
|
run(["python3", ROOT / "tools/build_filesystem_helper.py", TITLE, helper, "--sdk", sdk],
|
|
env=env, log=BUILD / "filesystem-helper.log")
|
|
archive_ports = ports()
|
|
network_ports = archive_ports if config.get("networking") else None
|
|
native = hui / "tooling/native"
|
|
host_tool = BUILD / "host/ps5-native-tool"
|
|
run(["clang++", "-std=c++20", "-O2", "-Wall", "-Wextra", "-Werror",
|
|
*[native / (n + ".cpp") for n in ("native_app_builder", "self_container", "elf_object", "sce_module_writer")],
|
|
"-lz", "-o", host_tool], log=BUILD / "host-tool.log")
|
|
libc_builder = BUILD / "host/libc-builder"
|
|
run(["clang++", "-std=c++20", "-O2", "-Wall", "-Wextra", "-Werror",
|
|
native / "libc_builder.cpp", "-o", libc_builder], log=BUILD / "libc-tool.log")
|
|
raw_runtime, runtime = BUILD / "libc.raw.elf", BUILD / "libc.prx"
|
|
run([libc_builder, native / "runtime/api-surface.txt", native / "runtime/imports.txt", raw_runtime])
|
|
verify(raw_runtime, "8ee6e124993e1af26420cb455890fd002f5d6c7e78883c860ce45734e7d002bb")
|
|
run([host_tool, "self", "--sign", "--in", raw_runtime, "--out", runtime])
|
|
verify(runtime, RT_HASH)
|
|
|
|
cross = BUILD / "runtime"
|
|
quickjs = dependency("quickjsSource")
|
|
run([sdk / "bin/prospero-cmake", "-S", ROOT / "native/ps5", "-B", cross,
|
|
f"-DER_ROOT={er}",
|
|
f"-DFETCHCONTENT_SOURCE_DIR_QUICKJS={quickjs}",
|
|
"-DCMAKE_VERBOSE_MAKEFILE=OFF", "-DCMAKE_BUILD_TYPE=Release"], env=env, log=BUILD / "runtime-configure.log")
|
|
run(["cmake", "--build", cross, "--target", "er-bridge-quickjs", "er-software", "-j", "6"],
|
|
env=env, log=BUILD / "runtime-build.log")
|
|
|
|
# native/ps5/layouts.h: each layout's bundle, and its baked assets under their own register function.
|
|
app_layouts = layouts(config)
|
|
layout_dirs = [layout_directory(generated, layout) for layout in app_layouts]
|
|
layouts_c = BUILD / "layouts.c"
|
|
table = ['#include "layouts.h"']
|
|
for i, directory in enumerate(layout_dirs):
|
|
js_bundle = (directory / "app.bundle.js").read_bytes()
|
|
table += [f"void ps5_react_register_assets_{i}(void);",
|
|
f"static const char bundle_{i}[] = {{" + ",".join(str(x) for x in js_bundle) + ",0};"]
|
|
table += ["const Ps5ReactLayout ps5_react_layouts[] = {" + ",".join(
|
|
f"{{{w}, {h}, {sw}, {sh}, bundle_{i}, sizeof bundle_{i} - 1, ps5_react_register_assets_{i}}}"
|
|
for i, (w, h, sw, sh) in enumerate(app_layouts)) + "};",
|
|
f"const unsigned ps5_react_layout_count = {len(app_layouts)};", ""]
|
|
layouts_c.write_text("\n".join(table))
|
|
layout_assets = {directory / "assets.generated.c": [f"-Der_register_assets=ps5_react_register_assets_{i}"]
|
|
for i, directory in enumerate(layout_dirs)}
|
|
definitions = next(line.split("=", 1)[1] for line in
|
|
(cross / "bridge/engine/CMakeFiles/embedded-react.dir/flags.make").read_text().splitlines()
|
|
if line.startswith("C_DEFINES ="))
|
|
sources = [ROOT / "native/ps5/native_host.cpp", ROOT / "native/ps5/async_log.cpp", ROOT / "native/ps5/host_platform.cpp", ROOT / "native/ps5/video_output.cpp", ROOT / "native/ps5/time_compat.c", ROOT / "native/ps5/archive_compat.c",
|
|
ROOT / "native/ps5/filesystem_access.cpp",
|
|
ROOT / "native/ps5/elevation_transport.cpp",
|
|
ROOT / "native/shared/host_api.cpp", ROOT / "native/shared/removals.cpp", ROOT / "native/shared/network.cpp",
|
|
ROOT / "native/shared/network_api.cpp", ROOT / "native/shared/archives.cpp", ROOT / "native/shared/archive_preflight.cpp", ROOT / "native/shared/rar5_password.cpp", ROOT / "native/shared/archive_writer.cpp", ROOT / "native/shared/archive_api.cpp", ROOT / "native/shared/browser_capture_api.cpp", ROOT / "native/shared/lan.cpp", ROOT / "native/shared/lan_api.cpp", ROOT / "native/ps5/browser_capture.cpp", ROOT / "native/shared/package_api.cpp", ROOT / "native/ps5/package_installer.cpp", ROOT / "native/ps5/payload_loader.cpp", ROOT / "native/ps5/relaunch.cpp", ROOT / "native/ps5/crash_log.cpp", ROOT / "native/ps5/utf8_locale.cpp", ROOT / "native/ps5/network_platform.cpp",
|
|
ROOT / "native/shared/image_loader.cpp", ROOT / "native/shared/image_api.cpp",
|
|
ROOT / "native/shared/sound_api.cpp", layout_dirs[0] / "sounds.generated.c",
|
|
ROOT / "native/shared/audio_sink.cpp", ROOT / "native/shared/audio_output_api.cpp", ROOT / "native/ps5/bitstream_output.cpp",
|
|
ROOT / "native/shared/gl_presenter.cpp", ROOT / "native/shared/gl_video.cpp", ROOT / "native/shared/gl_upscale.cpp", ROOT / "native/shared/video_api.cpp",
|
|
ROOT / "native/shared/frame_stats.cpp", ROOT / "native/shared/js_heap.cpp", ROOT / "native/shared/js_profiler.cpp", ROOT / "native/shared/damage_tracker.cpp", ROOT / "native/shared/render_workers.cpp", ROOT / "native/shared/input_script.cpp", ROOT / "native/shared/screenshot.cpp", ROOT / "native/shared/text_shaper.cpp", *layout_assets,
|
|
layouts_c, *[hui / ("src/platform/ps5/" + n + ".cpp") for n in ("display_egl", "pad", "system", "audio_out")],
|
|
hui / "src/core/input.cpp", hui / "src/audio/mixer.cpp", hui / "src/runtime/app_heap.c", hui / "src/runtime/runtime_shims.c",
|
|
native / "app_crt.cpp", native / "app_cpp_runtime.cpp"]
|
|
hb = harfbuzz() / "src"
|
|
# HarfBuzz (MIT): OpenType shaping and outline rasterization for runtime text, single-threaded.
|
|
harfbuzz_sources = [hb / "harfbuzz.cc", hb / "hb-raster-draw.cc", hb / "hb-raster-image.cc"]
|
|
# SheenBidi (Apache-2.0): the Unicode bidirectional algorithm, as one translation unit.
|
|
sb = sheenbidi()
|
|
sources.extend([*harfbuzz_sources, sb / "Source/SheenBidi.c"])
|
|
includes = [hui / "src", ROOT / "native/shared", ROOT / "native/ps5", generated, gl / "include", er / "engine/include", er / "bridges/quickjs",
|
|
er / "backends/software", er / "engine/text", hb, sb / "Headers", sb / "Source", quickjs, stb_image(), archive_ports / "include"]
|
|
if access_client:
|
|
sources.append(access_client / "examples/sandbox-elevation/src/elevation.cpp")
|
|
includes.append(access_client / "examples/sandbox-elevation")
|
|
compatibility_client = access_client or dependency("filesystemHelperClient")
|
|
sources.append(compatibility_client / "examples/pacbrew-curl/compat.c")
|
|
if network_ports:
|
|
sources.extend(access_client / "examples/pacbrew-curl" / name for name in ("netdb.c",))
|
|
includes.append(network_ports / "include")
|
|
# Video: the player and the pinned FFmpeg, only in apps that ask for them.
|
|
video_libs, video_stubs = [], []
|
|
if config.get("video"):
|
|
video_prefix = ffmpeg.ps5(sdk, archive_ports, env)
|
|
sources.extend([ROOT / "native/shared/video_player.cpp", ROOT / "native/shared/video_subtitles.cpp",
|
|
ROOT / "native/ps5/video_decoder.cpp"])
|
|
includes.append(video_prefix / "include")
|
|
video_libs = [video_prefix / "lib" / name for name in ffmpeg.LIBRARIES] + [archive_ports / "lib/libssl.a"]
|
|
# The SDK has no import stub for the hardware decoder: one empty function per call we make, under the
|
|
# module's name. Only these: an import the firmware lacks would keep the whole eboot from loading.
|
|
stub_source = BUILD / "libSceVideodec2.c"
|
|
stub_source.write_text("".join(f"void sceVideodec2{name}(void) {{}}\n" for name in (
|
|
"QueryComputeMemoryInfo", "AllocateComputeQueue", "ReleaseComputeQueue", "QueryDecoderMemoryInfo",
|
|
"CreateDecoder", "DeleteDecoder", "Reset", "Decode", "Flush")))
|
|
video_stubs = [BUILD / "libSceVideodec2.so"]
|
|
run([sdk / "bin/prospero-clang", "-shared", "-nostdlib", "-nodefaultlibs", "-fPIC",
|
|
"-Wl,-soname,libSceVideodec2.sprx", "-o", video_stubs[0], stub_source], env=env, log=BUILD / "videodec2-stub.log")
|
|
objects = []
|
|
for i, source in enumerate(sources):
|
|
obj = BUILD / "obj" / f"{i}-{source.name}.o"
|
|
cpp = source.suffix in (".cpp", ".cc")
|
|
flags = ["-std=c++20", "-fno-exceptions", "-fno-rtti"] if cpp else ["-std=c11"]
|
|
# A video app keeps 448 MiB of flexible memory outside its heap for the hardware decoders' workspaces.
|
|
if source.name == "app_heap.c" and config.get("video"):
|
|
flags = flags + ["-DHUI_HEAP_RESERVE=(448u * 1024u * 1024u)"]
|
|
warnings = (["-w", "-DHB_TINY", "-DHB_HAS_RASTER"] if source in harfbuzz_sources else
|
|
["-w", "-DSB_CONFIG_UNITY"] if source.name == "SheenBidi.c" else ["-Wall", "-Wextra", "-Werror"])
|
|
run(["sh", hui / "tooling/prospero-clang18", *flags, "-O2", *warnings, *layout_assets.get(source, []),
|
|
"-DPROSPERO=1", "-DGL_GLEXT_PROTOTYPES=1", *shlex.split(definitions),
|
|
*[flag for path in includes for flag in ("-I", path)], "-c", source, "-o", obj],
|
|
env=env, log=BUILD / (f"compile-{i}.log"))
|
|
objects.append(obj)
|
|
libs = sdk / "target/lib"
|
|
agc_stubs = [gl / "lib/libSceAgc.so", gl / "lib/libSceAgcDriver.so"]
|
|
network_libs = [network_ports / "lib" / name for name in ("libcurl.a", "libssl.a", "libcrypto.a", "libz.a", "libzstd.a", "libpsl.a")] if network_ports else []
|
|
archive_libs = [archive_ports / "lib" / name for name in ("libarchive.a", "liblzma.a", "libbz2.a", "libzstd.a", "libz.a", "libcrypto.a")]
|
|
pie = BUILD / "llvm-pie.elf"
|
|
wraps = ["malloc", "calloc", "realloc", "free", "posix_memalign", "malloc_usable_size",
|
|
"sceSystemServiceHideSplashScreen", "sceKernelDebugOutText",
|
|
"nl_langinfo", "___mb_cur_max", "mbrtowc", "wcrtomb"]
|
|
if access_client:
|
|
wraps += ["sceNetSocket", "sceNetSetsockopt", "sceNetConnect", "sceNetSend", "sceNetRecv"]
|
|
run([sdk / "bin/prospero-lld", "-L", libs, "-T", native / "ps5-pie.ld", "--eh-frame-hdr",
|
|
"--version-script", native / "app-symbols.map", "-e", "_start", "-o", pie,
|
|
*[f"--wrap={symbol}" for symbol in wraps], "--undefined=ps5_agc_gate2_run", *objects,
|
|
"--start-group", cross / "liber-software.a", cross / "bridge/liber-bridge-quickjs.a",
|
|
cross / "bridge/engine/libembedded-react.a", cross / "_deps/quickjs-build/libqjs.a",
|
|
gl / "lib/libPS5OpenGL.a", libs / "libunwind.a", libs / "libc++abi.a", libs / "libc++.a",
|
|
*video_libs, *network_libs, *archive_libs, builtins, "--end-group", *video_stubs, "--as-needed", *agc_stubs,
|
|
*sorted(libs.glob("*.so"))],
|
|
env=env, log=BUILD / "link.log")
|
|
elf = BUILD / "eboot.elf"
|
|
run([host_tool, "link", "--in", pie, "--out", elf, "--stub-dir", libs,
|
|
*[flag for path in [*agc_stubs, *video_stubs] for flag in ("--stub", path)], "--module-sdk", "0x02000009",
|
|
"--companion-sdk", "0x08050001", "--file-name", "eboot.elf"])
|
|
# The PKG installer runs as a loader payload: an app may not use the console's install service.
|
|
installer = BUILD / "pkg-installer.elf"
|
|
run([sdk / "bin/prospero-clang", "-std=gnu11", "-O2", "-Wall", "-Wextra", "-Werror", "-o", installer,
|
|
ROOT / "native/ps5/payloads/pkg_installer.c", "-lkernel_sys", "-lSceUserService"],
|
|
env=env, log=BUILD / "pkg-installer.log")
|
|
# RAR sets extract in a payload of their own: UnRAR's license keeps its code out of this GPL app.
|
|
rar_extract = build_rar_worker(BUILD / "rar-extract.elf", [sdk / "bin/prospero-clang++"],
|
|
["-I", archive_ports / "include"], [archive_ports / "lib/libcrypto.a", builtins], env)
|
|
# Opens the app again after it closes, which the app itself cannot do.
|
|
relauncher = BUILD / "relauncher.elf"
|
|
run([sdk / "bin/prospero-clang", "-std=gnu11", "-O2", "-Wall", "-Wextra", "-Werror", "-o", relauncher,
|
|
ROOT / "native/ps5/payloads/relauncher.c", "-lSceUserService", "-lSceSystemService"],
|
|
env=env, log=BUILD / "relauncher.log")
|
|
capture = BUILD / "browser-capture.elf"
|
|
browser = ROOT / "native/ps5/payloads/browser"
|
|
run([sdk / "bin/prospero-clang", "-std=gnu11", "-O2", "-Wall", "-Wextra", "-Werror", "-o", capture,
|
|
ROOT / "native/ps5/payloads/browser_capture.c", browser / "browser_platform.c",
|
|
browser / "browser_scan.c", browser / "browser_match.c", browser / "browser_url.c", "-lkernel_sys"],
|
|
env=env, log=BUILD / "browser-capture.log")
|
|
if args.compile_only:
|
|
print(f"PS5 ELF linked: {elf}; no application package generated")
|
|
return
|
|
app = ROOT / "dist" / TITLE
|
|
(app / "sce_sys").mkdir(parents=True, exist_ok=True)
|
|
(app / "sce_module").mkdir(exist_ok=True)
|
|
run([host_tool, "self", "--sign", "--in", elf, "--out", app / "eboot.bin", "--magic", "0x1D3D154F"])
|
|
shutil.copy2(runtime, app / "sce_module/libc.prx")
|
|
shutil.copy2(installer, app / "pkg-installer.elf")
|
|
run([sdk / "bin/prospero-strip", "-o", app / "rar-extract.elf", rar_extract], env=env)
|
|
shutil.copy2(relauncher, app / "relauncher.elf")
|
|
shutil.copy2(capture, app / "browser-capture.elf")
|
|
param = json.loads((hui / "sce_sys/param.json").read_text())
|
|
if refresh_rate_mask(config) & 2:
|
|
param["attribute3"] = param.get("attribute3", 0) | 0x80040 # The console grants 120 Hz output only with these bits.
|
|
if config.get("video"):
|
|
# The most the console gives (from the default 448 MiB); the rest comes out of direct memory.
|
|
param.setdefault("kernel", {})["flexibleMemorySize"] = 1 << 30
|
|
param.update(titleId=TITLE, conceptId=TITLE[4:], contentId=config["contentId"], contentVersion=config["version"])
|
|
param["localizedParameters"]["en-US"]["titleName"] = config["name"]
|
|
(app / "sce_sys/param.json").write_text(json.dumps(param, indent=2) + "\n")
|
|
# An app's own home-screen art (icon0.png 512x512, pic0.png 1920x1080) replaces the proof icon.
|
|
art = sorted((app_dir / "sce_sys").glob("*.png"))
|
|
for path in art:
|
|
shutil.copy2(path, app / "sce_sys" / path.name)
|
|
if not any(path.name == "icon0.png" for path in art):
|
|
# A reproducible proof icon in the demo's existing colors, without kit artwork.
|
|
from PIL import Image, ImageDraw, ImageFont
|
|
image = Image.new("RGB", (512, 512), "#101820")
|
|
draw = ImageDraw.Draw(image)
|
|
font = ImageFont.truetype(str(er / "assets/fonts/Inter-Regular.ttf"), 64)
|
|
draw.text((48, 140), "PS5", font=font, fill="#45d4de")
|
|
draw.text((48, 225), "React", font=font, fill="white")
|
|
image.save(app / "sce_sys/icon0.png")
|
|
shutil.rmtree(app / "fonts", ignore_errors=True)
|
|
for relative, source in resource_files(app_dir, config).items():
|
|
target = app / relative
|
|
target.parent.mkdir(parents=True, exist_ok=True)
|
|
shutil.copy2(source, target)
|
|
fonts = sorted((generated / "fonts").glob("*"))
|
|
if fonts:
|
|
(app / "fonts").mkdir()
|
|
for font in fonts: shutil.copy2(font, app / "fonts" / font.name)
|
|
notices = app / "notices"
|
|
notices.mkdir(exist_ok=True)
|
|
(notices / "Noto-fonts-LICENSE.txt").unlink(missing_ok=True)
|
|
if fonts: shutil.copy2(ROOT / "licenses/Noto-fonts-LICENSE.txt", notices / "Noto-fonts-LICENSE.txt")
|
|
if access_client:
|
|
for name in ("lapy.elf", "lapy-manifest.json"):
|
|
shutil.copy2(helper / name, app / name)
|
|
shutil.copy2(helper / "Lapy-MIT.txt", notices / "Lapy-MIT.txt")
|
|
shutil.copy2(access_client / "LICENSE", notices / "filesystem-helper-client-LICENSE")
|
|
else:
|
|
for name in ("lapy.elf", "lapy-manifest.json"):
|
|
(app / name).unlink(missing_ok=True)
|
|
shutil.copy2(unrar_source() / "license.txt", notices / "unRAR-license.txt")
|
|
stb = (stb_image() / "stb_image.h").read_text()
|
|
(notices / "stb_image-LICENSE").write_text(stb[stb.index("This software is available under 2 licenses"):])
|
|
copy_notices(compatibility_client, notices / "networking")
|
|
shutil.copy2(compatibility_client / "LICENSE", notices / "networking-compat-LICENSE")
|
|
for entry in json.loads((ROOT / "licenses/archives-SOURCES.json").read_text()):
|
|
source = ROOT / "licenses" / entry["file"]
|
|
verify(source, entry["sha256"])
|
|
shutil.copy2(source, notices / entry["file"])
|
|
shutil.copy2(ROOT / "licenses/archives-SOURCES.json", notices / "archives-SOURCES.json")
|
|
for name, source in {
|
|
"ps5-react-LICENSE": ROOT / "LICENSE",
|
|
"ps5-react-LICENSE-ATTRIBUTION": ROOT / "LICENSE-ATTRIBUTION",
|
|
"ps5-react-NOTICE": ROOT / "NOTICE",
|
|
"ps5-homebrew-ui-LICENSE": hui / "LICENSE",
|
|
"embedded-react-LICENSE": er / "LICENSE",
|
|
"embedded-react-engine-LICENSE": er / "engine/LICENSE",
|
|
"QuickJS-LICENSE": quickjs / "LICENSE",
|
|
"harfbuzz-COPYING": hb.parent / "COPYING",
|
|
"SheenBidi-LICENSE": sb / "LICENSE",
|
|
"Inter-LICENSE": hui / "assets/fonts/Inter-LICENSE.txt",
|
|
"material-sounds-NOTICE.txt": ROOT / "licenses/material-sounds-NOTICE.txt",
|
|
"ArtCNN-LICENSE": ROOT / "licenses/ArtCNN-LICENSE",
|
|
"Anime4K-LICENSE": ROOT / "licenses/Anime4K-LICENSE",
|
|
"ps5-opengl-LICENSE": gl.parent / "LICENSE",
|
|
"ps5-opengl-NOTICES.md": gl.parent / "THIRD_PARTY_NOTICES.md",
|
|
}.items():
|
|
shutil.copy2(source, notices / name)
|
|
if (gl.parent / "LICENSES").exists():
|
|
shutil.copytree(gl.parent / "LICENSES", notices / "ps5-opengl", dirs_exist_ok=True)
|
|
for name in ("ffmpeg-LICENSE.md", "ffmpeg-COPYING.LGPLv3", "video-decoder-NOTICE.txt", "audio-passthrough-NOTICE.txt"):
|
|
(notices / name).unlink(missing_ok=True)
|
|
if config.get("video"):
|
|
# FFmpeg as configured is LGPL-3.0-or-later (tools/ffmpeg.py).
|
|
shutil.copy2(ffmpeg.source() / "LICENSE.md", notices / "ffmpeg-LICENSE.md")
|
|
shutil.copy2(ffmpeg.source() / "COPYING.LGPLv3", notices / "ffmpeg-COPYING.LGPLv3")
|
|
shutil.copy2(ROOT / "licenses/video-decoder-NOTICE.txt", notices / "video-decoder-NOTICE.txt")
|
|
shutil.copy2(ROOT / "licenses/audio-passthrough-NOTICE.txt", notices / "audio-passthrough-NOTICE.txt")
|
|
shutil.copy2(ROOT / "docs/DEPENDENCIES.md", notices / "SOURCES.md")
|
|
run([host_tool, "self", "--inspect", "--file", app / "eboot.bin"])
|
|
run([host_tool, "self", "--inspect", "--file", app / "sce_module/libc.prx"])
|
|
receipts = {"title": TITLE, "app": app_dir.name, "framework_version": "0.1.0",
|
|
"hardware_tested": False, "dependencies": LOCK,
|
|
"config": config, "bundle_sha256": {d.name: digest(d / "app.bundle.js") for d in layout_dirs},
|
|
"llvm_version": subprocess.check_output([llvm / "clang", "--version"], text=True).splitlines()[0],
|
|
"sdk_root": str(sdk),
|
|
"network_libraries": {p.name: digest(p) for p in network_libs},
|
|
"sdk_libraries": {p.name: digest(p) for p in sorted(libs.iterdir()) if p.suffix in (".a", ".so")},
|
|
"ui_reference": HUI_REV, "embedded_react": ER_REV,
|
|
"opengl_version": "1.0.0", "opengl_archive_sha256": GL_HASH,
|
|
"compiler_builtins_package_sha256": BUILTINS_HASH,
|
|
"sources": {**{str(p.relative_to(ROOT)): digest(p) for base in (ROOT / "native", ROOT / "runtime", ROOT / "tools", ROOT / "patches")
|
|
for p in sorted(base.rglob("*")) if p.is_file() and "__pycache__" not in str(p)},
|
|
**{"app/" + str(p.relative_to(app_dir)): digest(p) for p in app_files(app_dir)}},
|
|
"files": {str(p.relative_to(app)): digest(p) for p in sorted(app.rglob("*")) if p.is_file()}}
|
|
(ROOT / "dist" / (TITLE + ".receipt.json")).write_text(json.dumps(receipts, indent=2) + "\n")
|
|
with zipfile.ZipFile(ROOT / "dist" / (TITLE + ".zip"), "w", zipfile.ZIP_DEFLATED) as package:
|
|
for path in sorted(app.rglob("*")):
|
|
if path.is_file(): package.write(path, path.relative_to(app.parent))
|
|
run(["python3", ROOT / "tools/verify.py", TITLE])
|
|
print(f"Local title build complete: {app}")
|
|
|
|
if __name__ == "__main__":
|
|
main()
|