PS5_HOST ?= 192.168.137.2
PS5_PORT ?= 9021

ifdef PS5_PAYLOAD_SDK
    include $(PS5_PAYLOAD_SDK)/toolchain/prospero.mk
else
    $(error PS5_PAYLOAD_SDK is undefined)
endif

# Our own build of the SDK's startup code; see sdk-crt.mk for why.
include sdk-crt.mk

ELF := ps5upload.elf

BLAKE3_DIR := third_party/blake3
# Use the SIMD dispatcher + all x86-64 assembly backends (SSE2/SSE4.1/AVX2/AVX512).
# Zen 2 (PS5) supports through AVX2; dispatcher picks best at runtime.
# Portable falls back on unsupported hardware.
BLAKE3_C_SRCS := $(BLAKE3_DIR)/blake3.c \
                 $(BLAKE3_DIR)/blake3_portable.c \
                 $(BLAKE3_DIR)/blake3_dispatch.c
# The C files are built as separate objects at -O2 (review P5): the rest of the payload is
# compiled without -O, which left the BLAKE3 dispatcher, chunk state and parent merges at
# -O0 on every hashed byte (the assembly kernels were unaffected). Third-party code is
# not held to -Werror, like SQLite and Monocypher. The assembly stays in SRCS.
BLAKE3_OBJS := $(patsubst %.c,build/%.o,$(BLAKE3_C_SRCS))
BLAKE3_SRCS := $(BLAKE3_DIR)/blake3_sse2_x86-64_unix.S \
               $(BLAKE3_DIR)/blake3_sse41_x86-64_unix.S \
               $(BLAKE3_DIR)/blake3_avx2_x86-64_unix.S \
               $(BLAKE3_DIR)/blake3_avx512_x86-64_unix.S

# SQLite, compiled into the payload from the amalgamation that
# scripts/install-ps5-sdk.sh fetches and checksum-verifies.
#
# Why the payload carries its own copy: no PS5 firmware ships
# libSceSqlite.sprx anywhere under /system/common/lib, /system/priv/lib or
# /system_ex/common/lib, so dlsym(RTLD_DEFAULT, "sqlite3_open_v2") can never
# resolve on a console. That is a statement about the *platform*, not about
# what we can link — a static library is just more of our own .text.
# ps5-payload-dev/pacbrew-repo builds this exact version for prospero, which
# is where the approach came from.
#
# Costs ~900 KB of .text (~480 KB more in the gzipped bundle copy). Paid
# because the alternative is parsing SQLite's on-disk b-tree format by hand,
# which can only ever read — and repairing a console that lists a title it
# refuses to delete means writing.
SQLITE_DIR := third_party/sqlite3
# The object lands here, NOT in $(SQLITE_DIR): CI installs the SDK (and with
# it these sources) under sudo, so the source directory ends up root-owned
# while the build step runs as the normal user. Writing the object beside
# the sources would fail with EACCES there.
SQLITE_OBJ := sqlite3.o
# Monocypher (X25519 + constant-time helpers for AVA1), compiled like SQLite: third-party
# code is not held to our -Werror policy.
MONO_DIR := third_party/monocypher
MONO_OBJ := monocypher.o
AVA1_SRCS := ava1/ava1_wire.c ava1/ava1_frame.c ava1/ava1_keys.c ava1/ava1_noise.c ava1/ava1_conn.c \
             ava1/ava1_store.c ava1/ava1_pairlimit.c ava1/ava1_server.c ava1/ava1_trust.c ava1/platform_ps5.c \
             ava1/ava1_aead.c ava1/ava1_chacha_avx2.c ava1/ava1_b3.c ava1/ava1_ranges.c \
             ava1/ava1_journal.c ava1/ava1_tune.c ava1/ava1_thread.c ava1/ava1_manifest.c \
             ava1/ava1_job.c ava1/ava1_data.c ava1/ava1_apply.c ava1/ava1_recv.c ava1/ava1_send.c \
             ava1/ava1_copy.c ava1/ava1_calibrate.c ava1/ava1_op.c ava1/ava1_events.c \
             ava1/gen/ava1_gen.c src/ava1_glue.c
# AVA1 is built as objects at -O2: it seals every byte of a transfer, and the rest of the
# payload is compiled without -O (i.e. -O0). The AVX2 ChaCha20 alone gets -mavx2;
# ava1_aead.c calls it only once CPUID has confirmed AVX2, so the ELF stays safe on any
# x86-64. payload/build/ is git-ignored.
AVA1_OBJS := $(patsubst %.c,build/ava1/%.o,$(AVA1_SRCS))
AVA1_CFLAGS = $(CFLAGS) -O2

# Only the compile-time options that are safe with the *prebuilt*
# amalgamation. SQLite's grammar-affecting SQLITE_OMIT_* flags (OMIT_VACUUM,
# OMIT_WINDOWFUNC, OMIT_CTE, …) require regenerating the parser from
# canonical sources; set against the amalgamation they leave the
# pre-generated parser calling functions that no longer exist and the link
# fails. Verified one at a time — do not add an OMIT flag here without
# rebuilding.
#
#   TEMP_STORE=2     temp b-trees in memory; /system_data is not reliably
#                    writable and a spill there would fail mid-query.
#   THREADSAFE=1     serialised. Several connection threads reach the
#                    content-database handlers concurrently.
#   OMIT_LOAD_EXTENSION  no dlopen of arbitrary .so from a database file.
#   DQS=0            double-quoted string literals are an error, not a
#                    silent fallback to a string when an identifier is
#                    misspelled.
#
# UTF-16 is deliberately NOT omitted: it saves 18 KB and would break
# outright on a database written with a UTF-16 encoding.
SQLITE_CFLAGS := -Os -g0 \
                 -DSQLITE_OS_UNIX=1 \
                 -DSQLITE_THREADSAFE=1 \
                 -DSQLITE_TEMP_STORE=2 \
                 -DSQLITE_OMIT_LOAD_EXTENSION=1 \
                 -DSQLITE_OMIT_DEPRECATED=1 \
                 -DSQLITE_OMIT_JSON=1 \
                 -DSQLITE_DEFAULT_MEMSTATUS=0 \
                 -DSQLITE_DQS=0 \
                 -DSQLITE_UNTESTABLE=1

INCLUDES := -Iinclude -I$(BLAKE3_DIR) -I$(SQLITE_DIR) -Ithird_party/tiny_aes -Iava1 -Iava1/gen -Ithird_party/monocypher
CFLAGS := -Wall -Wextra -Werror -g $(INCLUDES)

# Sony sprx linkage. The payload calls sceLncUtilLaunchApp +
# sceKernelGetCpuTemperature etc., and rtld must run those
# libraries' init functions before main() invokes them. With
# compile-time linkage (DT_NEEDED entries here), the rtld walks
# init order correctly. An earlier dlopen-at-runtime path wedged
# on FW 9.60 because some Sony APIs reach into per-library
# internal state that gets initialised lazily — by the time we
# called the API, the state hadn't been built yet.
#
# register.c keeps its dlsym-via-RTLD_DEFAULT resolution as a
# backstop: on a firmware where one of these libraries fails to
# load, dlsym returns NULL for the missing symbols and the
# affected handler returns a clear "service_unavailable" error
# instead of crashing. Belt-and-braces.
# Compile-time linkage: ONLY libraries we know are present on every
# supported PS5 firmware. A missing DT_NEEDED entry makes rtld refuse
# to load the binary at all (no toast, no port bind, loader silently
# rejects the ELF — exact symptom we hit when the previous build
# added libSceFsInternalForVsh / libSceNet / libSceSysCore here).
# Optional features (sceFsMountGamePkg, sceNetGetIfList, etc.)
# dlopen their backing SPRX at runtime — missing SPRX → handler
# returns "service_unavailable" instead of bricking the payload.
#
# `-lkernel_sys` enables compile-time NID resolution for sceKernel* symbols
# (sceKernelGetCpuTemperature / sceKernelGetSocSensorTemperature /
# sceKernelGetCpuFrequency / sceKernelGetSocPowerConsumption). The older path
# used `dlsym(RTLD_DEFAULT, "sceKernelGetCpuTemperature")` which fails on FW
# points where libkernel exports NID-only, then fell back to ptrace-into-
# SceShellUI — which destabilises the console (screen blank, occasional
# power-off). Direct compile-time linkage gives stable per-call sensor reads
# with no ptrace dance. libkernel_sys.sprx is part of every supported PS5
# firmware so adding the DT_NEEDED entry is safe.
#
# libSceRegMgr is linked for the same reason. sys_registry.c resolves the
# sceRegMgr* symbols with dlsym(RTLD_DEFAULT), and on a payload that does
# NOT link the library those lookups all return NULL — every registry read
# and write failed with SYS_REGISTRY_ERR_NO_SYMBOL (0xE0003002), silently,
# because the callers passed NULL for the error code. Verified on hardware:
# the user-slot table read back empty and Remote Play could never find the
# logged-in user. The registry is core to the system (unlike the optional
# libSceRemoteplay), so the DT_NEEDED entry is safe.
# libSceIpmi is linked BEFORE libSceAppInstUtil on upstream's explicit
# instruction: ps5-payload-dev/sdk#26 (still open) — "you need to explicitly
# link with -lSceIpmi ... put -lSceIpmi before -lSceAppInstUtil, the order is
# important". The maintainer treats it as a bug in Sony's rtld: AppInstUtil
# depends on Ipmi, and without Ipmi loaded first the executable "does not work
# correctly". The SDK's own samples/install_app links it exactly this way.
#
# Why it is easy to miss: when the HOST process a loader injects into already
# has libSceIpmi loaded, the missing dependency is invisible — so a payload can
# work under one loader and break under another. Several popular payloads omit
# it and work, so this is hardening that follows upstream, not a proven cause
# of any specific report.
LIBS := -lSceSystemService -lSceUserService -lSceIpmi -lSceAppInstUtil \
        -lSceNotification -lSceRegMgr -lSceFsInternalForVsh -lkernel_sys

# The home-screen tile's icon0.png (512x512, the app icon client/src-tauri/icons/icon.png),
# embedded as a C array by xxd: webui_tile_icon0_png[] + webui_tile_icon0_png_len, which
# webui_tile.c writes into /data/ps5upload/tile/PSUP00001/sce_sys/. Generated under build/
# (git-ignored); `cd assets` keeps xxd's symbol names independent of the path.
WEBUI_TILE_ICON_PNG := assets/webui_tile_icon0.png
WEBUI_TILE_ICON_C := build/webui_tile_icon.c

# The console web page, built from the client (`npm run build:console`: one self-contained
# client/dist-console/index.html), gzip'd and embedded by xxd as webui_page_gz[] +
# webui_page_gz_len (include/webui_page.h), which the helper serves at GET /. Needs the client's
# node_modules (`npm ci` in client/). Rebuilt when the page's sources, the locale files (its
# strings), the PC app's tokens (src/index.css) or the build config change.
WEBUI_PAGE_C := build/webui_page.c
# The same build writes client/dist-console/strings/<lang>.json (every language but English, which
# is inside the page); scripts/embed-console-strings.mjs gzips each into build/webui_strings.c
# (include/webui_strings.h), served at GET /strings/<lang>.json.
WEBUI_STRINGS_C := build/webui_strings.c
# And its offline copy (client/src/console/offline/offlineCopy.ts): dist-console/sw.js and
# dist-console/cache.appcache, embedded by xxd as webui_sw_js[] and webui_cache_appcache[]
# (include/webui_offline.h), served at GET /sw.js and GET /cache.appcache.
WEBUI_OFFLINE_C := build/webui_offline.c
CLIENT_DIR := ../client
WEBUI_PAGE_DEPS := $(shell find $(CLIENT_DIR)/src/console -type f ! -name '*.test.ts' 2>/dev/null) \
        $(wildcard $(CLIENT_DIR)/src/i18n/locales/*.ts) $(CLIENT_DIR)/src/index.css \
        $(CLIENT_DIR)/public/logo-square.png $(CLIENT_DIR)/vite.console.config.ts \
        $(CLIENT_DIR)/package.json $(CLIENT_DIR)/package-lock.json ../scripts/build-console-strings.mjs

SRCS := src/main.c src/runtime.c src/shell_builtin.c src/takeover.c src/takeover_flag.c src/ownership_record.c src/ava1_stop.c src/register.c src/hw_info.c \
        src/drive_sensors.c \
        src/proc_list.c src/ptrace_remote.c src/shellui_rpc.c \
        src/sony_api_lock.c src/kernel_rw_lock.c src/sys_time.c \
        src/sys_registry.c src/smp_meta.c src/profile.c \
        src/backup.c src/remoteplay.c src/rp_pair.c src/fan_curve.c src/fan_map.c src/indicator.c src/notif.c \
        src/wake_watchdog.c src/icon_keeper.c src/ffpfsc_unpack.c src/cheats.c src/cheats_plan.c src/cheats_list.c src/xml_encoding.c src/activity.c src/sdk_changer.c src/fakelib_overlay.c \
        src/tmdb.c src/fw_spoof.c src/focus_probe.c \
        src/content_db.c src/mgmt_rpc.c src/fs_jobs.c src/net_probe.c src/net_ports.c src/mgmt_fs.c src/path_policy.c \
        src/webui_http.c src/webui_auth.c src/webui_server.c src/webui_codec.c src/webui_api.c \
        src/webui_settings.c src/webui_inputlog.c src/webui_files.c src/webui_cheats_store.c src/webui_tile.c src/webui_strings.c src/webui_offline.c src/files_prefs.c src/files_handoff.c $(WEBUI_TILE_ICON_C) \
        $(WEBUI_PAGE_C) $(WEBUI_STRINGS_C) $(WEBUI_OFFLINE_C) \
        third_party/tiny_aes/aes.c \
        $(BLAKE3_SRCS)

# Headers as Make prerequisites so editing a .h triggers a rebuild. This
# is belt-and-braces given the recipe below re-compiles every .c every
# time, but it guards against the one case where the build system
# otherwise misses: `touch include/foo.h` alone, with no .c change.
HEADERS := $(wildcard include/*.h) $(wildcard $(BLAKE3_DIR)/*.h) $(wildcard ava1/*.h) $(wildcard ava1/gen/*.h)

all: $(ELF) $(ELF).gz

# Third-party code is not held to our warning policy: sqlite3.c is a
# generated 9 MB translation unit and -Wall -Wextra -Werror on it would make
# every upstream SQLite release a build break for us. Compiled separately so
# our own sources keep -Werror.
$(SQLITE_OBJ): $(SQLITE_DIR)/sqlite3.c $(SQLITE_DIR)/sqlite3.h
	$(CC) $(SQLITE_CFLAGS) -c -o $@ $<

$(MONO_OBJ): $(MONO_DIR)/monocypher.c $(MONO_DIR)/monocypher.h
	$(CC) -O2 -g0 -I$(MONO_DIR) -c -o $@ $<

$(SQLITE_DIR)/sqlite3.c $(SQLITE_DIR)/sqlite3.h:
	@echo "ERROR: SQLite sources are missing from $(SQLITE_DIR)." >&2
	@echo "       Run scripts/install-ps5-sdk.sh to fetch them." >&2
	@false

# The Makefile itself is a prerequisite: LIBS and CFLAGS live here, and
# without this a change to either printed "Built" while leaving the previous
# link in place. Adding -lSceIpmi did exactly that — the stale ELF had no
# libSceIpmi in DT_NEEDED until the sources were touched.
$(ELF): $(SRCS) $(HEADERS) $(SQLITE_OBJ) $(MONO_OBJ) $(BLAKE3_OBJS) $(AVA1_OBJS) $(SDK_CRT1) Makefile
	$(CC) $(CFLAGS) -nostartfiles -o $@ $(SRCS) $(BLAKE3_OBJS) $(AVA1_OBJS) $(SQLITE_OBJ) $(MONO_OBJ) $(SDK_CRT1) $(LIBS)

build/$(BLAKE3_DIR)/%.o: $(BLAKE3_DIR)/%.c $(HEADERS) Makefile
	@mkdir -p $(dir $@)
	$(CC) $(filter-out -Werror,$(CFLAGS)) -O2 -c -o $@ $<

build/ava1/%.o: %.c $(HEADERS) Makefile
	@mkdir -p $(dir $@)
	$(CC) $(AVA1_CFLAGS) -c -o $@ $<

build/ava1/ava1/ava1_chacha_avx2.o: ava1/ava1_chacha_avx2.c $(HEADERS) Makefile
	@mkdir -p $(dir $@)
	$(CC) $(AVA1_CFLAGS) -mavx2 -c -o $@ $<

$(WEBUI_TILE_ICON_C): $(WEBUI_TILE_ICON_PNG) Makefile
	@mkdir -p $(dir $@)
	(cd assets && xxd -i webui_tile_icon0.png) > $@.tmp && mv $@.tmp $@

# `cd build` keeps xxd's symbol names webui_page_gz / webui_page_gz_len.
$(WEBUI_PAGE_C): $(WEBUI_PAGE_DEPS) Makefile
	@mkdir -p $(dir $@)
	npm --prefix $(CLIENT_DIR) run build:console
	gzip -9n -c $(CLIENT_DIR)/dist-console/index.html > build/webui_page.gz.tmp
	mv build/webui_page.gz.tmp build/webui_page.gz
	(cd build && xxd -i webui_page.gz) > $@.tmp && mv $@.tmp $@

# Written from the dist-console/strings the page rule's build just produced.
$(WEBUI_STRINGS_C): $(WEBUI_PAGE_C) ../scripts/embed-console-strings.mjs Makefile
	@mkdir -p $(dir $@)
	node ../scripts/embed-console-strings.mjs $(CLIENT_DIR)/dist-console/strings $@

# Copied under build/ first so xxd names the arrays webui_sw_js / webui_cache_appcache.
$(WEBUI_OFFLINE_C): $(WEBUI_PAGE_C) Makefile
	@mkdir -p $(dir $@)
	cp $(CLIENT_DIR)/dist-console/sw.js build/webui_sw.js
	cp $(CLIENT_DIR)/dist-console/cache.appcache build/webui_cache.appcache
	(cd build && xxd -i webui_sw.js && xxd -i webui_cache.appcache) > $@.tmp && mv $@.tmp $@

# Gzipped copy for the Tauri bundle resource. linuxdeploy (Linux
# AppImage bundler) walks every ELF in the AppDir and tries to
# resolve its .so deps via `patchelf --print-needed`. The PS5
# payload's FreeBSD ELF depends on PS5 sprx modules that can never
# exist on Linux, so linuxdeploy aborts the build. The .gz form
# shows gzip magic (`\x1f\x8b`) instead of ELF magic — bundler sees
# a regular data file and ships it as a resource. The host decompresses
# on first use (probes.rs::find_bundled_payload).
$(ELF).gz: $(ELF)
	gzip -kf $<

clean:
	rm -f $(ELF) $(ELF).gz $(SQLITE_OBJ) $(MONO_OBJ)
	rm -rf build/ava1 $(SDK_CRT_BUILD)
	rm -f $(WEBUI_TILE_ICON_C) $(WEBUI_PAGE_C) $(WEBUI_STRINGS_C) $(WEBUI_OFFLINE_C) build/webui_page.gz \
		build/webui_sw.js build/webui_cache.appcache

send: $(ELF)
	@echo "Sending $(ELF) to $(PS5_HOST):$(PS5_PORT) ..."
	nc -w 1 $(PS5_HOST) $(PS5_PORT) < $(ELF)
	@echo "Sent — wait for PS5 notification before testing"

.PHONY: all clean send
